From 5e1e892a8403176b7b5f806dd7dff7c12a7320d5 Mon Sep 17 00:00:00 2001 From: "Vaish, Ishan" Date: Fri, 19 Jun 2026 03:50:37 -0400 Subject: [PATCH 1/2] Add up/down arrow reordering for screen reader users on Cards screen Drag-and-drop reordering is not reliably accessible to screen reader users. When a screen reader is active, replace the drag handle with up/down arrow buttons that move a card by one position per tap, using the same underlying reorder logic as drag-and-drop. --- lib/ui/profile/cards.dart | 66 ++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/lib/ui/profile/cards.dart b/lib/ui/profile/cards.dart index ea9b86276..d66a50d93 100644 --- a/lib/ui/profile/cards.dart +++ b/lib/ui/profile/cards.dart @@ -28,10 +28,11 @@ class _CardsViewState extends State { } Widget buildCardsList() { + final screenReaderActive = MediaQuery.accessibleNavigationOf(context); var tempView = ReorderableListView( header: Padding( padding: const EdgeInsets.only(top: 10), - child: Text("Hold and drag to reorder", + child: Text(screenReaderActive ? "Use the up and down arrows to reorder" : "Hold and drag to reorder", textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall), ), children: createList(), @@ -74,19 +75,28 @@ class _CardsViewState extends State { return list; } - for (String card in _cardsDataProvider.cardOrder) { - try { - // Skip cards that aren't available - if (_cardsDataProvider.availableCards[card] == null) continue; + // Cards actually rendered, in display order - used to know which card is + // first/last so the up/down arrows can be disabled at the boundaries. + final visibleCards = + _cardsDataProvider.cardOrder.where((card) => _cardsDataProvider.availableCards[card] != null).toList(); + final screenReaderActive = MediaQuery.accessibleNavigationOf(context); + for (var i = 0; i < visibleCards.length; i++) { + final card = visibleCards[i]; + try { list.add( Card( key: Key(card), elevation: 2.0, margin: EdgeInsets.fromLTRB(cardMargin, 5, cardMargin, 5), child: ListTile( - leading: Icon(Icons.drag_handle, - color: Theme.of(context).brightness == Brightness.dark ? linkTextColorDark : linkTextColorLight), + // Dragging to reorder isn't reliably accessible to screen reader + // users, so up/down arrow buttons are shown instead whenever a + // screen reader is active. + leading: screenReaderActive + ? _buildReorderArrows(card, isFirst: i == 0, isLast: i == visibleCards.length - 1) + : Icon(Icons.drag_handle, + color: Theme.of(context).brightness == Brightness.dark ? linkTextColorDark : linkTextColorLight), title: Text(_cardsDataProvider.availableCards[card]!.titleText, style: Theme.of(context).textTheme.bodyMedium), trailing: Transform.scale( @@ -115,4 +125,46 @@ class _CardsViewState extends State { } return list; } + + Widget _buildReorderArrows(String card, {required bool isFirst, required bool isLast}) { + final title = _cardsDataProvider.availableCards[card]!.titleText; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.arrow_upward), + iconSize: 20, + padding: EdgeInsets.zero, + constraints: BoxConstraints(minWidth: 32, minHeight: 32), + tooltip: 'Move $title up', + onPressed: isFirst ? null : () => _moveCard(card, -1), + ), + IconButton( + icon: Icon(Icons.arrow_downward), + iconSize: 20, + padding: EdgeInsets.zero, + constraints: BoxConstraints(minWidth: 32, minHeight: 32), + tooltip: 'Move $title down', + onPressed: isLast ? null : () => _moveCard(card, 1), + ), + ], + ); + } + + /// Moves [card] by [delta] positions (e.g. -1 to move up, +1 to move down) + /// within the stored card order - the same effect as dragging it, just + /// driven by a button tap instead of a drag gesture. + void _moveCard(String card, int delta) { + final order = _cardsDataProvider.cardOrder; + final currentIndex = order.indexOf(card); + if (currentIndex == -1) return; + final newIndex = currentIndex + delta; + if (newIndex < 0 || newIndex >= order.length) return; + setState(() { + order.insert(newIndex, order.removeAt(currentIndex)); + // Checks against stored user order in remote profile + _cardsDataProvider.updateCardOrder(isUserReorder: true); + }); + } } From c874716d498c4c9d8a7cbb144d059df8700cd081 Mon Sep 17 00:00:00 2001 From: "Vaish, Ishan" Date: Mon, 22 Jun 2026 01:25:44 -0500 Subject: [PATCH 2/2] Reorder cards relative to visible items --- lib/ui/profile/cards.dart | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/ui/profile/cards.dart b/lib/ui/profile/cards.dart index d66a50d93..b9b5d5f6a 100644 --- a/lib/ui/profile/cards.dart +++ b/lib/ui/profile/cards.dart @@ -94,7 +94,7 @@ class _CardsViewState extends State { // users, so up/down arrow buttons are shown instead whenever a // screen reader is active. leading: screenReaderActive - ? _buildReorderArrows(card, isFirst: i == 0, isLast: i == visibleCards.length - 1) + ? _buildReorderArrows(card, visibleCards, isFirst: i == 0, isLast: i == visibleCards.length - 1) : Icon(Icons.drag_handle, color: Theme.of(context).brightness == Brightness.dark ? linkTextColorDark : linkTextColorLight), title: Text(_cardsDataProvider.availableCards[card]!.titleText, @@ -126,7 +126,7 @@ class _CardsViewState extends State { return list; } - Widget _buildReorderArrows(String card, {required bool isFirst, required bool isLast}) { + Widget _buildReorderArrows(String card, List visibleCards, {required bool isFirst, required bool isLast}) { final title = _cardsDataProvider.availableCards[card]!.titleText; return Column( mainAxisSize: MainAxisSize.min, @@ -138,7 +138,7 @@ class _CardsViewState extends State { padding: EdgeInsets.zero, constraints: BoxConstraints(minWidth: 32, minHeight: 32), tooltip: 'Move $title up', - onPressed: isFirst ? null : () => _moveCard(card, -1), + onPressed: isFirst ? null : () => _moveCard(card, visibleCards, -1), ), IconButton( icon: Icon(Icons.arrow_downward), @@ -146,23 +146,28 @@ class _CardsViewState extends State { padding: EdgeInsets.zero, constraints: BoxConstraints(minWidth: 32, minHeight: 32), tooltip: 'Move $title down', - onPressed: isLast ? null : () => _moveCard(card, 1), + onPressed: isLast ? null : () => _moveCard(card, visibleCards, 1), ), ], ); } - /// Moves [card] by [delta] positions (e.g. -1 to move up, +1 to move down) - /// within the stored card order - the same effect as dragging it, just - /// driven by a button tap instead of a drag gesture. - void _moveCard(String card, int delta) { + /// Moves [card] next to the adjacent visible card. The stored order may + /// contain unavailable cards, so raw list indices cannot drive this action. + void _moveCard(String card, List visibleCards, int delta) { final order = _cardsDataProvider.cardOrder; + final visibleIndex = visibleCards.indexOf(card); + final targetVisibleIndex = visibleIndex + delta; + if (visibleIndex == -1 || targetVisibleIndex < 0 || targetVisibleIndex >= visibleCards.length) return; + + final targetCard = visibleCards[targetVisibleIndex]; final currentIndex = order.indexOf(card); - if (currentIndex == -1) return; - final newIndex = currentIndex + delta; - if (newIndex < 0 || newIndex >= order.length) return; + final targetIndex = order.indexOf(targetCard); + if (currentIndex == -1 || targetIndex == -1) return; + setState(() { - order.insert(newIndex, order.removeAt(currentIndex)); + order.removeAt(currentIndex); + order.insert(targetIndex, card); // Checks against stored user order in remote profile _cardsDataProvider.updateCardOrder(isUserReorder: true); });