Extras

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:

  1. Expanded card content was too long to fit on screen → user loses visual context.
  2. Expansion caused content to jump unpredictably → not sticking to top.
  3. 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 Bloc when expanded.

4. Core Properties

PropertyTypePurpose
ordersList<Order>Order data to render
isShowingTradeAccountOrderHistoryboolControls if customer names are shown
isOverviewSectionboolShows only top 3 orders when true
expandedIndexint?Keeps track of expanded tile
_scrollControllerScrollControllerAttached 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?

KeyUsed WhenReason
tileKeyAfter details content loadsEnsures tile scrolls correctly from sub-widget load
endOfDetailsKeyWhen expansion startsEnsures 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 ExpansionTile opens.
  • 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 Element for 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

10. References


Copyright © 2026