a11y: expose ModernButton, CoinActionButton and ModalTopBar as single labeled button nodes (#3463)

* Give ModernButton and CoinActionButton one labeled a11y node ModernButton, CoinActionButton and the ModalTopBar chrome buttons were icon-only IconButtons with no tooltip and a detached caption sibling, so screen readers announced an unnamed "button" plus unrelated static text. - Add optional semanticLabel to ModernButton; wrap the control in MergeSemantics + Semantics(button: true, label: semanticLabel ?? label) and exclude both the icon and the visible caption so exactly one node is exposed. No visual or pointer change. - Give CoinActionButton the same treatment, reusing the label callers already pass (dashboard Send / Receive / Swap / Scan). - Add leadingSemanticLabel / trailingSemanticLabel to ModalTopBar, defaulting the leading button to "Close", and mark a non-empty title as a semantics header. - Migrate every ModernButton / ModalTopBar call site to pass a localized label, and drop the now-redundant external Semantics wrapper in base_page.dart so the back button is not labeled twice. - Localize the hard-coded "Settings" modal title in settings_page.dart. New English keys: rotate_address, swap_reverse_direction, switch_camera, switch_input_currency, test_node_speeds, turn_flash_off, turn_flash_on. * Require explicit accessible names on ModalTopBar and ModernButton icons ModalTopBar defaulted every leading icon's accessible name to "Close", so the ~40 callers that pass a back arrow announced the wrong action, and trailingSemanticLabel was optional, leaving the trailing icon controls unnamed. - Drop the "?? close" fallback in ModalTopBar: the leading ModernButton now gets exactly leadingSemanticLabel, and constructor asserts require a non-empty label whenever leadingIcon / trailingIcon is supplied. Callers that pass leadingWidget / trailingWidget instead are unaffected, and a null icon still builds no button at all. - Assert in both ModernButton constructors that the control has a name: either semanticLabel or a non-empty visible label. - Pass the matching localized label at every ModalTopBar call site: "Go back" for back arrows, "Close" for close icons, and the action name for trailing icons (History, Configure, Export CSV, Save, Scan, Close). - Label the last unnamed ModernButton (wallet accounts, wallet_info.dart). No new localization keys; all labels reuse existing strings_en.arb keys. * fix(a11y): put ModalTopBar header flag on the title Text node On-device audit (CW-1574, defect D2) found zero [heading] nodes in the Android accessibility tree on the Receive page: Semantics(header:) wrapped the AnimatedSwitcher rather than the Text inside it, so the flag never landed on the node that carries the title label. Move the header semantics inside the AnimatedSwitcher, directly around the Text. The ValueKey(title) moves with it onto the Semantics wrapper, since AnimatedSwitcher switches on its direct child's key -- keying the Semantics by title preserves the existing switch/animation behavior exactly. No test or robot resolves the title ValueKey through a Text-typed finder, so the key relocation is not observable from the test suites on this branch. * Set headingLevel so Android surfaces modal titles as headings The Flutter 3.41 engine drives AccessibilityNodeInfo.setHeading from headingLevel, not the IS_HEADER flag (which 3.35 used). Proven by an on-device probe: header:-only headings print on a 3.35-engine build and vanish on the 3.41-engine CI build.

Seth For Privacy committed Aug 2, 2026 at 03:50 UTC 26a16a4eef7c9976720d28b81c7a278ac55a1ee6
75 files changed +266 -96
lib/new-ui/pages/about_page.dart
+1
@@ -56,6 +56,7 @@ class _AboutPageState extends State<AboutPage> {
56 topBar: ModalTopBar(
57 title: S.of(context).about,
58 leadingIcon: Icon(Icons.arrow_back_ios_new),
59 + leadingSemanticLabel: S.of(context).seed_alert_back,
60 onLeadingPressed: Navigator.of(context).pop,
61 ),
62 content: Container(
lib/new-ui/pages/account_customizer.dart
+2
@@ -128,8 +128,10 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
128 ModalTopBar(
129 title: S.of(context).wallet_accounts,
130 leadingIcon: Icon(Icons.close),
131 + leadingSemanticLabel: S.of(context).close,
132 onLeadingPressed: Navigator.of(context).maybePop,
133 trailingIcon: Icon(Icons.refresh),
134 + trailingSemanticLabel: S.of(context).reset,
135 onTrailingPressed: showResetDialog,
136 ),
137 Padding(
lib/new-ui/pages/addresses_page.dart
+1
@@ -105,6 +105,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
105 ModalTopBar(
106 title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
107 leadingIcon: Icon(Icons.arrow_back),
108 + leadingSemanticLabel: S.of(context).seed_alert_back,
109 onLeadingPressed: Navigator.of(context).pop,
110 onTrailingPressed: () {}),
111 Expanded(
lib/new-ui/pages/bridge/bridge_amount_page.dart
+3
@@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
116 ModalTopBar(
117 title: S.of(context).enter_amount,
118 leadingIcon: Icon(Icons.arrow_back_ios_new),
119 + leadingSemanticLabel: S.of(context).seed_alert_back,
120 onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
121 trailingIcon: Icon(Icons.history),
122 + trailingSemanticLabel: S.of(context).history,
123 onTrailingPressed: () {
124 Navigator.pushNamed(context, Routes.bridgeHistoryPage,
125 arguments: widget.bridgeHistoryViewModel);
@@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
276 backgroundColor: theme.colorScheme.primary,
277 iconColor: theme.colorScheme.onPrimary,
278 icon: Icon(Icons.arrow_forward, size: 20),
279 + semanticLabel: S.of(context).continue_text,
280 onPressed: () {
281 Navigator.pushNamed(
282 context,
lib/new-ui/pages/bridge/bridge_confirm_sheet.dart
+1
@@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
87 ],
88 ),
89 trailingIcon: const Icon(Icons.close),
90 + trailingSemanticLabel: S.of(context).close,
91 onTrailingPressed: () => Navigator.of(context).maybePop(),
92 );
93 },
lib/new-ui/pages/bridge/bridge_detail_page.dart
+1
@@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget {
46 ModalTopBar(
47 title: "Bridge Transfer Details",
48 leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
49 + leadingSemanticLabel: S.of(context).seed_alert_back,
50 onLeadingPressed: () => Navigator.of(context).pop(),
51 ),
52 Expanded(
lib/new-ui/pages/bridge/bridge_history_page.dart
+2
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/di.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
4 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
5 import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
@@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget {
26 ModalTopBar(
27 title: "Bridge history",
28 leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
29 + leadingSemanticLabel: S.of(context).seed_alert_back,
30 onLeadingPressed: () => Navigator.of(context).pop(),
31 ),
32 Expanded(
lib/new-ui/pages/bridge/bridge_network_page.dart
+2
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
4 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
5 import 'package:cake_wallet/routes.dart';
@@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget {
31 ModalTopBar(
32 title: 'Destination Network',
33 leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
34 + leadingSemanticLabel: S.of(context).seed_alert_back,
35 onLeadingPressed: () => Navigator.of(context).pop(),
36 ),
37 SizedBox(height: 48),
lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart
+1
@@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInpu
55 ModalTopBar(
56 title: 'Input Receive Address',
57 leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
58 + leadingSemanticLabel: S.of(context).seed_alert_back,
59 onLeadingPressed: () => Navigator.of(context).pop(),
60 ),
61 SizedBox(height: 48),
lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart
+2
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
4 import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
5 import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
@@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
43 ModalTopBar(
44 title: 'Receiving Wallet',
45 leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
46 + leadingSemanticLabel: S.of(context).seed_alert_back,
47 onLeadingPressed: () => Navigator.of(context).pop(),
48 ),
49 const SizedBox(height: 48),
lib/new-ui/pages/card_customizer.dart
+1
@@ -84,6 +84,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
84 ModalTopBar(
85 title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
86 leadingIcon: Icon(Icons.close),
87 + leadingSemanticLabel: S.of(context).close,
88 // trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
89 onLeadingPressed: () => Navigator.of(context).maybePop(),
90 // onTrailingPressed: () {},
lib/new-ui/pages/lightning_username_page.dart
+1
@@ -68,6 +68,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
68 ModalTopBar(
69 title: "Lightning ${S.of(context).username}",
70 leadingIcon: Icon(Icons.arrow_back_ios_new),
71 + leadingSemanticLabel: S.of(context).seed_alert_back,
72 onLeadingPressed: Navigator.of(context).pop,
73 ),
74 Expanded(
lib/new-ui/pages/receive_page.dart
+4
@@ -201,6 +201,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
201 ModalTopBar(
202 title: _largeQrMode ? "" : S.of(context).receive,
203 leadingIcon: const Icon(Icons.close),
204 + leadingSemanticLabel: S.of(context).close,
205 onLeadingPressed: () {
206 Navigator.of(context, rootNavigator: true).pop();
207 },
@@ -223,6 +224,9 @@ class _NewReceivePageState extends State<NewReceivePage> {
224 : widget.addressListViewModel.isRotatingAddress
225 ? const CupertinoActivityIndicator()
226 : const Icon(Icons.refresh),
227 + semanticLabel: _largeQrMode
228 + ? S.of(context).share_address
229 + : S.of(context).rotate_address,
230 onPressed: () {
231 if (_largeQrMode) {
232 ShareUtil.share(
lib/new-ui/pages/scan_page.dart
+6
@@ -163,6 +163,7 @@ class _ScanPageState extends State<ScanPage> {
163 },
164 iconColor: buttonIconColor,
165 backgroundColor: buttonColor,
166 + semanticLabel: S.of(context).switch_camera,
167 ),
168 ModernButton(
169 size: 36,
@@ -171,6 +172,10 @@ class _ScanPageState extends State<ScanPage> {
172 (controller.value.torchState == TorchState.on || _frontFlashMode)
173 ? Icons.flash_off_outlined
174 : Icons.flash_on),
175 + semanticLabel:
176 + (controller.value.torchState == TorchState.on || _frontFlashMode)
177 + ? S.of(context).turn_flash_off
178 + : S.of(context).turn_flash_on,
179 onPressed: () {
180 if (controller.value.cameraDirection == CameraFacing.front) {
181 setState(() {
@@ -194,6 +199,7 @@ class _ScanPageState extends State<ScanPage> {
199 size: 36,
200 iconSize: 18,
201 icon: Icon(Icons.arrow_back_ios_new),
202 + semanticLabel: S.of(context).seed_alert_back,
203 onPressed: () {
204 if (_textInputMode) {
205 setState(() {
lib/new-ui/pages/send_page.dart
+5
@@ -328,6 +328,7 @@ class _NewSendPageState extends State<NewSendPage> {
328 title: widget.mode.title,
329 subtitle: widget.mode.description,
330 leadingIcon: const Icon(Icons.close),
331 + leadingSemanticLabel: S.of(context).close,
332 onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
333 trailingWidget: Observer(
334 builder: (_) => Row(
@@ -343,6 +344,7 @@ class _NewSendPageState extends State<NewSendPage> {
344 BlendMode.srcIn,
345 ),
346 ),
347 + semanticLabel: S.of(context).remove,
348 onPressed: () {
349 final outputIndex = _selectedOutput;
350 if (_selectedOutput != 0) {
@@ -362,6 +364,7 @@ class _NewSendPageState extends State<NewSendPage> {
364 ModernButton(
365 size: 36,
366 icon: const Icon(Icons.add),
367 + semanticLabel: S.of(context).add_receiver,
368 onPressed: () {
369 _addInputControllers();
370 widget.sendViewModel.addOutput();
@@ -378,6 +381,7 @@ class _NewSendPageState extends State<NewSendPage> {
381 BlendMode.srcIn,
382 ),
383 ),
384 + semanticLabel: S.of(context).help,
385 onPressed: () {
386 Navigator.of(context).push(
387 CupertinoPageRoute(
@@ -1492,6 +1496,7 @@ class SendHelpPage extends StatelessWidget {
1496 ModalTopBar(
1497 title: content.title,
1498 leadingIcon: const Icon(Icons.arrow_back_ios_new),
1499 + leadingSemanticLabel: S.of(context).seed_alert_back,
1500 onLeadingPressed: Navigator.of(context).pop,
1501 ),
1502 Padding(
lib/new-ui/pages/settings_page.dart
+2 -1
@@ -152,8 +152,9 @@ class SettingsMainPage extends StatelessWidget {
152 color: Theme.of(context).colorScheme.surface,
153 child: Column(children: [
154 ModalTopBar(
155 - title: "Settings",
155 + title: S.of(context).settings_title,
156 leadingIcon: Icon(Icons.close),
157 + leadingSemanticLabel: S.of(context).close,
158 onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
159 onTrailingPressed: () {},
160 ),
lib/new-ui/pages/swap_page.dart
+6
@@ -472,12 +472,14 @@ class _NewSwapPageState extends State<NewSwapPage> {
472 ModalTopBar(
473 title: S.of(context).swap,
474 leadingIcon: Icon(Icons.close),
475 + leadingSemanticLabel: S.of(context).close,
476 onLeadingPressed: Navigator.of(context).maybePop,
477 trailingIcon: CakeImageWidget(
478 imageUrl: "assets/new-ui/options.svg",
479 colorFilter:
480 ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
481 ),
482 + trailingSemanticLabel: S.of(context).configure,
483 onTrailingPressed: () {
484 Navigator.of(context).push(CupertinoPageRoute(
485 builder: (context) => Material(
@@ -576,6 +578,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
578 iconSize: 24,
579 svgPath: "assets/new-ui/swap_amounts.svg",
580 onPressed: widget.exchangeViewModel.reverseSwapDirection,
581 + semanticLabel: S.of(context).swap_reverse_direction,
582 ),
583 ],
584 ),
@@ -1174,6 +1177,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1177 size: 36,
1178 iconSize: 18,
1179 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1180 + semanticLabel: S.of(context).refund_address,
1181 ),
1182 if (widget.isReceiverCard &&
1183 widget.exchangeViewModel.receiveAddress.isEmpty) ...[
@@ -1183,6 +1187,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1187 size: 36,
1188 iconSize: 20,
1189 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1190 + semanticLabel: S.of(context).paste,
1191 ),
1192 ModernButton.svg(
1193 svgPath: "assets/new-ui/scan.svg",
@@ -1190,6 +1195,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1195 size: 36,
1196 iconSize: 20,
1197 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1198 + semanticLabel: S.of(context).scan,
1199 ),
1200 ]
1201 ],
lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart
+48 -38
@@ -23,47 +23,57 @@ class CoinActionButton extends StatelessWidget {
23 final size = MediaQuery.of(context).size.width * sizeFactor;
24 final double effectiveSize = min(size, 80);
25
26 - return Column(
27 - children: [
28 - Container(
29 - width: effectiveSize,
30 - height: effectiveSize,
31 - decoration: BoxDecoration(
32 - shape: BoxShape.circle,
33 - gradient: LinearGradient(
34 - colors: [
35 - context.customColors.cardGradientColorPrimary,
36 - context.customColors.cardGradientColorSecondary
37 - ],
38 - begin: Alignment.topCenter,
39 - end: Alignment.bottomCenter,
40 - ),
41 - border: Border.all(
42 - color: Theme.of(context).colorScheme.surfaceContainerHigh,
43 - width: 1,
26 + // The caption below the circle is the accessible name, so it is excluded
27 + // from the semantics tree to keep this a single button node.
28 + return MergeSemantics(
29 + child: Semantics(
30 + button: true,
31 + label: label,
32 + child: Column(
33 + children: [
34 + Container(
35 + width: effectiveSize,
36 + height: effectiveSize,
37 + decoration: BoxDecoration(
38 + shape: BoxShape.circle,
39 + gradient: LinearGradient(
40 + colors: [
41 + context.customColors.cardGradientColorPrimary,
42 + context.customColors.cardGradientColorSecondary
43 + ],
44 + begin: Alignment.topCenter,
45 + end: Alignment.bottomCenter,
46 + ),
47 + border: Border.all(
48 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
49 + width: 1,
50 + ),
51 + ),
52 + child: IconButton(
53 + padding: EdgeInsets.zero,
54 + constraints: const BoxConstraints(),
55 + onPressed: () {
56 + HapticFeedback.mediumImpact();
57 + action();
58 + },
59 + icon: ExcludeSemantics(child: icon),
60 + color: Theme.of(context).colorScheme.primary,
61 + ),
62 ),
45 - ),
46 - child: IconButton(
47 - padding: EdgeInsets.zero,
48 - constraints: const BoxConstraints(),
49 - onPressed: () {
50 - HapticFeedback.mediumImpact();
51 - action();
52 - },
53 - icon: icon,
54 - color: Theme.of(context).colorScheme.primary,
55 - ),
56 - ),
57 - Padding(
58 - padding: const EdgeInsets.only(top: 8.0),
59 - child: Text(
60 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
61 - color: Theme.of(context).colorScheme.onSurfaceVariant,
63 + Padding(
64 + padding: const EdgeInsets.only(top: 8.0),
65 + child: ExcludeSemantics(
66 + child: Text(
67 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
68 + color: Theme.of(context).colorScheme.onSurfaceVariant,
69 + ),
70 + label,
71 ),
63 - label,
64 - ),
72 + ),
73 + ),
74 + ],
75 ),
66 - ],
76 + ),
77 );
78 }
79 }
lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart
+17 -11
@@ -62,6 +62,7 @@ class AssetDetailsModal extends StatelessWidget {
62 ModalTopBar(
63 title: "",
64 trailingIcon: Icon(Icons.close),
65 + trailingSemanticLabel: S.of(context).close,
66 onTrailingPressed: Navigator.of(context).pop,
67 padding: EdgeInsets.only(top: 12, right: 18),
68 ),
@@ -356,17 +357,22 @@ class AssetDetailsModalBottomButton extends StatelessWidget {
357
358 @override
359 Widget build(BuildContext context) {
359 - return Column(
360 - spacing: 8,
361 - children: [
362 - ModernButton.svg(
363 - onPressed: onPressed,
364 - svgPath: iconPath,
365 - size: 60,
366 - iconSize: 32,
367 - ),
368 - Text(title)
369 - ],
360 + // The caption is the button's accessible name, so it is excluded from the
361 + // semantics tree to keep the whole control a single button node.
362 + return MergeSemantics(
363 + child: Column(
364 + spacing: 8,
365 + children: [
366 + ModernButton.svg(
367 + onPressed: onPressed,
368 + svgPath: iconPath,
369 + size: 60,
370 + iconSize: 32,
371 + semanticLabel: title,
372 + ),
373 + ExcludeSemantics(child: Text(title))
374 + ],
375 + ),
376 );
377 }
378 }
lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart
+1
@@ -27,6 +27,7 @@ class HistoryFiltersPage extends StatelessWidget {
27 ModalTopBar(
28 title: S.of(context).filters,
29 leadingIcon: Icon(Icons.arrow_back_ios_new),
30 + leadingSemanticLabel: S.of(context).seed_alert_back,
31 onLeadingPressed: Navigator.of(context).pop,
32 ),
33 Expanded(
lib/new-ui/widgets/coins_page/assets_history/history_modal.dart
+2
@@ -25,11 +25,13 @@ class HistoryModal extends StatelessWidget {
25 ModalTopBar(
26 title: S.of(context).history,
27 leadingIcon: Icon(Icons.close),
28 + leadingSemanticLabel: S.of(context).close,
29 onLeadingPressed: Navigator.of(context).maybePop,
30 trailingIcon: CakeImageWidget(
31 imageUrl: "assets/new-ui/tx_export.svg",
32 colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
33 ),
34 + trailingSemanticLabel: S.of(context).export_csv,
35 onTrailingPressed: () =>
36 CsvExportService().exportToCsv(dashboardViewModel.items, context),
37 ),
lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart
+1
@@ -24,6 +24,7 @@ class HistorySwapProvidersPage extends StatelessWidget {
24 ModalTopBar(
25 title: S.of(context).filter_swap_providers,
26 leadingIcon: Icon(Icons.arrow_back_ios_new),
27 + leadingSemanticLabel: S.of(context).seed_alert_back,
28 onLeadingPressed: Navigator.of(context).pop,
29 ),
30 Expanded(
lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+1
@@ -68,6 +68,7 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
68 ModalTopBar(
69 title: S.of(context).transaction,
70 leadingIcon: const Icon(Icons.close),
71 + leadingSemanticLabel: S.of(context).close,
72 onLeadingPressed: Navigator.of(context).pop,
73 ),
74 Expanded(
lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
+2
@@ -1,5 +1,6 @@
1 import 'dart:io';
2
3 +import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
5 import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
6 import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
@@ -53,6 +54,7 @@ class TopBar extends StatelessWidget {
54 onSettingsButtonPress();
55 },
56 svgPath: "assets/new-ui/top-settings.svg",
57 + semanticLabel: S.of(context).settings_title,
58 ),
59 ],
60 ),
lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart
+1
@@ -125,6 +125,7 @@ class UnconfirmedBalanceModal extends StatelessWidget {
125 ModalTopBar(
126 title: S.of(context).balance_confirmation,
127 leadingIcon: Icon(Icons.close),
128 + leadingSemanticLabel: S.of(context).close,
129 onLeadingPressed: Navigator.of(context).pop),
130 Padding(
131 padding: const EdgeInsets.symmetric(horizontal: 16.0),
lib/new-ui/widgets/coins_page/wallet_info.dart
+2
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
3 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
4 import 'package:cw_core/wallet_info.dart';
@@ -76,6 +77,7 @@ class WalletInfoBar extends StatelessWidget {
77 }
78 },
79 svgPath: "assets/new-ui/icon-accounts.svg",
80 + semanticLabel: S.of(context).wallet_accounts,
81 )
82 ]
83 ],
lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart
+1
@@ -43,6 +43,7 @@ class CurrencyPickerSheet extends StatelessWidget {
43 ModalTopBar(
44 title: S.of(context).select_asset,
45 leadingIcon: const Icon(Icons.close),
46 + leadingSemanticLabel: S.of(context).close,
47 onLeadingPressed: () => Navigator.of(context).maybePop(),
48 ),
49 Expanded(
lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart
+1
@@ -117,6 +117,7 @@ class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
117 ModalTopBar(
118 title: S.of(context).select_fiat_currency_title,
119 leadingIcon: const Icon(Icons.close),
120 + leadingSemanticLabel: S.of(context).close,
121 onLeadingPressed: () => Navigator.of(context).maybePop(),
122 ),
123 Expanded(
lib/new-ui/widgets/currency_picker/select_network_page.dart
+1
@@ -35,6 +35,7 @@ class SelectNetworkPage extends StatelessWidget {
35 ModalTopBar(
36 title: S.of(context).select_network_title,
37 leadingIcon: const Icon(Icons.arrow_back),
38 + leadingSemanticLabel: S.of(context).seed_alert_back,
39 onLeadingPressed: () => Navigator.of(context).maybePop(),
40 ),
41 const SizedBox(height: 24),
lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart
+2
@@ -106,6 +106,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
106 Icons.close,
107 color: Theme.of(context).colorScheme.onSurfaceVariant,
108 ),
109 + trailingSemanticLabel: S.of(context).close,
110 onTrailingPressed: () => showPopUp(
111 context: context,
112 builder: (context) => AlertWithTwoActions(
@@ -143,6 +144,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
144 iconColor: Theme.of(context).colorScheme.onPrimary,
145 size: 36,
146 icon: Icon(Icons.arrow_forward),
147 + semanticLabel: S.of(context).confirm,
148 onPressed: () =>
149 widget.trezorConnectVM.setParingPin(_controller.text.trim()),
150 ),
lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart
+1
@@ -32,6 +32,7 @@ class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorPa
32 onLeadingPressed: Navigator.of(context).pop,
33 onTrailingPressed: () {},
34 leadingIcon: Icon(Icons.close),
35 + leadingSemanticLabel: S.of(context).close,
36 ),
37 Padding(
38 padding: const EdgeInsets.all(18.0),
lib/new-ui/widgets/modern_button.dart
+50 -28
@@ -11,6 +11,11 @@ class ModernButton extends StatelessWidget {
11 final Color? backgroundColor;
12 final double? iconSize;
13 final String? label;
14 +
15 + /// Accessible name announced by screen readers. Falls back to [label] (the
16 + /// visible caption) when omitted, so one of the two is required. Must be
17 + /// localized by the caller.
18 + final String? semanticLabel;
19 static const iconSvgSizeRatio = 2 / 3;
20
21 const ModernButton(
@@ -21,8 +26,11 @@ class ModernButton extends StatelessWidget {
26 this.iconSize,
27 this.iconColor,
28 this.backgroundColor,
24 - this.label})
25 - : svgPath = null;
29 + this.label,
30 + this.semanticLabel})
31 + : svgPath = null,
32 + assert(semanticLabel != null || (label != null && label != ""),
33 + "ModernButton needs a semanticLabel when it has no visible label");
34
35 const ModernButton.svg(
36 {super.key,
@@ -32,8 +40,11 @@ class ModernButton extends StatelessWidget {
40 this.iconSize,
41 this.iconColor,
42 this.backgroundColor,
35 - this.label})
36 - : icon = null;
43 + this.label,
44 + this.semanticLabel})
45 + : icon = null,
46 + assert(semanticLabel != null || (label != null && label != ""),
47 + "ModernButton needs a semanticLabel when it has no visible label");
48
49 @override
50 Widget build(BuildContext context) {
@@ -56,31 +67,42 @@ class ModernButton extends StatelessWidget {
67 child: icon!,
68 );
69
59 - return Column(
60 - spacing: 8,
61 - children: [
62 - Container(
63 - decoration: BoxDecoration(
64 - color: resolvedBackgroundColor,
65 - borderRadius: BorderRadius.circular(size),
66 - ),
67 - width: size,
68 - height: size,
69 - child: IconButton(
70 - padding: EdgeInsets.zero,
71 - onPressed: onPressed,
72 - icon: resolvedIcon,
73 - ),
70 + // A single accessibility node for the whole control: the visible caption
71 + // duplicates the accessible name, so it is excluded to avoid a second,
72 + // detached stop for screen readers.
73 + return MergeSemantics(
74 + child: Semantics(
75 + button: true,
76 + label: semanticLabel ?? label,
77 + child: Column(
78 + spacing: 8,
79 + children: [
80 + Container(
81 + decoration: BoxDecoration(
82 + color: resolvedBackgroundColor,
83 + borderRadius: BorderRadius.circular(size),
84 + ),
85 + width: size,
86 + height: size,
87 + child: IconButton(
88 + padding: EdgeInsets.zero,
89 + onPressed: onPressed,
90 + icon: ExcludeSemantics(child: resolvedIcon),
91 + ),
92 + ),
93 + if (label != null && label!.isNotEmpty)
94 + ExcludeSemantics(
95 + child: Text(
96 + label!,
97 + style: TextStyle(
98 + fontSize: 12,
99 + fontWeight: FontWeight.w400,
100 + color: Theme.of(context).colorScheme.onSurface),
101 + ),
102 + )
103 + ],
104 ),
75 - if (label != null && label!.isNotEmpty)
76 - Text(
77 - label!,
78 - style: TextStyle(
79 - fontSize: 12,
80 - fontWeight: FontWeight.w400,
81 - color: Theme.of(context).colorScheme.onSurface),
82 - )
83 - ],
105 + ),
106 );
107 }
108 }
lib/new-ui/widgets/picker.dart
+3
@@ -1,5 +1,6 @@
1 import 'dart:io';
2
3 +import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
5 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
6 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -77,6 +78,7 @@ class _NewPickerState<Item> extends State<NewPicker<Item>> {
78 ModalTopBar(
79 title: widget.title,
80 leadingIcon: Icon(Icons.arrow_back_ios_new),
81 + leadingSemanticLabel: S.of(context).seed_alert_back,
82 onLeadingPressed: Navigator.of(context).maybePop,
83 ),
84 Padding(
@@ -387,6 +389,7 @@ class _PickerSliderPageState<Item> extends State<PickerSliderPage<Item>> {
389 ModalTopBar(
390 title: widget.title,
391 leadingIcon: Icon(Icons.arrow_back_ios_new),
392 + leadingSemanticLabel: S.of(context).seed_alert_back,
393 onLeadingPressed: Navigator.of(context).pop,
394 ),
395 Expanded(
lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart
+1
@@ -25,6 +25,7 @@ class PayjoinCopyModal extends StatelessWidget {
25 ModalTopBar(
26 title: S.of(context).select_address_to_copy,
27 leadingIcon: Icon(Icons.close),
28 + leadingSemanticLabel: S.of(context).close,
29 onLeadingPressed: Navigator.of(context).pop),
30 Padding(
31 padding: const EdgeInsets.symmetric(horizontal: 16.0),
lib/new-ui/widgets/receive_page/receive_address_type_selector.dart
+1
@@ -73,6 +73,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
73 ModalTopBar(
74 title: S.of(context).address_type,
75 leadingIcon: Icon(Icons.close),
76 + leadingSemanticLabel: S.of(context).close,
77 onLeadingPressed: Navigator.of(context).pop,
78 onTrailingPressed: () {}),
79 SizedBox(height: 24),
lib/new-ui/widgets/receive_page/receive_amount_modal.dart
+1
@@ -56,6 +56,7 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
56 onLeadingPressed: Navigator.of(context).pop,
57 onTrailingPressed: () {},
58 leadingIcon: Icon(Icons.close),
59 + leadingSemanticLabel: S.of(context).close,
60 ),
61 Padding(
62 padding: const EdgeInsets.all(18.0),
lib/new-ui/widgets/receive_page/receive_label_modal.dart
+1
@@ -66,6 +66,7 @@ class _ReceiveLabelModalState extends State<ReceiveLabelModal> {
66 ModalTopBar(
67 title: S.of(context).label_address,
68 leadingIcon: Icon(Icons.close),
69 + leadingSemanticLabel: S.of(context).close,
70 onLeadingPressed: Navigator.of(context).pop,
71 onTrailingPressed: () {}),
72 Padding(
lib/new-ui/widgets/receive_page/receive_top_bar.dart
+30 -4
@@ -14,7 +14,14 @@ class ModalTopBar extends StatelessWidget {
14 this.trailingIcon,
15 this.padding,
16 this.leadingWidget,
17 - this.trailingWidget}) {
17 + this.trailingWidget,
18 + this.leadingSemanticLabel,
19 + this.trailingSemanticLabel})
20 + : assert(leadingIcon == null || (leadingSemanticLabel != null && leadingSemanticLabel != ""),
21 + "leadingIcon requires a non-empty leadingSemanticLabel"),
22 + assert(
23 + trailingIcon == null || (trailingSemanticLabel != null && trailingSemanticLabel != ""),
24 + "trailingIcon requires a non-empty trailingSemanticLabel") {
25 if (leadingIcon != null && leadingWidget != null) {
26 throw Exception("Cannot have both leadingIcon and leadingWidget");
27 }
@@ -33,6 +40,17 @@ class ModalTopBar extends StatelessWidget {
40 final Widget? leadingWidget;
41 final Widget? trailingWidget;
42
43 + /// Accessible name for the leading chrome button. Required (and must be
44 + /// non-empty) whenever a [leadingIcon] is supplied, because the icon alone
45 + /// does not say whether it closes the modal or goes back. Must be localized
46 + /// by the caller.
47 + final String? leadingSemanticLabel;
48 +
49 + /// Accessible name for the trailing chrome button. Required (and must be
50 + /// non-empty) whenever a [trailingIcon] is supplied. Must be localized by
51 + /// the caller.
52 + final String? trailingSemanticLabel;
53 +
54 static const buttonSize = 36.0;
55
56 @override
@@ -51,10 +69,16 @@ class ModalTopBar extends StatelessWidget {
69 children: [
70 AnimatedSwitcher(
71 duration: const Duration(milliseconds: 200),
54 - child: Text(
55 - title,
72 + child: Semantics(
73 key: ValueKey(title),
57 - style: Theme.of(context).textTheme.headlineMedium,
74 + header: title.isNotEmpty,
75 + // Android reads the heading from headingLevel since the
76 + // Flutter 3.41 engine; header: alone only covers iOS.
77 + headingLevel: title.isNotEmpty ? 1 : null,
78 + child: Text(
79 + title,
80 + style: Theme.of(context).textTheme.headlineMedium,
81 + ),
82 ),
83 ),
84 if (subtitle != null && subtitle!.isNotEmpty)
@@ -77,6 +101,7 @@ class ModalTopBar extends StatelessWidget {
101 size: buttonSize,
102 onPressed: onLeadingPressed,
103 icon: leadingIcon!,
104 + semanticLabel: leadingSemanticLabel,
105 iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
106 : leadingWidget!
107 else
@@ -90,6 +115,7 @@ class ModalTopBar extends StatelessWidget {
115 size: buttonSize,
116 onPressed: onTrailingPressed,
117 icon: trailingIcon!,
118 + semanticLabel: trailingSemanticLabel,
119 )
120 : trailingWidget!,
121 )
lib/new-ui/widgets/scan_page/network_list.dart
+1
@@ -22,6 +22,7 @@ class ScanPageNetworkList extends StatelessWidget {
22 ModalTopBar(
23 title: S.of(context).compatible_services,
24 leadingIcon: Icon(Icons.arrow_back_ios_new),
25 + leadingSemanticLabel: S.of(context).seed_alert_back,
26 onLeadingPressed: Navigator.of(context).pop,
27 ),
28 Padding(
lib/new-ui/widgets/send_page/fiat_amount_bar.dart
+1
@@ -47,6 +47,7 @@ class FiatAmountBar extends StatelessWidget {
47 svgPath: "assets/new-ui/switch.svg",
48 iconSize: 18,
49 onPressed: onSwitchButtonPressed,
50 + semanticLabel: S.of(context).switch_input_currency,
51 ),
52 GestureDetector(
53 onTap: onSwitchButtonPressed,
lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart
+1
@@ -90,6 +90,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
90 ? "${S.of(context).send_from}..."
91 : "${S.of(context).receive_to}...",
92 leadingIcon: const Icon(Icons.arrow_back_ios_new),
93 + leadingSemanticLabel: S.of(context).seed_alert_back,
94 onLeadingPressed: Navigator.of(context).pop,
95 ),
96 Flexible(
lib/new-ui/widgets/send_page/l2_send_external_modal.dart
+1
@@ -73,6 +73,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
73 ],
74 ),
75 trailingIcon: Icon(Icons.close),
76 + trailingSemanticLabel: S.of(context).close,
77 onTrailingPressed: Navigator.of(context).pop,
78 ),
79 SafeArea(
lib/new-ui/widgets/send_page/send_confirm_sheet.dart
+1
@@ -161,6 +161,7 @@ class SendTransactionDetails extends StatelessWidget {
161 ],
162 ),
163 trailingIcon: Icon(Icons.close),
164 + trailingSemanticLabel: S.of(context).close,
165 onTrailingPressed: Navigator.of(context).maybePop,
166 ),
167 isPage
lib/new-ui/widgets/swap_page/provider_options_page.dart
+1
@@ -37,6 +37,7 @@ class ProviderOptionsPage extends StatelessWidget {
37 title: S.of(context).swap_providers,
38 onLeadingPressed: Navigator.of(context).pop,
39 leadingIcon: Icon(Icons.arrow_back_ios_new),
40 + leadingSemanticLabel: S.of(context).seed_alert_back,
41 ),
42 Expanded(
43 child: SingleChildScrollView(
lib/new-ui/widgets/swap_page/provider_selector_page.dart
+1
@@ -24,6 +24,7 @@ class ProviderSelectorPage extends StatelessWidget {
24 ModalTopBar(
25 title: S.of(context).change_provider,
26 leadingIcon: Icon(Icons.arrow_back_ios_new),
27 + leadingSemanticLabel: S.of(context).seed_alert_back,
28 onLeadingPressed: Navigator.of(context).maybePop,
29 ),
30 Expanded(
lib/new-ui/widgets/swap_page/refund_address_modal.dart
+1
@@ -44,6 +44,7 @@ class _RefundAddressModalState extends State<RefundAddressModal> {
44 ModalTopBar(
45 title: S.of(context).set_refund_address,
46 trailingIcon: Icon(Icons.close),
47 + trailingSemanticLabel: S.of(context).close,
48 onTrailingPressed: Navigator.of(context).pop,
49 ),
50 SafeArea(
lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart
+1
@@ -82,6 +82,7 @@ class _SwapAddressSelectionModalState extends State<SwapAddressSelectionModal> {
82 ? "${S.of(context).receive_to}..."
83 : "${S.of(context).send_from}...",
84 leadingIcon: Icon(Icons.close),
85 + leadingSemanticLabel: S.of(context).close,
86 onLeadingPressed: Navigator.of(context).pop,
87 ),
88 Flexible(
lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart
+1
@@ -156,6 +156,7 @@ class SwapTransactionDetails extends StatelessWidget {
156 fromIconPath: exchangeViewModel.depositCurrency.iconPath ?? "",
157 toIconPath: exchangeViewModel.receiveCurrency.iconPath ?? ""),
158 trailingIcon: Icon(Icons.close),
159 + trailingSemanticLabel: S.of(context).close,
160 onTrailingPressed: Navigator.of(context).maybePop,
161 ),
162 SafeArea(
lib/new-ui/widgets/swap_page/swap_options_page.dart
+1
@@ -25,6 +25,7 @@ class SwapOptionsPage extends StatelessWidget {
25 ModalTopBar(
26 title: S.of(context).configure,
27 leadingIcon: Icon(Icons.arrow_back_ios_new),
28 + leadingSemanticLabel: S.of(context).seed_alert_back,
29 onLeadingPressed: Navigator.of(context).pop,
30 ),
31 Padding(
lib/new-ui/widgets/swap_page/swap_send_external_modal.dart
+1
@@ -69,6 +69,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
69 leadingWidget: SwapModalHeader(
70 fromIconPath: widget.from.iconPath ?? "", toIconPath: widget.to.iconPath ?? ""),
71 trailingIcon: Icon(Icons.close),
72 + trailingSemanticLabel: S.of(context).close,
73 onTrailingPressed: Navigator.of(context).pop,
74 ),
75 Padding(
lib/new-ui/widgets/swap_page/trocador_providers_settings.dart
+1
@@ -25,6 +25,7 @@ class TrocadorProvidersSettings extends StatelessWidget {
25 ModalTopBar(
26 title: "Trocador ${S.of(context).providers}",
27 leadingIcon: Icon(Icons.arrow_back_ios_new),
28 + leadingSemanticLabel: S.of(context).seed_alert_back,
29 onLeadingPressed: Navigator.of(context).pop,
30 ),
31 Expanded(
lib/new-ui/widgets/wallet_deprecation_popup.dart
+2
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart';
3 import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
4 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
@@ -27,6 +28,7 @@ class WalletDeprecationPopup extends StatelessWidget {
28 ModalTopBar(
29 title: "",
30 leadingIcon: Icon(Icons.close),
31 + leadingSemanticLabel: S.of(context).close,
32 onLeadingPressed: Navigator.of(context).pop,
33 ),
34 Padding(
lib/src/screens/auth/auth_page.dart
+1
@@ -141,6 +141,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
141 size: 36,
142 icon: Icon(Icons.close),
143 onPressed: Navigator.of(context).pop,
144 + semanticLabel: S.of(context).close,
145 iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
146 : Container(),
147 backgroundColor: Theme.of(context).colorScheme.surface,
lib/src/screens/backup/edit_backup_password_page.dart
+1
@@ -36,6 +36,7 @@ class _EditBackupPasswordPageState extends State<EditBackupPasswordPage> {
36 ModalTopBar(
37 title: S.of(context).edit_backup_password,
38 leadingIcon: Icon(Icons.arrow_back_ios_new),
39 + leadingSemanticLabel: S.of(context).seed_alert_back,
40 onLeadingPressed: Navigator.of(context).pop,
41 ),
42 Expanded(
lib/src/screens/base_page.dart
+10 -14
@@ -84,20 +84,16 @@ abstract class BasePage extends StatelessWidget {
84 return null;
85 }
86
87 - return MergeSemantics(
88 - child: Padding(
89 - padding: const EdgeInsets.only(top: 4.0),
90 - child: ButtonTheme(
91 - minWidth: double.minPositive,
92 - child: Semantics(
93 - label: S.of(context).seed_alert_back,
94 - child: ModernButton(
95 - size: 37,
96 - icon: Icon(CupertinoIcons.back),
97 - iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
98 - onPressed: () => onClose(context),
99 - ),
100 - ),
87 + return Padding(
88 + padding: const EdgeInsets.only(top: 4.0),
89 + child: ButtonTheme(
90 + minWidth: double.minPositive,
91 + child: ModernButton(
92 + size: 37,
93 + icon: Icon(CupertinoIcons.back),
94 + semanticLabel: S.of(context).seed_alert_back,
95 + iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
96 + onPressed: () => onClose(context),
97 ),
98 ),
99 );
lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart
+1
@@ -24,6 +24,7 @@ class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
24 ModalTopBar(
25 title: S.of(context).passphrase_entry,
26 leadingIcon: const Icon(Icons.close),
27 + leadingSemanticLabel: S.of(context).close,
28 onLeadingPressed: Navigator.of(context).pop,
29 ),
30 Padding(
lib/src/screens/dashboard/favorite_token_modal.dart
+1
@@ -22,6 +22,7 @@ class FavoriteTokenModal extends StatelessWidget {
22 ModalTopBar(
23 title: S.of(context).favorite_token,
24 leadingIcon: Icon(Icons.close),
25 + leadingSemanticLabel: S.of(context).close,
26 onLeadingPressed: Navigator.of(context).pop,
27 ),
28 Expanded(
lib/src/screens/dashboard/sign_page.dart
+1
@@ -59,6 +59,7 @@ class _SignPageState extends State<SignPage> {
59 ModalTopBar(
60 title: S.current.sign_verify_title,
61 leadingIcon: Icon(Icons.arrow_back_ios_new),
62 + leadingSemanticLabel: S.current.seed_alert_back,
63 onLeadingPressed: Navigator.of(context).pop,
64 ),
65 Expanded(
lib/src/screens/nodes/node_create_or_edit_page.dart
+2
@@ -93,8 +93,10 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
93 ModalTopBar(
94 title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
95 leadingIcon: Icon(Icons.arrow_back_ios_new),
96 + leadingSemanticLabel: S.current.seed_alert_back,
97 onLeadingPressed: Navigator.of(context).pop,
98 trailingIcon: Icon(Icons.check),
99 + trailingSemanticLabel: S.current.save,
100 onTrailingPressed: () async {
101 if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
102 return;
lib/src/screens/nodes/node_share_page.dart
+1
@@ -24,6 +24,7 @@ class NodeSharePage extends StatelessWidget {
24 ModalTopBar(
25 title: S.of(context).share,
26 leadingIcon: Icon(Icons.arrow_back_ios_new),
27 + leadingSemanticLabel: S.of(context).seed_alert_back,
28 onLeadingPressed: Navigator.of(context).pop,
29 ),
30 Expanded(
lib/src/screens/nodes/pow_node_create_or_edit_page.dart
+2
@@ -75,6 +75,7 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
75 ModalTopBar(
76 title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
77 leadingIcon: Icon(Icons.arrow_back_ios_new),
78 + leadingSemanticLabel: S.current.seed_alert_back,
79 onLeadingPressed: Navigator.of(context).pop,
80 trailingIcon: CakeImageWidget(
81 imageUrl: "assets/new-ui/scan.svg",
@@ -83,6 +84,7 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
84 colorFilter:
85 ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
86 ),
87 + trailingSemanticLabel: S.current.scan,
88 onTrailingPressed: () =>
89 widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
90 ),
lib/src/screens/receive/address_list_page.dart
+1
@@ -22,6 +22,7 @@ class AddressListPage extends StatelessWidget {
22 ModalTopBar(
23 title: S.of(context).accounts_subaddresses,
24 leadingIcon: Icon(Icons.arrow_back_ios_new),
25 + leadingSemanticLabel: S.of(context).seed_alert_back,
26 onLeadingPressed: Navigator.of(context).pop,
27 ),
28 Expanded(
lib/src/screens/rescan/rescan_page.dart
+1
@@ -114,6 +114,7 @@ class _RescanPageState extends State<RescanPage> {
114 ? S.current.silent_payments_scanning
115 : S.current.rescan,
116 leadingIcon: Icon(Icons.arrow_back_ios_new),
117 + leadingSemanticLabel: S.current.seed_alert_back,
118 onLeadingPressed: Navigator.of(context).pop),
119 Expanded(
120 child: Padding(
lib/src/screens/settings/connection_sync_page.dart
+1
@@ -34,6 +34,7 @@ class ConnectionSyncPage extends BasePage {
34 topBar: ModalTopBar(
35 title: "",
36 leadingIcon: Icon(Icons.arrow_back_ios_new),
37 + leadingSemanticLabel: S.of(context).seed_alert_back,
38 onLeadingPressed: () => Navigator.of(context).pop()),
39 header: ModalHeader(
40 iconPath: "assets/new-ui/settings_row_icons/connections.svg",
lib/src/screens/settings/display_settings_page.dart
+1
@@ -35,6 +35,7 @@ class DisplaySettingsPage extends StatelessWidget {
35 topBar: ModalTopBar(
36 title: S.of(context).display,
37 leadingIcon: Icon(Icons.arrow_back_ios_new),
38 + leadingSemanticLabel: S.of(context).seed_alert_back,
39 onLeadingPressed: () => Navigator.of(context).pop(),
40 ),
41 content: Column(
lib/src/screens/settings/manage_nodes_page.dart
+3
@@ -40,6 +40,7 @@ class _ManageNodesPageState extends State<ManageNodesPage> {
40 topBar: ModalTopBar(
41 title: S.of(context).manage_nodes,
42 leadingIcon: Icon(Icons.arrow_back_ios_new),
43 + leadingSemanticLabel: S.of(context).seed_alert_back,
44 onLeadingPressed: () => Navigator.of(context).pop(),
45 trailingWidget: Row(
46 spacing: 8,
@@ -50,11 +51,13 @@ class _ManageNodesPageState extends State<ManageNodesPage> {
51 icon: widget.nodeListViewModel.isTestingNodeSpeed
52 ? CupertinoActivityIndicator()
53 : Icon(Icons.refresh),
54 + semanticLabel: S.of(context).test_node_speeds,
55 onPressed: () => widget.nodeListViewModel.speedTestNodes()),
56 ),
57 ModernButton(
58 size: 36,
59 icon: Icon(Icons.add),
60 + semanticLabel: S.of(context).add_new_node,
61 onPressed: () async {
62 final res = await Navigator.of(context)
63 .pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode);
lib/src/screens/settings/mweb_node_page.dart
+1
@@ -34,6 +34,7 @@ class _MwebNodePageState extends State<MwebNodePage> {
34 topBar: ModalTopBar(
35 title: S.current.litecoin_mweb_settings,
36 onLeadingPressed: Navigator.of(context).pop,
37 + leadingSemanticLabel: S.current.seed_alert_back,
38 leadingIcon: Icon(Icons.arrow_back_ios_new)),
39 content: Container(
40 child: NewListSections(controllers: {
lib/src/screens/settings/other_settings_page.dart
+1
@@ -42,6 +42,7 @@ class OtherSettingsPage extends BasePage {
42 topBar: ModalTopBar(
43 title: S.of(context).other,
44 leadingIcon: Icon(Icons.arrow_back_ios_new),
45 + leadingSemanticLabel: S.of(context).seed_alert_back,
46 onLeadingPressed: () => Navigator.of(context).pop(),
47 ),
48 // header: ModalHeader(
lib/src/screens/settings/privacy_page.dart
+1
@@ -26,6 +26,7 @@ class PrivacyPage extends BasePage {
26 topBar: ModalTopBar(
27 title: "",
28 leadingIcon: Icon(Icons.arrow_back_ios_new),
29 + leadingSemanticLabel: S.of(context).seed_alert_back,
30 onLeadingPressed: () => Navigator.of(context).pop(),
31 ),
32 header: ModalHeader(
lib/src/screens/settings/security_backup_page.dart
+1
@@ -45,6 +45,7 @@ class SecurityBackupPage extends BasePage {
45 topBar: ModalTopBar(
46 title: "",
47 leadingIcon: Icon(Icons.arrow_back_ios_new),
48 + leadingSemanticLabel: S.of(context).seed_alert_back,
49 onLeadingPressed: () => Navigator.of(context).pop()),
50 header: ModalHeader(
51 iconPath: "assets/new-ui/settings_row_icons/security.svg",
lib/src/screens/settings/silent_payments_settings.dart
+1
@@ -22,6 +22,7 @@ class SilentPaymentsSettingsPage extends StatelessWidget {
22 ModalTopBar(
23 title: S.current.silent_payments_settings,
24 leadingIcon: Icon(Icons.arrow_back_ios_new),
25 + leadingSemanticLabel: S.current.seed_alert_back,
26 onLeadingPressed: Navigator.of(context).pop,
27 ),
28 Expanded(
lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
+1
@@ -144,6 +144,7 @@ class _TOTPEnterCodeState extends State<TOTPEnterCode> {
144 ModalTopBar(
145 title: widget.isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa,
146 leadingIcon: Icon(Icons.arrow_back_ios_new),
147 + leadingSemanticLabel: S.current.seed_alert_back,
148 onLeadingPressed: Navigator.of(context).pop,
149 ),
150 Expanded(
lib/src/screens/support_chat/support_chat_page.dart
+1
@@ -22,6 +22,7 @@ class SupportChatPage extends StatelessWidget {
22 ModalTopBar(
23 title: S.current.settings_support,
24 leadingIcon: Icon(Icons.arrow_back_ios_new),
25 + leadingSemanticLabel: S.current.seed_alert_back,
26 onLeadingPressed: Navigator.of(context).pop),
27 FutureBuilder<String>(
28 future: getCookie(),
lib/src/screens/wallet_connect/wc_connections_listing_view.dart
+1
@@ -131,6 +131,7 @@ class WalletConnectConnectionsView extends StatelessWidget {
131 size: 40,
132 onPressed: () => Navigator.of(context).maybePop(),
133 icon: Icon(Icons.arrow_back_ios_new, size: 16),
134 + semanticLabel: S.of(context).seed_alert_back,
135 iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
136 ),
137 ),
res/values/strings_en.arb
+7
@@ -918,6 +918,7 @@
918 "return_address_desc": "In the rare case of a failed swap, our partners can automatically refund your payment to the provided refund address.",
919 "return_address_desc_external": "Since you chose Send From External, please set a refund address manually below.",
920 "robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.",
921 + "rotate_address": "Get new address",
922 "router_no_route": "No route defined for ${name}",
923 "salary": "Salary",
924 "save": "Save",
@@ -1147,10 +1148,13 @@
1148 "swap_id": "Swap ID",
1149 "swap_provider_initial_desc": "Select the type of swap provider you prefer to use when swapping in",
1150 "swap_providers": "Swap providers",
1151 + "swap_reverse_direction": "Reverse swap direction",
1152 "sweeping_wallet": "Sweeping wallet",
1153 "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
1154 "swipe_to_bridge": "Swipe to bridge",
1155 "swipe_to_send": "Swipe to send",
1156 + "switch_camera": "Switch camera",
1157 + "switch_input_currency": "Switch between crypto and fiat entry",
1158 "switch_wallet": "Switch Wallet",
1159 "switchToETHWallet": "Please switch to an Ethereum wallet and try again",
1160 "switchToWCCompatibleWallet": "Please switch to a WalletConnect compatible wallet and try again (${chains}).",
@@ -1187,6 +1191,7 @@
1191 "tap_to_copy": "Tap to copy",
1192 "template": "Template",
1193 "template_name": "Template Name",
1194 + "test_node_speeds": "Test node speeds",
1195 "testnet_coins_no_value": "Testnet coins have no value",
1196 "third_intro_content": "Yats live outside of Cake Wallet, too. Any wallet address on earth can be replaced with a Yat!",
1197 "third_intro_title": "Yat plays nicely with others",
@@ -1285,6 +1290,8 @@
1290 "trusted": "Trusted",
1291 "try_again": "Try again",
1292 "trying_to_spend_locked_funds": "Transaction rejected because you are trying to spend funds that have relative time lock, please wait and try again later, or disable the locked funds from coin control",
1293 + "turn_flash_off": "Turn flash off",
1294 + "turn_flash_on": "Turn flash on",
1295 "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.",
1296 "tx_commit_failed": "Transaction commit failed. Please contact support.",
1297 "tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second",