diff --git a/lib/ui/profile/cards.dart b/lib/ui/profile/cards.dart index ea9b86276..b9b5d5f6a 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, 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, style: Theme.of(context).textTheme.bodyMedium), trailing: Transform.scale( @@ -115,4 +125,51 @@ class _CardsViewState extends State { } return list; } + + Widget _buildReorderArrows(String card, List visibleCards, {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, visibleCards, -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, visibleCards, 1), + ), + ], + ); + } + + /// 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); + final targetIndex = order.indexOf(targetCard); + if (currentIndex == -1 || targetIndex == -1) return; + + setState(() { + order.removeAt(currentIndex); + order.insert(targetIndex, card); + // Checks against stored user order in remote profile + _cardsDataProvider.updateCardOrder(isUserReorder: true); + }); + } }