a11y: dashboard and navigation semantics (navbar, cards, sync, switchers) (#3468)
What changed, and why it matters
This commit improves accessibility for screen-reader users in the Cake Wallet app's new dashboard. It adds labels and roles to navigation tabs, balance cards, buttons, and status indicators so assistive technology can describe them properly. There is no security-relevant change here.
No security action needed. Treat as a normal accessibility improvement. If desired, verify the new strings are translated consistently across locales.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit is a pure accessibility (a11y) refactor of Flutter widgets in the new dashboard UI. It wraps interactive elements with Semantics/MergeSemantics/ExcludeSemantics, adds button/toggled/selected/inMutuallyExclusiveGroup annotations, removes invisible but focusable widgets (e.g., the Opacity 0 action chip and the transparent pointer-absorbing strip), and adds localized string resources for labels/hints. No logic affecting cryptography, authentication, networking, storage, or permissions was changed.
Changed components
lib/new-ui/new_dashboard.dartlib/new-ui/pages/account_customizer.dartlib/new-ui/widgets/apps_widget.dartlib/new-ui/widgets/coins_page/assets_history/asset_tile.dartlib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dartlib/new-ui/widgets/coins_page/assets_history/history_section.dartlib/new-ui/widgets/coins_page/assets_history/history_tile.dartlib/new-ui/widgets/coins_page/assets_history/history_top_bar.dartlib/new-ui/widgets/coins_page/cards/balance_card.dartlib/new-ui/widgets/coins_page/cards/cards_view.dartlib/new-ui/widgets/coins_page/mweb_ad.dartlib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dartlib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dartlib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dartlib/new-ui/widgets/coins_page/wallet_info.dartlib/new-ui/widgets/line_tab_switcher.dartlib/new-ui/widgets/long_press_menu.dartlib/src/screens/dashboard/widgets/new_main_navbar_widget.dartres/values/strings_en.arbInspect captured patch +1010 / −750
diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart
index a285e9ca..9c929464 100644
--- a/lib/new-ui/new_dashboard.dart
+++ b/lib/new-ui/new_dashboard.dart
@@ -90,14 +90,18 @@ class _NewDashboardState extends State<NewDashboard> {
),
),
),
- 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 8ebaf4c2..ff9d6f59 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<AccountCustomizer> {
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<AccountCustomizer> {
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<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
+ Future<void> _generateAccountName() async => _controller.text = await generateName();
+
@override
Widget build(BuildContext context) {
return Container(
@@ -441,18 +460,23 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
),
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 d440d91b..63cd77f4 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 57e632dc..585fe846 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 98bb99d9..e04a179a 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 ab93a690..cc73b556 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<TransactionDetailsModal>(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 02534372..f26b0790 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 ad4f291a..85865109 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 da852bf5..20b16ed6 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: <Widget>[
- ...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: <Widget>[
+ ...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 7f80d094..cde4e8f5 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<CardsView> {
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<CardsView> {
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 = '●●●●●●';
+ // 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();
+ }
+ 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";
+ }
+ } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
+ walletBalance = "";
+ walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
} else {
- walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
- walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
+ walletBalance = walletBalanceRecord?.availableBalance ?? "0";
+ walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
}
- } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
- walletBalance = "";
- walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
- } else {
- walletBalance = walletBalanceRecord?.availableBalance ?? "0";
- walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
- }
-
- // 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 String accountName;
- final String accountBalance;
- if (account == null) {
- accountName = "";
- accountBalance = "";
- } else {
- accountName = account.label;
- accountBalance = account.balance ?? "0.00";
- }
-
- final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
- ? ""
- : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
-
- final List<BalanceCardAction> 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,
- );
- }),
+
+ // 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 String accountName;
+ final String accountBalance;
+ if (account == null) {
+ accountName = "";
+ accountBalance = "";
+ } else {
+ accountName = account.label;
+ accountBalance = account.balance ?? "0.00";
+ }
+
+ final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
+ ? ""
+ : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
+
+ final List<BalanceCardAction> 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 5eae49cf..9c6ef00f 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 418abfe1..3fe2ebd7 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 5c1fb799..6ea00051 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,
+ // 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,
+ 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 915ee54b..97ba0232 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<ManageNodesPage>(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<ManageNodesPage>(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<String> 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 fa77237e..dcdbc23a 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 4f7eccfc..9b7ebf09 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<LineTabSwitcher> {
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 3e660457..6a97f0fd 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<LongPressMenu> {
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 5e2f636f..45883ae6 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<NewMainNavBar> {
? 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<Color?>(
- 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<Color?>(
+ 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 ea39386c..9376a633 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",
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.