Modern Order History – Auto-Scroll Expansion Tile Behaviour
1. Overview
The Modern Order History page contains expandable order tiles. Each tile can reveal a large set of order details, which often exceeds the screen height.
To improve usability, an automatic scroll behaviour was implemented:
- When an item expands, the card scrolls to the correct position so the content starts at the top.
- When the item contracts, the list maintains the user's current scroll position.
- The scroll logic ensures tiles do not jump unexpectedly and maintains UX consistency.
This document describes the implementation, behaviours, key components, and internal logic.
2. Goal
Solve the following issues:
- Expanded card content was too long to fit on screen → user loses visual context.
- Expansion caused content to jump unpredictably → not sticking to top.
- When collapsing, the list repositions incorrectly → unexpected scroll jumps.
Solution:
Precisely control scroll behaviour using keyed elements + viewport visibility checks + Scrollable.ensureVisible().
3. Class: OrdersExpansionPanel
- Render a scrollable list of expansion tiles.
- Handle expansion logic.
- Manage scroll adjustments using element keys.
- Load order details via a
Blocwhen expanded.
4. Core Properties
| Property | Type | Purpose |
|---|---|---|
orders | List<Order> | Order data to render |
isShowingTradeAccountOrderHistory | bool | Controls if customer names are shown |
isOverviewSection | bool | Shows only top 3 orders when true |
expandedIndex | int? | Keeps track of expanded tile |
_scrollController | ScrollController | Attached to ListView for programmatic scrolling |
5. Key-Based Scrolling Strategy
Every tile uses two keys:
tileKey: Represents the collapsed tile.endOfDetailsKey: Represents the end marker inside expanded details.
5.1 Why two keys?
| Key | Used When | Reason |
|---|---|---|
tileKey | After details content loads | Ensures tile scrolls correctly from sub-widget load |
endOfDetailsKey | When expansion starts | Ensures top of tile stays in view |
This prevents the tile from being partially off-screen and avoids layout jumps.
6. Expand Behaviour(onExpansionChanged: true)
onExpansionChanged
- Triggered immediately when the
ExpansionTileopens. - The scroll happens after a small delay to allow the widget to finish opening.
- It scrolls to the bottom of the expanded content (
endOfDetailsKey) so the start of the details becomes visible. - This ensures the user sees the detailed section without missing content hidden off-screen.
whenLoaded callback (inside the BLoC)
- This runs after the order details finish loading asynchronously.
- The details section might grow in height once relevant data arrives.
- After loading completes, we again scroll (this time to the
tileKey), ensuring the entire card remains visible on screen. - This prevents UI jumps or the expanded view being pushed off-screen once data arrives.
Why do we scroll twice?
Because the user sees two visual changes at two different moments:
- UI expands immediately → scroll once to ensure initial (card with loading indicator) section is visible.
- More data arrives and rebuilds the widget → scroll again to maintain visibility after layout changes.
This double-scroll ensures a smooth, polished expansion experience where the user never loses context, even while async loading updates the layout.
7 Collapse Behaviour (onExpansionChanged: false)
If the same tile is being collapsed:
expandedIndex = null
- No scroll occurs — user stays where they are.
8. Internal Helper Methods
8.1 void _scrollToValueKey({required ValueKey key, bool returnIfVisible})
1. Logic:
- Checks if the widget is already fully visible using
_isFullyVisible(only if returnIfVisible is true). If so, returns early. - Finds the widget's BuildContext using
_findElementByKey. Uses Scrollable.ensureVisible()to perform the scroll.
2. Alignment Policy:
- returnIfVisible: true: Uses ScrollPositionAlignmentPolicy.keepVisibleAtEnd. This is less intrusive, only scrolling enough to bring the item into view (used for endOfDetailsKey).
- returnIfVisible: false: Uses ScrollPositionAlignmentPolicy.explicit with alignment: 0.0. This forces the item to the top of the viewport (used for tileKey).
8.2 bool _isFullyVisible({required ValueKey key})
- Used to prevent unnecessary scrolling.
- Gets widget's global position
- Uses a reduced viewport height (screen – 100px)
- Ensures the entire widget fits inside the visible area before skipping scroll
8.3 BuildContext? _findElementByKey()
- Tree walker that traverses the widget hierarchy to find the associated
Elementfor a given key.
9 UX Behaviour Summary
9.1 When expanding:
- Tile animates open
- After animation completes, tile scrolls to bring content to top
- Once details load, minor correction scroll ensures full visibility
9.2 When collapsing:
- No scroll changes
- User remains at the same scroll offset
9.3 Ensures:
- Zero jumpiness
- Smooth animations
- Predictable scroll behaviour
- Large content remains usable