diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart index a285e9cac..9c9294648 100644 --- a/lib/new-ui/new_dashboard.dart +++ b/lib/new-ui/new_dashboard.dart @@ -90,14 +90,18 @@ class _NewDashboardState extends State { ), ), ), - SafeArea( - bottom: !(Platform.isIOS), - child: SizedBox( - width: double.infinity, - height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding, - child: AbsorbPointer( - absorbing: true, - child: Container(color: Colors.transparent), + // Invisible pointer-absorbing strip behind the nav bar: it must not + // be reachable by screen-reader traversal either. + ExcludeSemantics( + child: SafeArea( + bottom: !(Platform.isIOS), + child: SizedBox( + width: double.infinity, + height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding, + child: AbsorbPointer( + absorbing: true, + child: Container(color: Colors.transparent), + ), ), ), ), diff --git a/lib/new-ui/pages/account_customizer.dart b/lib/new-ui/pages/account_customizer.dart index 8ebaf4c20..ff9d6f59c 100644 --- a/lib/new-ui/pages/account_customizer.dart +++ b/lib/new-ui/pages/account_customizer.dart @@ -174,17 +174,29 @@ class _AccountCustomizerState extends State { itemCount: _items.length, itemBuilder: (BuildContext context, int index) { final card = _items[index].card; + // The stack is ordered bottom to top, so the last item + // is the account currently in front — the selected one. + final selectedItemIndex = _items.length - 1; return Container( key: ValueKey(index), - child: GestureDetector( - onTap: () { - reorder(index, _items.length); - }, - child: Align( - alignment: Alignment.topCenter, - heightFactor: _kStackVisibleFactor, - child: card, + // One labeled, selectable node per account; the card's own + // texts stay reachable underneath it. + child: Semantics( + button: true, + selected: selectedItemIndex == index, + label: _items[index].accountListItem.label, + onTap: () => reorder(index, _items.length), + child: GestureDetector( + excludeFromSemantics: true, + onTap: () { + reorder(index, _items.length); + }, + child: Align( + alignment: Alignment.topCenter, + heightFactor: _kStackVisibleFactor, + child: card, + ), ), ), ); @@ -199,31 +211,36 @@ class _AccountCustomizerState extends State { padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Material( color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(999999), - onTap: _showAddAccountModal, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(999999)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 18.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, - children: [ - Icon( - Icons.add, - size: 28, - color: Theme.of(context).colorScheme.primary, - ), - Text( - S.of(context).add_account, - style: TextStyle( + child: MergeSemantics( + child: Semantics( + button: true, + child: InkWell( + borderRadius: BorderRadius.circular(999999), + onTap: _showAddAccountModal, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999999)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Icon( + Icons.add, + size: 28, color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500), - ) - ], + ), + Text( + S.of(context).add_account, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500), + ) + ], + ), + ), ), ), ), @@ -406,6 +423,8 @@ class _AccountCreationModalState extends State { final TextEditingController _controller = TextEditingController(); bool _loading = false; + Future _generateAccountName() async => _controller.text = await generateName(); + @override Widget build(BuildContext context) { return Container( @@ -441,18 +460,23 @@ class _AccountCreationModalState extends State { ), Padding( padding: const EdgeInsets.all(12.0), - child: GestureDetector( - onTap: () async { - _controller.text = await generateName(); - }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(5)), - child: CakeImageWidget( - imageUrl: "assets/new-ui/randomize.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn), + child: Semantics( + button: true, + label: S.of(context).generate_name, + onTap: _generateAccountName, + child: ExcludeSemantics( + child: GestureDetector( + onTap: _generateAccountName, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(5)), + child: CakeImageWidget( + imageUrl: "assets/new-ui/randomize.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + ), ), ), ), diff --git a/lib/new-ui/widgets/apps_widget.dart b/lib/new-ui/widgets/apps_widget.dart index d440d91bc..63cd77f4d 100644 --- a/lib/new-ui/widgets/apps_widget.dart +++ b/lib/new-ui/widgets/apps_widget.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; import 'package:flutter/material.dart'; @@ -60,71 +61,86 @@ class AppsWidget extends StatelessWidget { end: Alignment.bottomCenter, ), ), - child: TextButton( - onPressed: onTap, - style: TextButton.styleFrom( - shape: RoundedSuperellipseBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide( - width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh), - ), - padding: EdgeInsets.all(24), - ), - child: Column( - children: [ - Row( + // The card is one node; the arrow glyph's "leaves the app" meaning + // becomes a hint instead of an unlabeled icon. + child: MergeSemantics( + child: Semantics( + hint: isLink == true ? S.of(context).opens_externally : null, + child: TextButton( + onPressed: onTap, + style: TextButton.styleFrom( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh), + ), + padding: EdgeInsets.all(24), + ), + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(right: 20), - child: CakeImageWidget(imageUrl: image, height: 54, width: 54), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 6.0, + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 20), + child: ExcludeSemantics( + child: CakeImageWidget(imageUrl: image, height: 54, width: 54), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + spacing: 6.0, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w800, + fontSize: 20, + ), + softWrap: true, + ), + isCake == true + ? ExcludeSemantics( + child: CakeImageWidget( + imageUrl: "assets/new-ui/cakelabs-icon.svg", + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + ) + : SizedBox(), + ], + ), + SizedBox(height: 5), Text( - title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w800, - fontSize: 20, + subTitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, ), softWrap: true, ), - isCake == true - ? CakeImageWidget( - imageUrl: "assets/new-ui/cakelabs-icon.svg", - color: Theme.of(context).colorScheme.onSurfaceVariant) - : SizedBox(), ], ), - SizedBox(height: 5), - Text( - subTitle, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - softWrap: true, + ), + ExcludeSemantics( + child: Icon( + isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, ), - ], - ), + ) + ], ), - Icon( - isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios, - color: Theme.of(context).colorScheme.onSurfaceVariant, - size: 20, - ) + if (hint != null) ...[ + SizedBox(height: 10), + hint!, + ] ], ), - if (hint != null) ...[ - SizedBox(height: 10), - hint!, - ] - ], + ), ), ), ), @@ -132,10 +148,16 @@ class AppsWidget extends StatelessWidget { Positioned( top: 10, right: 10, - child: IconButton( - icon: Icon(Icons.close), - onPressed: onClose, - //color: Theme.of(context).colorScheme.onSurface, + // Label the icon-only button without adding a visible tooltip. + child: MergeSemantics( + child: Semantics( + label: S.of(context).close, + child: IconButton( + icon: Icon(Icons.close), + onPressed: onClose, + //color: Theme.of(context).colorScheme.onSurface, + ), + ), ), ), ], diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart index 57e632dcf..585fe8465 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart @@ -39,125 +39,138 @@ class AssetTile extends StatelessWidget { Widget build(BuildContext context) { final iconPath = balance.asset.iconPath ?? ""; - return GestureDetector( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) { - return AssetDetailsModal( - showSwap: showSwap, - showBridgeButton: showBridgeButton, - asset: balance.asset, - title: title ?? balance.asset.fullName ?? balance.asset.name, - chainTitle: "", - subtitle: trailingText ?? _getChainTitle(), - amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance, - currencyTitle: balance.asset.title, - fiatAmount: showSecondary - ? balance.fiatSecondAvailableBalance - : balance.fiatAvailableBalance, - iconPath: balance.asset.iconPath ?? "", - chainIconPath: chainIconPath, - mode: modalMode, - wallet: wallet, - ); - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Container( - width: double.infinity, - height: 72, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.vertical( - top: isFirst ? Radius.circular(18) : Radius.zero, - bottom: isLast ? Radius.circular(18) : Radius.zero, - ), - ), + // The row is one control: name, amount and fiat value merge into a single + // button node that opens the asset details sheet. + return MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return AssetDetailsModal( + showSwap: showSwap, + showBridgeButton: showBridgeButton, + asset: balance.asset, + title: title ?? balance.asset.fullName ?? balance.asset.name, + chainTitle: "", + subtitle: trailingText ?? _getChainTitle(), + amount: + showSecondary ? balance.secondAvailableBalance : balance.availableBalance, + currencyTitle: balance.asset.title, + fiatAmount: showSecondary + ? balance.fiatSecondAvailableBalance + : balance.fiatAvailableBalance, + iconPath: balance.asset.iconPath ?? "", + chainIconPath: chainIconPath, + mode: modalMode, + wallet: wallet, + ); + }); + }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - iconPath.isNotEmpty - ? TokenImageWidget( - imageUrl: iconPath, - size: 36, - ) - : Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - shape: BoxShape.circle, - ), - child: Center( - child: Text( - balance.asset.name - .substring(0, min(2, balance.asset.name.length)), - style: TextStyle( - fontSize: 20, - color: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Container( + width: double.infinity, + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: isFirst ? Radius.circular(18) : Radius.zero, + bottom: isLast ? Radius.circular(18) : Radius.zero, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Decorative: the asset name is already in the row text. + ExcludeSemantics( + child: iconPath.isNotEmpty + ? TokenImageWidget( + imageUrl: iconPath, + size: 36, + ) + : Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + balance.asset.name + .substring(0, min(2, balance.asset.name.length)), + style: TextStyle( + fontSize: 20, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), ), - ), - ), - ), - SizedBox(width: 12.0), - Expanded( - child: Column( - spacing: 4.0, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 4, + ), + SizedBox(width: 12.0), + Expanded( + child: Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title ?? balance.asset.fullName ?? balance.asset.name, - style: TextStyle(fontWeight: FontWeight.w500), + Row( + spacing: 4, + children: [ + Text( + title ?? balance.asset.fullName ?? balance.asset.name, + style: TextStyle(fontWeight: FontWeight.w500), + ), + if (trailingText != null) + Text( + trailingText!, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], ), - if (trailingText != null) - Text( - trailingText!, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + "${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}", + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), + ), ], ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - "${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}", - maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ], - ), + ), + ], ), - ], - ), + ), + Text( + showSecondary + ? balance.fiatSecondAvailableBalance + : balance.fiatAvailableBalance, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], ), - Text( - showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], + ), ), ), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart index 98bb99d9d..e04a179a7 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -22,6 +22,8 @@ class AssetsTopBar extends StatelessWidget { @override Widget build(BuildContext context) { + final actionButton = tabs[selectedTab].actionButton; + return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0), @@ -49,40 +51,49 @@ class AssetsTopBar extends StatelessWidget { key: ValueKey(selectedTab), spacing: 8, children: [ - Opacity( - opacity: tabs[selectedTab].actionButton != null ? 1 : 0, - child: GestureDetector( - onTap: () { - if (tabs[selectedTab].actionButton != null) { - tabs[selectedTab].actionButton?.onPressed(); - } - }, - child: Container( - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999999), - color: Theme.of(context).colorScheme.surfaceContainer, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - spacing: 6, - children: [ - if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty) - Text( - tabs[selectedTab].actionButton?.title ?? "", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - CakeImageWidget( - imageUrl: tabs[selectedTab].actionButton?.iconPath, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn)), - ], + // Built conditionally rather than rendered at opacity 0: an + // invisible chip stayed focusable for screen readers. The + // SizedBox keeps the 40px header height the invisible chip + // used to occupy. + if (actionButton == null) + const SizedBox(height: 40) + else + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: actionButton.onPressed, + child: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999999), + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + spacing: 6, + children: [ + if (actionButton.title.isNotEmpty) + Text( + actionButton.title, + style: + TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: actionButton.iconPath, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn)), + ), + ], + ), + ), ), ), ), ), - ), ], ), ), diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart index ab93a6901..cc73b556f 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -36,6 +36,15 @@ class HistorySection extends StatelessWidget { final bool roundedTopSection; final bool detailsAsPage; + /// A history row is a single button node: every text inside it (direction, + /// date, amounts) merges into one label. + Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector(onTap: onTap, child: child), + ), + ); + @override Widget build(BuildContext context) { return SliverPadding( @@ -90,7 +99,7 @@ class HistorySection extends StatelessWidget { else asset = item.assetOfTransaction; - return GestureDetector( + return _historyRow( onTap: () { final page = getIt.get(param1: transaction); @@ -125,7 +134,7 @@ class HistorySection extends StatelessWidget { final tradeFrom = trade.from; final tradeTo = trade.to; - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context) .pushNamed(Routes.tradeDetails, arguments: trade), child: HistoryTradeTile( @@ -161,7 +170,7 @@ class HistorySection extends StatelessWidget { style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant))); } else if (item is OrderListItem) { - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context) .pushNamed(Routes.orderDetails, arguments: item.order), child: HistoryOrderTile( @@ -176,7 +185,7 @@ class HistorySection extends StatelessWidget { } else if (item is PayjoinTransactionListItem) { final session = item.session; - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context).pushNamed( Routes.payjoinDetails, arguments: [item.sessionId, item.transaction], @@ -196,7 +205,7 @@ class HistorySection extends StatelessWidget { } else if (item is AnonpayTransactionListItem) { final transactionInfo = item.transaction; - return GestureDetector( + return _historyRow( onTap: () => Navigator.of(context).pushNamed( Routes.anonPayDetailsPage, arguments: transactionInfo), diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart index 02534372f..f26b0790e 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget { date: date, amount: amount, amountFiat: amountFiat, - leadingIcon: _getLeadingIcon(context), + // Decorative: `title` already reads out sent/received/pending. + leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)), primaryTextColor: _getPrimaryTextColor(), roundedTop: roundedTop, roundedBottom: roundedBottom, diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart index ad4f291a8..85865109e 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart @@ -11,46 +11,53 @@ class HistoryTopBar extends StatelessWidget { @override Widget build(BuildContext context) { return SliverToBoxAdapter( - child: GestureDetector( + child: Semantics( + button: true, + label: S.of(context).history, onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 24), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.vertical( - top: Radius.circular(18), - bottom: roundedBottom ? Radius.circular(18) : Radius.zero), - color: Theme.of(context).colorScheme.surfaceContainer), + child: ExcludeSemantics( + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, child: Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 12, - ), - child: Column( - spacing: 12, - children: [ - SizedBox.shrink(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.only(left: 16, right: 16, top: 24), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: Radius.circular(18), + bottom: roundedBottom ? Radius.circular(18) : Radius.zero), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: 12, + ), + child: Column( + spacing: 12, children: [ - Text(S.of(context).history), - CakeImageWidget( - imageUrl: "assets/new-ui/arrow_right.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), - ) + SizedBox.shrink(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).history), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_right.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ) + ], + ), + if (!roundedBottom) + Container( + height: 1, + width: double.infinity, + color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175), + ) + else + Container(height: 2) ], ), - if (!roundedBottom) - Container( - height: 1, - width: double.infinity, - color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175), - ) - else - Container(height: 2) - ], + ), ), ), ), diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart index da852bf59..20b16ed65 100644 --- a/lib/new-ui/widgets/coins_page/cards/balance_card.dart +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -91,11 +91,14 @@ class BalanceCard extends StatelessWidget { ? ClipRSuperellipse( borderRadius: BorderRadius.circular(borderRadius), key: ValueKey(design.imagePath), - child: CakeImageWidget( - imageUrl: design.imagePath, - width: width, - height: height, - fit: BoxFit.fill, + // Purely decorative card artwork. + child: ExcludeSemantics( + child: CakeImageWidget( + imageUrl: design.imagePath, + width: width, + height: height, + fit: BoxFit.fill, + ), ), ) : const SizedBox.shrink( @@ -146,9 +149,14 @@ class BalanceCard extends StatelessWidget { AnimatedOpacity( opacity: selected ? 0 : 1, duration: textFadeDuration, - child: Text( - accountBalance, - style: TextStyle(color: design.colors.textColor, fontSize: 14), + // Opacity alone keeps the text readable by screen + // readers, so drop it while it is invisible. + child: ExcludeSemantics( + excluding: selected, + child: Text( + accountBalance, + style: TextStyle(color: design.colors.textColor, fontSize: 14), + ), ), ), ], @@ -156,40 +164,45 @@ class BalanceCard extends StatelessWidget { AnimatedOpacity( opacity: selected ? 1 : 0, duration: textFadeDuration, - child: AnimatedSwitcher( - duration: designSwitchDuration, - layoutBuilder: (currentChild, previousChildren) { - return Stack( - alignment: Alignment.centerLeft, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, + // Only the selected card's balance is visible, so only it + // may be announced. + child: ExcludeSemantics( + excluding: !selected, + child: AnimatedSwitcher( + duration: designSwitchDuration, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: Row( + key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"), + spacing: 8.0, + children: [ + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context).style.copyWith( + color: design.colors.textColor, + fontSize: 28, + fontWeight: FontWeight.w500, + letterSpacing: -0.4), + child: Text(fiatFirst ? fiatBalance : balance), + ), + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context).style.copyWith( + color: design.colors.textColorSecondary, + fontSize: 28, + fontWeight: FontWeight.w400, + letterSpacing: -0.4), + child: Text(resolvedAssetName), + ), ], - ); - }, - child: Row( - key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"), - spacing: 8.0, - children: [ - AnimatedDefaultTextStyle( - duration: designSwitchDuration, - style: DefaultTextStyle.of(context).style.copyWith( - color: design.colors.textColor, - fontSize: 28, - fontWeight: FontWeight.w500, - letterSpacing: -0.4), - child: Text(fiatFirst ? fiatBalance : balance), - ), - AnimatedDefaultTextStyle( - duration: designSwitchDuration, - style: DefaultTextStyle.of(context).style.copyWith( - color: design.colors.textColorSecondary, - fontSize: 28, - fontWeight: FontWeight.w400, - letterSpacing: -0.4), - child: Text(resolvedAssetName), - ), - ], + ), ), ), ), @@ -250,7 +263,10 @@ class BalanceCard extends StatelessWidget { switchInCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut, child: design.backgroundType == CardDesignBackgroundTypes.svgIcon - ? _CornerSvgIcon(design: design, iconWidth: iconWidth) + // Purely decorative card artwork. + ? ExcludeSemantics( + child: _CornerSvgIcon(design: design, iconWidth: iconWidth), + ) : const SizedBox.shrink( key: ValueKey('svgIconOff'), ), @@ -266,18 +282,28 @@ class BalanceCard extends StatelessWidget { child: AnimatedOpacity( duration: designSwitchDuration, opacity: onCustomizeTapped == null ? 0 : 1, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onCustomizeTapped, - child: Container( - height: 40, - width: 40, - child: Center( - child: CakeImageWidget( - imageUrl: "assets/new-ui/3dots_vertical.svg", - alignment: Alignment.topRight, - colorFilter: - ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn), + // Faded out and inert: must not be a focusable phantom control. + child: ExcludeSemantics( + excluding: onCustomizeTapped == null, + child: Semantics( + button: true, + label: S.of(context).wallet_menu, + onTap: onCustomizeTapped, + child: GestureDetector( + excludeFromSemantics: true, + behavior: HitTestBehavior.opaque, + onTap: onCustomizeTapped, + child: Container( + height: 40, + width: 40, + child: Center( + child: CakeImageWidget( + imageUrl: "assets/new-ui/3dots_vertical.svg", + alignment: Alignment.topRight, + colorFilter: + ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn), + ), + ), ), ), ), @@ -289,27 +315,34 @@ class BalanceCard extends StatelessWidget { ); } - Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector( + Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics( + button: true, + label: action.label, onTap: action.onTap, - child: Container( - decoration: BoxDecoration( - color: design.colors.backgroundImageColor.withAlpha(75), - borderRadius: BorderRadius.circular(10000000), - ), - margin: const EdgeInsets.only(right: 10), - padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(right: 6), - child: Text( - action.label, - style: TextStyle(color: design.colors.textColor, fontSize: 16), - ), + child: ExcludeSemantics( + child: GestureDetector( + onTap: action.onTap, + child: Container( + decoration: BoxDecoration( + color: design.colors.backgroundImageColor.withAlpha(75), + borderRadius: BorderRadius.circular(10000000), ), - Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize), - ], + margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(right: 6), + child: Text( + action.label, + style: TextStyle(color: design.colors.textColor, fontSize: 16), + ), + ), + Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize), + ], + ), + ), ), ), ); diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index 7f80d094d..cde4e8f5c 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -76,6 +76,32 @@ class _CardsViewState extends State { final left = (parentWidth - effectiveCardWidth) / 2.0; + final isSelected = _selectedIndex == visualIndex; + final accounts = widget.accountListViewModel?.accounts; + final cardLabel = (accounts != null && realIndex < accounts.length) + ? accounts[realIndex].label + : S.of(context).balance; + + void onCardTap() { + // printV(visualIndex); + if (compactMode && visualIndex != 0) { + widget.onCompactModeBackgroundCardsTapped(); + } else if (!compactMode) { + setState(() { + if (widget.accountListViewModel != null) + widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]); + _selectedIndex = visualIndex; + }); + } + } + + void onCardLongPress() { + if (_selectedIndex == visualIndex) { + widget.dashboardViewModel.balanceViewModel.switchBalanceValue(); + } + HapticFeedback.heavyImpact(); + } + return AnimatedPositioned( key: ValueKey("$visualIndex $realIndex"), duration: animDuration, @@ -86,124 +112,122 @@ class _CardsViewState extends State { duration: animDuration, curve: Curves.easeOut, scale: scale, - child: GestureDetector( - onTap: () { - // printV(visualIndex); - if (compactMode && visualIndex != 0) { - widget.onCompactModeBackgroundCardsTapped(); - } else if (!compactMode) { - setState(() { - if (widget.accountListViewModel != null) - widget.accountListViewModel! - .select(widget.accountListViewModel!.accounts[realIndex]); - _selectedIndex = visualIndex; - }); - } - }, - onLongPress: () { - if (_selectedIndex == visualIndex) { - widget.dashboardViewModel.balanceViewModel.switchBalanceValue(); - } - ; - HapticFeedback.heavyImpact(); - }, - child: Observer(builder: (_) { - if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) { - return Container(); - } - final account = widget.accountListViewModel?.accounts[realIndex]; - - // The second balance should always be the lightning balance - // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance); - final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel - .getMainBalanceRecord(widget.lightningMode); - - late final String walletBalance; - late final String walletFiatBalance; - if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) { - if (widget.dashboardViewModel.balanceViewModel.displayMode == - BalanceDisplayMode.hiddenBalance) { - walletBalance = '●●●●●●'; - walletFiatBalance = '●●●●●●'; - } else { - walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0"; - walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00"; + // The card is the tap target; the balances and the card's own buttons stay + // reachable as children of this node. + child: Semantics( + button: true, + selected: isSelected, + label: cardLabel, + hint: isSelected + ? (widget.dashboardViewModel.balanceViewModel.displayMode == + BalanceDisplayMode.hiddenBalance + ? S.of(context).long_press_show_balance + : S.of(context).long_press_hide_balance) + : null, + onTap: onCardTap, + onLongPress: isSelected ? onCardLongPress : null, + child: GestureDetector( + excludeFromSemantics: true, + onTap: onCardTap, + onLongPress: onCardLongPress, + child: Observer(builder: (_) { + if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) { + return Container(); } - } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) { - walletBalance = ""; - walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance; - } else { - walletBalance = walletBalanceRecord?.availableBalance ?? "0"; - walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00"; - } + final account = widget.accountListViewModel?.accounts[realIndex]; - // the card designs is empty if widget gets built before it loads. - // should get populated before user sees anything - final CardDesign cardDesign; - if (widget.dashboardViewModel.cardDesigns.isEmpty || - realIndex >= widget.dashboardViewModel.cardDesigns.length) - cardDesign = CardDesign.genericDefault; - else if (widget.lightningMode) - cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1]; - else - cardDesign = widget.dashboardViewModel.cardDesigns[realIndex]; + // The second balance should always be the lightning balance + // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance); + final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel + .getMainBalanceRecord(widget.lightningMode); - final String accountName; - final String accountBalance; - if (account == null) { - accountName = ""; - accountBalance = ""; - } else { - accountName = account.label; - accountBalance = account.balance ?? "0.00"; - } + late final String walletBalance; + late final String walletFiatBalance; + if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) { + if (widget.dashboardViewModel.balanceViewModel.displayMode == + BalanceDisplayMode.hiddenBalance) { + walletBalance = '●●●●●●'; + walletFiatBalance = '●●●●●●'; + } else { + walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0"; + walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00"; + } + } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) { + walletBalance = ""; + walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance; + } else { + walletBalance = walletBalanceRecord?.availableBalance ?? "0"; + walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00"; + } - final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance - ? "" - : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback; + // the card designs is empty if widget gets built before it loads. + // should get populated before user sees anything + final CardDesign cardDesign; + if (widget.dashboardViewModel.cardDesigns.isEmpty || + realIndex >= widget.dashboardViewModel.cardDesigns.length) + cardDesign = CardDesign.genericDefault; + else if (widget.lightningMode) + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1]; + else + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex]; - final List actions = widget.lightningMode - ? [ - BalanceCardAction( - label: S.current.bitcoin_lightning_deposit, - icon: Icons.arrow_downward, - onTap: depositToL2, - ), - BalanceCardAction( - label: S.current.bitcoin_lightning_withdraw, - icon: Icons.arrow_upward, - onTap: withdrawFromL2, - ) - ] - : widget.dashboardViewModel.isEnabledTradeAction - ? [ - BalanceCardAction( - label: S.current.buy, - icon: Icons.arrow_forward_ios_rounded, - iconSize: 12, - onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage), - ) - ] - : []; + final String accountName; + final String accountBalance; + if (account == null) { + accountName = ""; + accountBalance = ""; + } else { + accountName = account.label; + accountBalance = account.balance ?? "0.00"; + } - return BalanceCard( - width: effectiveCardWidth, - accountName: accountName, - accountBalance: accountBalance, - designSwitchDuration: Duration(milliseconds: 150), - assetName: assetName, - capitalizeAssetName: _shouldCapitalizeAssetName(), - balance: walletBalance, - fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ?? - widget.dashboardViewModel.settingsStore.fiatCurrency.title, - fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance, - fiatBalance: walletFiatBalance, - selected: _selectedIndex == visualIndex, - onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null, - design: cardDesign, - actions: actions, - ); - }), + final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance + ? "" + : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback; + + final List actions = widget.lightningMode + ? [ + BalanceCardAction( + label: S.current.bitcoin_lightning_deposit, + icon: Icons.arrow_downward, + onTap: depositToL2, + ), + BalanceCardAction( + label: S.current.bitcoin_lightning_withdraw, + icon: Icons.arrow_upward, + onTap: withdrawFromL2, + ) + ] + : widget.dashboardViewModel.isEnabledTradeAction + ? [ + BalanceCardAction( + label: S.current.buy, + icon: Icons.arrow_forward_ios_rounded, + iconSize: 12, + onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage), + ) + ] + : []; + + return BalanceCard( + width: effectiveCardWidth, + accountName: accountName, + accountBalance: accountBalance, + designSwitchDuration: Duration(milliseconds: 150), + assetName: assetName, + capitalizeAssetName: _shouldCapitalizeAssetName(), + balance: walletBalance, + fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ?? + widget.dashboardViewModel.settingsStore.fiatCurrency.title, + fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance, + fiatBalance: walletFiatBalance, + selected: _selectedIndex == visualIndex, + onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null, + design: cardDesign, + actions: actions, + ); + }), + ), ), ), ); diff --git a/lib/new-ui/widgets/coins_page/mweb_ad.dart b/lib/new-ui/widgets/coins_page/mweb_ad.dart index 5eae49cf2..9c6ef00fb 100644 --- a/lib/new-ui/widgets/coins_page/mweb_ad.dart +++ b/lib/new-ui/widgets/coins_page/mweb_ad.dart @@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget { child: Column( spacing: 12, children: [ - GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings), - child: Container( - height: 64, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - color: Theme.of(context).colorScheme.surfaceContainer), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CakeImageWidget( - imageUrl: "assets/new-ui/settings_row_icons/mweb.svg", - width: 24, - height: 24, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Text( - S.of(context).mweb_ad, - softWrap: true, - style: TextStyle(fontSize: 12), + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings), + child: Container( + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: "assets/new-ui/settings_row_icons/mweb.svg", + width: 24, + height: 24, + ), ), - ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Text( + S.of(context).mweb_ad, + softWrap: true, + style: TextStyle(fontSize: 12), + ), + ), + ), + Icon( + size: 16, + Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.primary, + ) + ], ), - Icon( - size: 16, - Icons.arrow_forward_ios, - color: Theme.of(context).colorScheme.primary, - ) - ], + ), ), ), ), ), - GestureDetector( - onTap: () => dashboardViewModel.dismissMwebAd(false), - child: Text( - S.of(context).do_not_show_anymore, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary), + MergeSemantics( + child: Semantics( + button: true, + child: GestureDetector( + onTap: () => dashboardViewModel.dismissMwebAd(false), + child: Text( + S.of(context).do_not_show_anymore, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ), + ), ), ) ], diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart index 418abfe19..3fe2ebd76 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; @@ -27,10 +28,16 @@ class ChainIcon extends StatelessWidget { AnimatedOpacity( duration: Duration(milliseconds: 100), opacity: done ? 0 : 1, - child: CircularProgressIndicator( - value: progress, - color: Color(0xFFFFB84E), - strokeWidth: 2, + // Faded out means "nothing to report", so it must leave the tree too. + child: ExcludeSemantics( + excluding: done, + child: CircularProgressIndicator( + value: progress, + color: Color(0xFFFFB84E), + strokeWidth: 2, + semanticsLabel: S.of(context).synchronizing, + semanticsValue: "${(progress * 100).round()}%", + ), ), ), AnimatedScale( diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart index 5c1fb7991..6ea000518 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,76 +13,84 @@ class LightningSwitcher extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - child: InkWell( - onTap: () { - HapticFeedback.mediumImpact(); - onLightningSwitchPress(); - }, - child: Container( - decoration: ShapeDecoration( - shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)), - color: Theme.of(context).colorScheme.surfaceContainer), - width: 70, - height: 36, - padding: EdgeInsets.symmetric(vertical: 2), - child: Stack( - children: [ - AnimatedContainer( - alignment: Alignment.centerRight, - margin: EdgeInsets.only(left: lightningMode ? 36 : 2), - duration: Duration(milliseconds: 250), - curve: Curves.easeOutCubic, - width: 32, - height: 32, - // height: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(9999990.0)), - color: Theme.of(context).colorScheme.primary), - ), - Container( - child: Row( - spacing: 2.0, - children: [ - SizedBox(), - AnimatedSwitcher( - duration: Duration(milliseconds: 150), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: CakeImageWidget( - imageUrl: 'assets/new-ui/switcher-bitcoin.svg', - key: ValueKey(lightningMode), - width: 32, - height: 32, - colorFilter: ColorFilter.mode( - lightningMode - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainer, - BlendMode.srcIn, - ), - ), - ), - AnimatedSwitcher( - duration: Duration(milliseconds: 150), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: CakeImageWidget( - imageUrl: 'assets/new-ui/switcher-lightning.svg', - key: ValueKey(lightningMode), - width: 32, - height: 32, - colorFilter: ColorFilter.mode( - lightningMode - ? Theme.of(context).colorScheme.surfaceContainer - : Theme.of(context).colorScheme.primary, - BlendMode.srcIn, - ), - ), - ), - ], + // One toggle node: the knob position and the coloured glyphs are the only + // visual cue for which mode is active. + return Semantics( + button: true, + toggled: lightningMode, + label: S.of(context).lightning_mode, + child: SizedBox( + child: InkWell( + onTap: () { + HapticFeedback.mediumImpact(); + onLightningSwitchPress(); + }, + child: Container( + decoration: ShapeDecoration( + shape: + RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)), + color: Theme.of(context).colorScheme.surfaceContainer), + width: 70, + height: 36, + padding: EdgeInsets.symmetric(vertical: 2), + child: Stack( + children: [ + AnimatedContainer( + alignment: Alignment.centerRight, + margin: EdgeInsets.only(left: lightningMode ? 36 : 2), + duration: Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + width: 32, + height: 32, + // height: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(9999990.0)), + color: Theme.of(context).colorScheme.primary), ), - ), - ], + Container( + child: Row( + spacing: 2.0, + children: [ + SizedBox(), + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: CakeImageWidget( + imageUrl: 'assets/new-ui/switcher-bitcoin.svg', + key: ValueKey(lightningMode), + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + lightningMode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainer, + BlendMode.srcIn, + ), + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: CakeImageWidget( + imageUrl: 'assets/new-ui/switcher-lightning.svg', + key: ValueKey(lightningMode), + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + lightningMode + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + ], + ), ), ), ), diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart index 915ee54b5..97ba02326 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/core/sync_status_title.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; @@ -47,65 +48,55 @@ class SyncBar extends StatelessWidget { child: Stack( alignment: Alignment.centerLeft, children: [ - if (!_showFullBar()) - Row( - mainAxisSize: MainAxisSize.min, - spacing: 6, - children: [ - if (dashboardViewModel.isTorEnabled) - CakeImageWidget( - imageUrl: "assets/new-ui/tor.svg", - width: 20, - height: 20, - ), - if (_showDot()) PulsingDot(), - ], - ), + if (!_showFullBar()) _buildCompactBar(context), if (_showFullBar()) - GestureDetector( - onTap: () { - CupertinoScaffold.showCupertinoModalBottomSheet( - context: context, - barrierColor: Colors.black.withAlpha(85), - builder: (context) => FractionallySizedBox( - child: Material( - child: getIt.get(param1: false), - ))); - }, - child: AnimatedSwitcher( - duration: Duration(milliseconds: 100), - child: Container( - key: ValueKey(status.runtimeType), - height: 36, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9999), - border: _getBorder(context, status.runtimeType), - color: _getBackgroundColor(context, status.runtimeType), - ), - child: Row( - spacing: 10, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - if (icon != null) icon, - // if (dashboardViewModel.silentPaymentsScanningActive && - // progressStatuses.contains(status.runtimeType)) ...[ - // Text( - // "${(status.progress() * 100).toInt()}%", - // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)), - // ), - // Text( - // "·", - // style: TextStyle(fontSize: 12), - // ) - // ], - Text( - syncStatusTitle( - status, dashboardViewModel.settingsStore.syncStatusDisplayMode), - style: _getTextStyle(context, status.runtimeType), + // A single node: the localized status text (plus any active + // Tor/MWEB/Silent Payments badge) is the label, and the hint says + // where tapping leads. Everything inside is redundant with it. + Semantics( + button: true, + label: _statusSemanticsLabel(context, status), + hint: S.of(context).manage_nodes, + onTap: () => _openNodeManagement(context), + child: ExcludeSemantics( + child: GestureDetector( + onTap: () => _openNodeManagement(context), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 100), + child: Container( + key: ValueKey(status.runtimeType), + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9999), + border: _getBorder(context, status.runtimeType), + color: _getBackgroundColor(context, status.runtimeType), ), - ], + child: Row( + spacing: 10, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (icon != null) icon, + // if (dashboardViewModel.silentPaymentsScanningActive && + // progressStatuses.contains(status.runtimeType)) ...[ + // Text( + // "${(status.progress() * 100).toInt()}%", + // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)), + // ), + // Text( + // "·", + // style: TextStyle(fontSize: 12), + // ) + // ], + Text( + syncStatusTitle(status, + dashboardViewModel.settingsStore.syncStatusDisplayMode), + style: _getTextStyle(context, status.runtimeType), + ), + ], + ), + ), ), ), ), @@ -117,6 +108,55 @@ class SyncBar extends StatelessWidget { ); } + void _openNodeManagement(BuildContext context) => + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(85), + builder: (context) => FractionallySizedBox( + child: Material( + child: getIt.get(param1: false), + ))); + + /// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so + /// the whole row needs a text equivalent. + Widget _buildCompactBar(BuildContext context) { + final row = Row( + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + if (dashboardViewModel.isTorEnabled) + CakeImageWidget( + imageUrl: "assets/new-ui/tor.svg", + width: 20, + height: 20, + ), + if (_showDot()) PulsingDot(), + ], + ); + + final label = _joinLabels([ + if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection, + if (_showDot()) S.of(context).synchronizing, + ]); + + if (label.isEmpty) return row; + + return Semantics(label: label, child: ExcludeSemantics(child: row)); + } + + String _statusSemanticsLabel(BuildContext context, SyncStatus status) { + final isFailure = failStatuses.contains(status.runtimeType); + + return _joinLabels([ + syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode), + if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection, + if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb, + if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments, + ]); + } + + String _joinLabels(List parts) => parts.where((part) => part.isNotEmpty).join(", "); + Color? _getBackgroundColor(BuildContext context, Type status) { if (failStatuses.contains(status)) { return Theme.of(context).colorScheme.errorContainer.withAlpha(64); diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart index fa77237ee..dcdbc23a5 100644 --- a/lib/new-ui/widgets/coins_page/wallet_info.dart +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -21,15 +21,22 @@ class WalletInfoBar extends StatelessWidget { final bool hasCustomize; final VoidCallback onCustomizeButtonTap; + void _openAccountCustomizer() { + if (hasCustomize) { + onCustomizeButtonTap(); + HapticFeedback.mediumImpact(); + } + } + @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (hasCustomize) { - onCustomizeButtonTap(); - HapticFeedback.mediumImpact(); - } - }, + // The row, the hardware-wallet glyph and the inner accounts button are one + // control for a screen reader: a single labeled node opening the customizer. + final semanticsLabel = + hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}"; + + final row = GestureDetector( + onTap: _openAccountCustomizer, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, @@ -70,12 +77,7 @@ class WalletInfoBar extends StatelessWidget { SizedBox(width: 8), ModernButton.svg( size: 24, - onPressed: () { - if (hasCustomize) { - onCustomizeButtonTap(); - HapticFeedback.mediumImpact(); - } - }, + onPressed: _openAccountCustomizer, svgPath: "assets/new-ui/icon-accounts.svg", semanticLabel: S.of(context).wallet_accounts, ) @@ -83,6 +85,18 @@ class WalletInfoBar extends StatelessWidget { ], ), ); + + if (!hasCustomize) { + return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row)); + } + + return Semantics( + button: true, + label: semanticsLabel, + hint: S.of(context).wallet_accounts, + onTap: _openAccountCustomizer, + child: ExcludeSemantics(child: row), + ); } String? get hardwareWalletIcon { diff --git a/lib/new-ui/widgets/line_tab_switcher.dart b/lib/new-ui/widgets/line_tab_switcher.dart index 4f7eccfcd..9b7ebf09d 100644 --- a/lib/new-ui/widgets/line_tab_switcher.dart +++ b/lib/new-ui/widgets/line_tab_switcher.dart @@ -93,33 +93,42 @@ class _LineTabSwitcherState extends State { children: widget.tabs.map((item) { final index = widget.tabs.indexOf(item); - return GestureDetector( - onTap: () { - widget.onTabChange(index); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedDefaultTextStyle( - duration: Duration(milliseconds: 150), - style: DefaultTextStyle.of(context).style.copyWith( - inherit: true, - fontSize: 16, - fontWeight: FontWeight.w500, - color: widget.selectedTab == index - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurfaceVariant, + // One tab = one node: the visible label merges into a button node + // that also carries the selected state of this tab. + return MergeSemantics( + child: Semantics( + button: true, + selected: widget.selectedTab == index, + inMutuallyExclusiveGroup: true, + child: GestureDetector( + onTap: () { + widget.onTabChange(index); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedDefaultTextStyle( + duration: Duration(milliseconds: 150), + style: DefaultTextStyle.of(context).style.copyWith( + inherit: true, + fontSize: 16, + fontWeight: FontWeight.w500, + color: widget.selectedTab == index + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: itemPadding / 2), + child: Text( + item, + key: textWidgetKeys[index], + ), ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: itemPadding / 2), - child: Text( - item, - key: textWidgetKeys[index], ), - ), + ], ), - ], + ), ), ); }).toList())), diff --git a/lib/new-ui/widgets/long_press_menu.dart b/lib/new-ui/widgets/long_press_menu.dart index 3e660457b..6a97f0fdb 100644 --- a/lib/new-ui/widgets/long_press_menu.dart +++ b/lib/new-ui/widgets/long_press_menu.dart @@ -60,31 +60,38 @@ class _LongPressMenuState extends State { final color = item.color ?? Theme.of(context).colorScheme.onSurface; return Material( color: Colors.transparent, - child: InkWell( - onTap: item.onSelected, - child: Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 12, - bottom: 12, - ), - child: Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - spacing: 8, - children: [ - CakeImageWidget( - imageUrl: item.iconPath, - height: 20, - width: 20, - colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + child: MergeSemantics( + child: Semantics( + button: true, + child: InkWell( + onTap: item.onSelected, + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 12, + bottom: 12, + ), + child: Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + spacing: 8, + children: [ + ExcludeSemantics( + child: CakeImageWidget( + imageUrl: item.iconPath, + height: 20, + width: 20, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ), + ), + Text(item.label, + style: TextStyle( + color: color, fontSize: 14, fontWeight: FontWeight.w500)), + ], ), - Text(item.label, - style: TextStyle( - color: color, fontSize: 14, fontWeight: FontWeight.w500)), - ], + ), ), ), ), diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 5e2f636f0..45883ae68 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -187,50 +187,56 @@ class _NEWNewMainNavBarState extends State { ? iconHorizontalPadding / 100 : 0), curve: Curves.easeOutCubic, - child: InkWell( - splashFactory: NoSplash.splashFactory, - splashColor: Colors.transparent, - borderRadius: BorderRadius.circular(pillBorderRadius), - onTap: () => _onItemTap(i), - child: AnimatedContainer( - duration: - _firstFrame ? Duration.zero : inactiveIconMoveDuration, - curve: Curves.easeOutCubic, - width: i == widget.selectedIndex ? pillWidth : iconBoxWidth, - alignment: Alignment.center, - child: AnimatedAlign( - duration: inactiveIconFadeDuration, + child: Semantics( + button: true, + selected: i == widget.selectedIndex, + inMutuallyExclusiveGroup: true, + label: visibleActions[i].name(context), + child: InkWell( + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + borderRadius: BorderRadius.circular(pillBorderRadius), + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: + _firstFrame ? Duration.zero : inactiveIconMoveDuration, curve: Curves.easeOutCubic, + width: i == widget.selectedIndex ? pillWidth : iconBoxWidth, alignment: Alignment.center, - child: AnimatedScale( - duration: inactiveIconAppearDuration, + child: AnimatedAlign( + duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - scale: (i == widget.selectedIndex) ? 0.857 : 1.0, - child: TweenAnimationBuilder( - tween: ColorTween( - begin: (i == widget.selectedIndex) - ? inactiveColor - : activeColor, - end: (i == widget.selectedIndex) - ? activeColor - : inactiveColor, - ), - duration: iconColorChangeDuration, - builder: (context, value, child) { - return Container( - height: NewMainNavBar.barHeight, - child: CakeImageWidget( - imageUrl: visibleActions[i].image, - width: iconWidth, - height: iconHeight, - //fit: BoxFit.scaleDown, - colorFilter: ColorFilter.mode( - value ?? inactiveColor, - BlendMode.srcIn, + alignment: Alignment.center, + child: AnimatedScale( + duration: inactiveIconAppearDuration, + curve: Curves.easeOutCubic, + scale: (i == widget.selectedIndex) ? 0.857 : 1.0, + child: TweenAnimationBuilder( + tween: ColorTween( + begin: (i == widget.selectedIndex) + ? inactiveColor + : activeColor, + end: (i == widget.selectedIndex) + ? activeColor + : inactiveColor, + ), + duration: iconColorChangeDuration, + builder: (context, value, child) { + return Container( + height: NewMainNavBar.barHeight, + child: CakeImageWidget( + imageUrl: visibleActions[i].image, + width: iconWidth, + height: iconHeight, + //fit: BoxFit.scaleDown, + colorFilter: ColorFilter.mode( + value ?? inactiveColor, + BlendMode.srcIn, + ), ), - ), - ); - }), + ); + }), + ), ), ), ), @@ -306,11 +312,15 @@ class AnimatedPill extends StatelessWidget { children: [ Padding( padding: EdgeInsets.only(left: pillIconWidth + 2), - child: Text( - currentAction.name(context), - style: pillTextStyle.copyWith(color: contentColor), - overflow: TextOverflow.fade, - softWrap: false, + // The selected tab's InkWell already announces this name, so the + // pill text must not become a second stop for screen readers. + child: ExcludeSemantics( + child: Text( + currentAction.name(context), + style: pillTextStyle.copyWith(color: contentColor), + overflow: TextOverflow.fade, + softWrap: false, + ), ), ), ], diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ea39386cb..9376a6332 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -528,6 +528,7 @@ "got_it": "Got it", "gross_balance": "Gross Balance", "group_by_type": "Group by type", + "hardware_wallet": "Hardware wallet", "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", @@ -588,6 +589,7 @@ "lightning_deposit_desc": "When you deposit to Lightning, you are swapping your on-chain Bitcoin from this wallet to your Lightning account.", "lightning_deposit_disclaimer": "The new Lightning balance will not be available until the Bitcoin transaction is fully confirmed.", "lightning_external_disclaimer": "Don't close this page until you send the funds, or else the deposit won't proceed.", + "lightning_mode": "Lightning mode", "lightning_username_desc": "Easily receive Lightning payments with a Lightning username by choosing a custom name or using a randomly generated one below.", "lightning_username_desc_completed": "You can use this username to receive payments from any Lightning wallet.", "lightning_username_setup_later": "If you skip now, you can set this up later in the Settings.", @@ -618,6 +620,7 @@ "login": "Login", "logout": "Logout", "long_press_edit_address": "Long press to edit address", + "long_press_hide_balance": "Long press card to hide balance", "long_press_show_balance": "Long press card to show balance", "low_fee": "Low fee", "low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.", @@ -726,6 +729,7 @@ "onramper_option_description": "Quickly buy crypto with many payment methods. Available in most countries. Spreads and fees vary.", "open_gift_card": "Open Gift Card", "open_wallet": "Open Wallet", + "opens_externally": "Opens externally", "optional_description": "Optional description", "optional_email_hint": "Optional payee notification email", "optional_name": "Optional recipient name",