a11y: expose ModernButton, CoinActionButton and ModalTopBar as single labeled button nodes (#3463)
What changed, and why it matters
This commit is an accessibility (a11y) improvement, not a security fix. It makes icon-only buttons and modal titles readable by screen readers by attaching proper labels to them. There is no vulnerability being patched and no way for an attacker to exploit these changes.
No security action needed. Treat as a normal accessibility improvement; verify localized strings are present for supported languages and that assert failures are caught in CI.
Security signals we found
No security signals present in commit or diff
Accessibility-only refactor with no change to authentication, cryptography, networking, storage, or input handling
Evidence from the diff
The change exposes ModernButton, CoinActionButton, and ModalTopBar as single labeled Semantics nodes in Flutter. It adds required semanticLabel parameters, wraps controls in MergeSemantics + Semantics(button: true, label: …), excludes duplicate visible captions, fixes header flag placement on the title Text node, and adds headingLevel for Android. It also localizes previously hard-coded strings. No functional, pointer, or visual behavior changes; purely a screen-reader/assistive-technology improvement.
Changed components
lib/new-ui/widgets/modern_button.dartlib/new-ui/widgets/coins_page/action_row/coin_action_button.dartlib/new-ui/widgets/receive_page/receive_top_bar.dartlib/src/screens/base_page.dartres/values/strings_en.arbAll call sites using ModernButton, CoinActionButton, and ModalTopBarInspect captured patch +266 / −96
diff --git a/lib/new-ui/pages/about_page.dart b/lib/new-ui/pages/about_page.dart
index 533184e..766ae23 100644
--- a/lib/new-ui/pages/about_page.dart
+++ b/lib/new-ui/pages/about_page.dart
@@ -56,6 +56,7 @@ class _AboutPageState extends State<AboutPage> {
topBar: ModalTopBar(
title: S.of(context).about,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
content: Container(
diff --git a/lib/new-ui/pages/account_customizer.dart b/lib/new-ui/pages/account_customizer.dart
index 15aa1f0..8ebaf4c 100644
--- a/lib/new-ui/pages/account_customizer.dart
+++ b/lib/new-ui/pages/account_customizer.dart
@@ -128,8 +128,10 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: Icon(Icons.refresh),
+ trailingSemanticLabel: S.of(context).reset,
onTrailingPressed: showResetDialog,
),
Padding(
diff --git a/lib/new-ui/pages/addresses_page.dart b/lib/new-ui/pages/addresses_page.dart
index 62075e2..e75dc16 100644
--- a/lib/new-ui/pages/addresses_page.dart
+++ b/lib/new-ui/pages/addresses_page.dart
@@ -105,6 +105,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
ModalTopBar(
title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
leadingIcon: Icon(Icons.arrow_back),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
Expanded(
diff --git a/lib/new-ui/pages/bridge/bridge_amount_page.dart b/lib/new-ui/pages/bridge/bridge_amount_page.dart
index 757a957..d3255c1 100644
--- a/lib/new-ui/pages/bridge/bridge_amount_page.dart
+++ b/lib/new-ui/pages/bridge/bridge_amount_page.dart
@@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
ModalTopBar(
title: S.of(context).enter_amount,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
trailingIcon: Icon(Icons.history),
+ trailingSemanticLabel: S.of(context).history,
onTrailingPressed: () {
Navigator.pushNamed(context, Routes.bridgeHistoryPage,
arguments: widget.bridgeHistoryViewModel);
@@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
backgroundColor: theme.colorScheme.primary,
iconColor: theme.colorScheme.onPrimary,
icon: Icon(Icons.arrow_forward, size: 20),
+ semanticLabel: S.of(context).continue_text,
onPressed: () {
Navigator.pushNamed(
context,
diff --git a/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart b/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart
index 290666d..4ccbfb4 100644
--- a/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart
+++ b/lib/new-ui/pages/bridge/bridge_confirm_sheet.dart
@@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
],
),
trailingIcon: const Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
);
},
diff --git a/lib/new-ui/pages/bridge/bridge_detail_page.dart b/lib/new-ui/pages/bridge/bridge_detail_page.dart
index 64cc457..d29602b 100644
--- a/lib/new-ui/pages/bridge/bridge_detail_page.dart
+++ b/lib/new-ui/pages/bridge/bridge_detail_page.dart
@@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget {
ModalTopBar(
title: "Bridge Transfer Details",
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
Expanded(
diff --git a/lib/new-ui/pages/bridge/bridge_history_page.dart b/lib/new-ui/pages/bridge/bridge_history_page.dart
index 5dff00f..4959485 100644
--- a/lib/new-ui/pages/bridge/bridge_history_page.dart
+++ b/lib/new-ui/pages/bridge/bridge_history_page.dart
@@ -1,4 +1,5 @@
import 'package:cake_wallet/di.dart';
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
@@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget {
ModalTopBar(
title: "Bridge history",
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
Expanded(
diff --git a/lib/new-ui/pages/bridge/bridge_network_page.dart b/lib/new-ui/pages/bridge/bridge_network_page.dart
index 05f1c34..b0df527 100644
--- a/lib/new-ui/pages/bridge/bridge_network_page.dart
+++ b/lib/new-ui/pages/bridge/bridge_network_page.dart
@@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
@@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget {
ModalTopBar(
title: 'Destination Network',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
SizedBox(height: 48),
diff --git a/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart b/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart
index dfecf82..ee21669 100644
--- a/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart
+++ b/lib/new-ui/pages/bridge/bridge_receive_address_input_page.dart
@@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInpu
ModalTopBar(
title: 'Input Receive Address',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
SizedBox(height: 48),
diff --git a/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart b/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart
index 16872b5..6b4f3f0 100644
--- a/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart
+++ b/lib/new-ui/pages/bridge/bridge_receiving_wallet_page.dart
@@ -1,4 +1,5 @@
import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
@@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
ModalTopBar(
title: 'Receiving Wallet',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
const SizedBox(height: 48),
diff --git a/lib/new-ui/pages/card_customizer.dart b/lib/new-ui/pages/card_customizer.dart
index 9b2249d..9973ec2 100644
--- a/lib/new-ui/pages/card_customizer.dart
+++ b/lib/new-ui/pages/card_customizer.dart
@@ -84,6 +84,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
ModalTopBar(
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
diff --git a/lib/new-ui/pages/lightning_username_page.dart b/lib/new-ui/pages/lightning_username_page.dart
index 38c8014..c42c19f 100644
--- a/lib/new-ui/pages/lightning_username_page.dart
+++ b/lib/new-ui/pages/lightning_username_page.dart
@@ -68,6 +68,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
ModalTopBar(
title: "Lightning ${S.of(context).username}",
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart
index 4b7846c..e68a093 100644
--- a/lib/new-ui/pages/receive_page.dart
+++ b/lib/new-ui/pages/receive_page.dart
@@ -201,6 +201,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
ModalTopBar(
title: _largeQrMode ? "" : S.of(context).receive,
leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
@@ -223,6 +224,9 @@ class _NewReceivePageState extends State<NewReceivePage> {
: widget.addressListViewModel.isRotatingAddress
? const CupertinoActivityIndicator()
: const Icon(Icons.refresh),
+ semanticLabel: _largeQrMode
+ ? S.of(context).share_address
+ : S.of(context).rotate_address,
onPressed: () {
if (_largeQrMode) {
ShareUtil.share(
diff --git a/lib/new-ui/pages/scan_page.dart b/lib/new-ui/pages/scan_page.dart
index 16dcb38..695382b 100644
--- a/lib/new-ui/pages/scan_page.dart
+++ b/lib/new-ui/pages/scan_page.dart
@@ -163,6 +163,7 @@ class _ScanPageState extends State<ScanPage> {
},
iconColor: buttonIconColor,
backgroundColor: buttonColor,
+ semanticLabel: S.of(context).switch_camera,
),
ModernButton(
size: 36,
@@ -171,6 +172,10 @@ class _ScanPageState extends State<ScanPage> {
(controller.value.torchState == TorchState.on || _frontFlashMode)
? Icons.flash_off_outlined
: Icons.flash_on),
+ semanticLabel:
+ (controller.value.torchState == TorchState.on || _frontFlashMode)
+ ? S.of(context).turn_flash_off
+ : S.of(context).turn_flash_on,
onPressed: () {
if (controller.value.cameraDirection == CameraFacing.front) {
setState(() {
@@ -194,6 +199,7 @@ class _ScanPageState extends State<ScanPage> {
size: 36,
iconSize: 18,
icon: Icon(Icons.arrow_back_ios_new),
+ semanticLabel: S.of(context).seed_alert_back,
onPressed: () {
if (_textInputMode) {
setState(() {
diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart
index 7e38680..f033817 100644
--- a/lib/new-ui/pages/send_page.dart
+++ b/lib/new-ui/pages/send_page.dart
@@ -328,6 +328,7 @@ class _NewSendPageState extends State<NewSendPage> {
title: widget.mode.title,
subtitle: widget.mode.description,
leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
trailingWidget: Observer(
builder: (_) => Row(
@@ -343,6 +344,7 @@ class _NewSendPageState extends State<NewSendPage> {
BlendMode.srcIn,
),
),
+ semanticLabel: S.of(context).remove,
onPressed: () {
final outputIndex = _selectedOutput;
if (_selectedOutput != 0) {
@@ -362,6 +364,7 @@ class _NewSendPageState extends State<NewSendPage> {
ModernButton(
size: 36,
icon: const Icon(Icons.add),
+ semanticLabel: S.of(context).add_receiver,
onPressed: () {
_addInputControllers();
widget.sendViewModel.addOutput();
@@ -378,6 +381,7 @@ class _NewSendPageState extends State<NewSendPage> {
BlendMode.srcIn,
),
),
+ semanticLabel: S.of(context).help,
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
@@ -1492,6 +1496,7 @@ class SendHelpPage extends StatelessWidget {
ModalTopBar(
title: content.title,
leadingIcon: const Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
diff --git a/lib/new-ui/pages/settings_page.dart b/lib/new-ui/pages/settings_page.dart
index f7b8d4c..2183ec5 100644
--- a/lib/new-ui/pages/settings_page.dart
+++ b/lib/new-ui/pages/settings_page.dart
@@ -152,8 +152,9 @@ class SettingsMainPage extends StatelessWidget {
color: Theme.of(context).colorScheme.surface,
child: Column(children: [
ModalTopBar(
- title: "Settings",
+ title: S.of(context).settings_title,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
onTrailingPressed: () {},
),
diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart
index 4f82a4e..6d3d704 100644
--- a/lib/new-ui/pages/swap_page.dart
+++ b/lib/new-ui/pages/swap_page.dart
@@ -472,12 +472,14 @@ class _NewSwapPageState extends State<NewSwapPage> {
ModalTopBar(
title: S.of(context).swap,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: CakeImageWidget(
imageUrl: "assets/new-ui/options.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
+ trailingSemanticLabel: S.of(context).configure,
onTrailingPressed: () {
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => Material(
@@ -576,6 +578,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
iconSize: 24,
svgPath: "assets/new-ui/swap_amounts.svg",
onPressed: widget.exchangeViewModel.reverseSwapDirection,
+ semanticLabel: S.of(context).swap_reverse_direction,
),
],
),
@@ -1174,6 +1177,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
size: 36,
iconSize: 18,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
+ semanticLabel: S.of(context).refund_address,
),
if (widget.isReceiverCard &&
widget.exchangeViewModel.receiveAddress.isEmpty) ...[
@@ -1183,6 +1187,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
size: 36,
iconSize: 20,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
+ semanticLabel: S.of(context).paste,
),
ModernButton.svg(
svgPath: "assets/new-ui/scan.svg",
@@ -1190,6 +1195,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
size: 36,
iconSize: 20,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
+ semanticLabel: S.of(context).scan,
),
]
],
diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart
index 4c02acf..cb2ff10 100644
--- a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart
+++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart
@@ -23,47 +23,57 @@ class CoinActionButton extends StatelessWidget {
final size = MediaQuery.of(context).size.width * sizeFactor;
final double effectiveSize = min(size, 80);
- return Column(
- children: [
- Container(
- width: effectiveSize,
- height: effectiveSize,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- gradient: LinearGradient(
- colors: [
- context.customColors.cardGradientColorPrimary,
- context.customColors.cardGradientColorSecondary
- ],
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- ),
- border: Border.all(
- color: Theme.of(context).colorScheme.surfaceContainerHigh,
- width: 1,
+ // The caption below the circle is the accessible name, so it is excluded
+ // from the semantics tree to keep this a single button node.
+ return MergeSemantics(
+ child: Semantics(
+ button: true,
+ label: label,
+ child: Column(
+ children: [
+ Container(
+ width: effectiveSize,
+ height: effectiveSize,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ colors: [
+ context.customColors.cardGradientColorPrimary,
+ context.customColors.cardGradientColorSecondary
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ ),
+ border: Border.all(
+ color: Theme.of(context).colorScheme.surfaceContainerHigh,
+ width: 1,
+ ),
+ ),
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ onPressed: () {
+ HapticFeedback.mediumImpact();
+ action();
+ },
+ icon: ExcludeSemantics(child: icon),
+ color: Theme.of(context).colorScheme.primary,
+ ),
),
- ),
- child: IconButton(
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(),
- onPressed: () {
- HapticFeedback.mediumImpact();
- action();
- },
- icon: icon,
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(top: 8.0),
- child: Text(
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Theme.of(context).colorScheme.onSurfaceVariant,
+ Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: ExcludeSemantics(
+ child: Text(
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ label,
),
- label,
- ),
+ ),
+ ),
+ ],
),
- ],
+ ),
);
}
}
diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart
index 3bb7084..0ee643c 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart
@@ -62,6 +62,7 @@ class AssetDetailsModal extends StatelessWidget {
ModalTopBar(
title: "",
trailingIcon: Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).pop,
padding: EdgeInsets.only(top: 12, right: 18),
),
@@ -356,17 +357,22 @@ class AssetDetailsModalBottomButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Column(
- spacing: 8,
- children: [
- ModernButton.svg(
- onPressed: onPressed,
- svgPath: iconPath,
- size: 60,
- iconSize: 32,
- ),
- Text(title)
- ],
+ // The caption is the button's accessible name, so it is excluded from the
+ // semantics tree to keep the whole control a single button node.
+ return MergeSemantics(
+ child: Column(
+ spacing: 8,
+ children: [
+ ModernButton.svg(
+ onPressed: onPressed,
+ svgPath: iconPath,
+ size: 60,
+ iconSize: 32,
+ semanticLabel: title,
+ ),
+ ExcludeSemantics(child: Text(title))
+ ],
+ ),
);
}
}
diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart b/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart
index 97c9260..383ced5 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/history_filters_page.dart
@@ -27,6 +27,7 @@ class HistoryFiltersPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).filters,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart
index c357799..9132667 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/history_modal.dart
@@ -25,11 +25,13 @@ class HistoryModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).history,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: CakeImageWidget(
imageUrl: "assets/new-ui/tx_export.svg",
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
+ trailingSemanticLabel: S.of(context).export_csv,
onTrailingPressed: () =>
CsvExportService().exportToCsv(dashboardViewModel.items, context),
),
diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart b/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart
index 16051f3..d58773f 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart
@@ -24,6 +24,7 @@ class HistorySwapProvidersPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).filter_swap_providers,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
index fb705ac..4002d23 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
@@ -68,6 +68,7 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
ModalTopBar(
title: S.of(context).transaction,
leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
index de08f70..71627f9 100644
--- a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
+++ b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
@@ -53,6 +54,7 @@ class TopBar extends StatelessWidget {
onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
+ semanticLabel: S.of(context).settings_title,
),
],
),
diff --git a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart
index 5420d6a..461b816 100644
--- a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart
+++ b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart
@@ -125,6 +125,7 @@ class UnconfirmedBalanceModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).balance_confirmation,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart
index 212f577..fa77237 100644
--- a/lib/new-ui/widgets/coins_page/wallet_info.dart
+++ b/lib/new-ui/widgets/coins_page/wallet_info.dart
@@ -1,3 +1,4 @@
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/wallet_info.dart';
@@ -76,6 +77,7 @@ class WalletInfoBar extends StatelessWidget {
}
},
svgPath: "assets/new-ui/icon-accounts.svg",
+ semanticLabel: S.of(context).wallet_accounts,
)
]
],
diff --git a/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart b/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart
index be6600d..a59b8d7 100644
--- a/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart
+++ b/lib/new-ui/widgets/currency_picker/currency_picker_sheet.dart
@@ -43,6 +43,7 @@ class CurrencyPickerSheet extends StatelessWidget {
ModalTopBar(
title: S.of(context).select_asset,
leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
diff --git a/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart b/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart
index 47dba9f..c471370 100644
--- a/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart
+++ b/lib/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart
@@ -117,6 +117,7 @@ class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
ModalTopBar(
title: S.of(context).select_fiat_currency_title,
leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
diff --git a/lib/new-ui/widgets/currency_picker/select_network_page.dart b/lib/new-ui/widgets/currency_picker/select_network_page.dart
index 988dc3b..29f18af 100644
--- a/lib/new-ui/widgets/currency_picker/select_network_page.dart
+++ b/lib/new-ui/widgets/currency_picker/select_network_page.dart
@@ -35,6 +35,7 @@ class SelectNetworkPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).select_network_title,
leadingIcon: const Icon(Icons.arrow_back),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
),
const SizedBox(height: 24),
diff --git a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart
index 2a06f37..b68a401 100644
--- a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart
+++ b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart
@@ -106,6 +106,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
Icons.close,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => showPopUp(
context: context,
builder: (context) => AlertWithTwoActions(
@@ -143,6 +144,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
iconColor: Theme.of(context).colorScheme.onPrimary,
size: 36,
icon: Icon(Icons.arrow_forward),
+ semanticLabel: S.of(context).confirm,
onPressed: () =>
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
),
diff --git a/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart b/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart
index 9b429ed..5d0dcf1 100644
--- a/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart
+++ b/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart
@@ -32,6 +32,7 @@ class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorPa
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {},
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
),
Padding(
padding: const EdgeInsets.all(18.0),
diff --git a/lib/new-ui/widgets/modern_button.dart b/lib/new-ui/widgets/modern_button.dart
index 6f5e5a5..bba456d 100644
--- a/lib/new-ui/widgets/modern_button.dart
+++ b/lib/new-ui/widgets/modern_button.dart
@@ -11,6 +11,11 @@ class ModernButton extends StatelessWidget {
final Color? backgroundColor;
final double? iconSize;
final String? label;
+
+ /// Accessible name announced by screen readers. Falls back to [label] (the
+ /// visible caption) when omitted, so one of the two is required. Must be
+ /// localized by the caller.
+ final String? semanticLabel;
static const iconSvgSizeRatio = 2 / 3;
const ModernButton(
@@ -21,8 +26,11 @@ class ModernButton extends StatelessWidget {
this.iconSize,
this.iconColor,
this.backgroundColor,
- this.label})
- : svgPath = null;
+ this.label,
+ this.semanticLabel})
+ : svgPath = null,
+ assert(semanticLabel != null || (label != null && label != ""),
+ "ModernButton needs a semanticLabel when it has no visible label");
const ModernButton.svg(
{super.key,
@@ -32,8 +40,11 @@ class ModernButton extends StatelessWidget {
this.iconSize,
this.iconColor,
this.backgroundColor,
- this.label})
- : icon = null;
+ this.label,
+ this.semanticLabel})
+ : icon = null,
+ assert(semanticLabel != null || (label != null && label != ""),
+ "ModernButton needs a semanticLabel when it has no visible label");
@override
Widget build(BuildContext context) {
@@ -56,31 +67,42 @@ class ModernButton extends StatelessWidget {
child: icon!,
);
- return Column(
- spacing: 8,
- children: [
- Container(
- decoration: BoxDecoration(
- color: resolvedBackgroundColor,
- borderRadius: BorderRadius.circular(size),
- ),
- width: size,
- height: size,
- child: IconButton(
- padding: EdgeInsets.zero,
- onPressed: onPressed,
- icon: resolvedIcon,
- ),
+ // A single accessibility node for the whole control: the visible caption
+ // duplicates the accessible name, so it is excluded to avoid a second,
+ // detached stop for screen readers.
+ return MergeSemantics(
+ child: Semantics(
+ button: true,
+ label: semanticLabel ?? label,
+ child: Column(
+ spacing: 8,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ color: resolvedBackgroundColor,
+ borderRadius: BorderRadius.circular(size),
+ ),
+ width: size,
+ height: size,
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ onPressed: onPressed,
+ icon: ExcludeSemantics(child: resolvedIcon),
+ ),
+ ),
+ if (label != null && label!.isNotEmpty)
+ ExcludeSemantics(
+ child: Text(
+ label!,
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w400,
+ color: Theme.of(context).colorScheme.onSurface),
+ ),
+ )
+ ],
),
- if (label != null && label!.isNotEmpty)
- Text(
- label!,
- style: TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w400,
- color: Theme.of(context).colorScheme.onSurface),
- )
- ],
+ ),
);
}
}
diff --git a/lib/new-ui/widgets/picker.dart b/lib/new-ui/widgets/picker.dart
index d654e4e..3520dab 100644
--- a/lib/new-ui/widgets/picker.dart
+++ b/lib/new-ui/widgets/picker.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -77,6 +78,7 @@ class _NewPickerState<Item> extends State<NewPicker<Item>> {
ModalTopBar(
title: widget.title,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).maybePop,
),
Padding(
@@ -387,6 +389,7 @@ class _PickerSliderPageState<Item> extends State<PickerSliderPage<Item>> {
ModalTopBar(
title: widget.title,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart b/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart
index 06ec6b4..8fd7c17 100644
--- a/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart
+++ b/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart
@@ -25,6 +25,7 @@ class PayjoinCopyModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).select_address_to_copy,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
diff --git a/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart b/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart
index d628573..1a3ce73 100644
--- a/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart
+++ b/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart
@@ -73,6 +73,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
ModalTopBar(
title: S.of(context).address_type,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
SizedBox(height: 24),
diff --git a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart
index 8828870..bbc1dae 100644
--- a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart
+++ b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart
@@ -56,6 +56,7 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {},
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
),
Padding(
padding: const EdgeInsets.all(18.0),
diff --git a/lib/new-ui/widgets/receive_page/receive_label_modal.dart b/lib/new-ui/widgets/receive_page/receive_label_modal.dart
index 9dc05c1..60e4c01 100644
--- a/lib/new-ui/widgets/receive_page/receive_label_modal.dart
+++ b/lib/new-ui/widgets/receive_page/receive_label_modal.dart
@@ -66,6 +66,7 @@ class _ReceiveLabelModalState extends State<ReceiveLabelModal> {
ModalTopBar(
title: S.of(context).label_address,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
Padding(
diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart
index 97b72b1..374c58d 100644
--- a/lib/new-ui/widgets/receive_page/receive_top_bar.dart
+++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart
@@ -14,7 +14,14 @@ class ModalTopBar extends StatelessWidget {
this.trailingIcon,
this.padding,
this.leadingWidget,
- this.trailingWidget}) {
+ this.trailingWidget,
+ this.leadingSemanticLabel,
+ this.trailingSemanticLabel})
+ : assert(leadingIcon == null || (leadingSemanticLabel != null && leadingSemanticLabel != ""),
+ "leadingIcon requires a non-empty leadingSemanticLabel"),
+ assert(
+ trailingIcon == null || (trailingSemanticLabel != null && trailingSemanticLabel != ""),
+ "trailingIcon requires a non-empty trailingSemanticLabel") {
if (leadingIcon != null && leadingWidget != null) {
throw Exception("Cannot have both leadingIcon and leadingWidget");
}
@@ -33,6 +40,17 @@ class ModalTopBar extends StatelessWidget {
final Widget? leadingWidget;
final Widget? trailingWidget;
+ /// Accessible name for the leading chrome button. Required (and must be
+ /// non-empty) whenever a [leadingIcon] is supplied, because the icon alone
+ /// does not say whether it closes the modal or goes back. Must be localized
+ /// by the caller.
+ final String? leadingSemanticLabel;
+
+ /// Accessible name for the trailing chrome button. Required (and must be
+ /// non-empty) whenever a [trailingIcon] is supplied. Must be localized by
+ /// the caller.
+ final String? trailingSemanticLabel;
+
static const buttonSize = 36.0;
@override
@@ -51,10 +69,16 @@ class ModalTopBar extends StatelessWidget {
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
- child: Text(
- title,
+ child: Semantics(
key: ValueKey(title),
- style: Theme.of(context).textTheme.headlineMedium,
+ header: title.isNotEmpty,
+ // Android reads the heading from headingLevel since the
+ // Flutter 3.41 engine; header: alone only covers iOS.
+ headingLevel: title.isNotEmpty ? 1 : null,
+ child: Text(
+ title,
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
),
),
if (subtitle != null && subtitle!.isNotEmpty)
@@ -77,6 +101,7 @@ class ModalTopBar extends StatelessWidget {
size: buttonSize,
onPressed: onLeadingPressed,
icon: leadingIcon!,
+ semanticLabel: leadingSemanticLabel,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
: leadingWidget!
else
@@ -90,6 +115,7 @@ class ModalTopBar extends StatelessWidget {
size: buttonSize,
onPressed: onTrailingPressed,
icon: trailingIcon!,
+ semanticLabel: trailingSemanticLabel,
)
: trailingWidget!,
)
diff --git a/lib/new-ui/widgets/scan_page/network_list.dart b/lib/new-ui/widgets/scan_page/network_list.dart
index aa3d8e4..007ba51 100644
--- a/lib/new-ui/widgets/scan_page/network_list.dart
+++ b/lib/new-ui/widgets/scan_page/network_list.dart
@@ -22,6 +22,7 @@ class ScanPageNetworkList extends StatelessWidget {
ModalTopBar(
title: S.of(context).compatible_services,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
diff --git a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart
index bd383b5..76cbcfc 100644
--- a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart
+++ b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart
@@ -47,6 +47,7 @@ class FiatAmountBar extends StatelessWidget {
svgPath: "assets/new-ui/switch.svg",
iconSize: 18,
onPressed: onSwitchButtonPressed,
+ semanticLabel: S.of(context).switch_input_currency,
),
GestureDetector(
onTap: onSwitchButtonPressed,
diff --git a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart
index 835631e..e9aac02 100644
--- a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart
+++ b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart
@@ -90,6 +90,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
? "${S.of(context).send_from}..."
: "${S.of(context).receive_to}...",
leadingIcon: const Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Flexible(
diff --git a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart
index 82c077d..6fb6ddf 100644
--- a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart
+++ b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart
@@ -73,6 +73,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
],
),
trailingIcon: Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).pop,
),
SafeArea(
diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart
index b6df11d..dccaf41 100644
--- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart
+++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart
@@ -161,6 +161,7 @@ class SendTransactionDetails extends StatelessWidget {
],
),
trailingIcon: Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).maybePop,
),
isPage
diff --git a/lib/new-ui/widgets/swap_page/provider_options_page.dart b/lib/new-ui/widgets/swap_page/provider_options_page.dart
index 782a5b0..8833345 100644
--- a/lib/new-ui/widgets/swap_page/provider_options_page.dart
+++ b/lib/new-ui/widgets/swap_page/provider_options_page.dart
@@ -37,6 +37,7 @@ class ProviderOptionsPage extends StatelessWidget {
title: S.of(context).swap_providers,
onLeadingPressed: Navigator.of(context).pop,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
),
Expanded(
child: SingleChildScrollView(
diff --git a/lib/new-ui/widgets/swap_page/provider_selector_page.dart b/lib/new-ui/widgets/swap_page/provider_selector_page.dart
index e521ea9..c8e15e8 100644
--- a/lib/new-ui/widgets/swap_page/provider_selector_page.dart
+++ b/lib/new-ui/widgets/swap_page/provider_selector_page.dart
@@ -24,6 +24,7 @@ class ProviderSelectorPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).change_provider,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).maybePop,
),
Expanded(
diff --git a/lib/new-ui/widgets/swap_page/refund_address_modal.dart b/lib/new-ui/widgets/swap_page/refund_address_modal.dart
index 6a8f0e2..031d67e 100644
--- a/lib/new-ui/widgets/swap_page/refund_address_modal.dart
+++ b/lib/new-ui/widgets/swap_page/refund_address_modal.dart
@@ -44,6 +44,7 @@ class _RefundAddressModalState extends State<RefundAddressModal> {
ModalTopBar(
title: S.of(context).set_refund_address,
trailingIcon: Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).pop,
),
SafeArea(
diff --git a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart
index 67eb2d1..8c6d834 100644
--- a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart
+++ b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart
@@ -82,6 +82,7 @@ class _SwapAddressSelectionModalState extends State<SwapAddressSelectionModal> {
? "${S.of(context).receive_to}..."
: "${S.of(context).send_from}...",
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
),
Flexible(
diff --git a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart
index c948d6b..b562605 100644
--- a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart
+++ b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart
@@ -156,6 +156,7 @@ class SwapTransactionDetails extends StatelessWidget {
fromIconPath: exchangeViewModel.depositCurrency.iconPath ?? "",
toIconPath: exchangeViewModel.receiveCurrency.iconPath ?? ""),
trailingIcon: Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).maybePop,
),
SafeArea(
diff --git a/lib/new-ui/widgets/swap_page/swap_options_page.dart b/lib/new-ui/widgets/swap_page/swap_options_page.dart
index b472644..98c710e 100644
--- a/lib/new-ui/widgets/swap_page/swap_options_page.dart
+++ b/lib/new-ui/widgets/swap_page/swap_options_page.dart
@@ -25,6 +25,7 @@ class SwapOptionsPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).configure,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
diff --git a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart
index f48d96a..6b33af2 100644
--- a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart
+++ b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart
@@ -69,6 +69,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
leadingWidget: SwapModalHeader(
fromIconPath: widget.from.iconPath ?? "", toIconPath: widget.to.iconPath ?? ""),
trailingIcon: Icon(Icons.close),
+ trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).pop,
),
Padding(
diff --git a/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart b/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart
index 36474ac..a07b2cd 100644
--- a/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart
+++ b/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart
@@ -25,6 +25,7 @@ class TrocadorProvidersSettings extends StatelessWidget {
ModalTopBar(
title: "Trocador ${S.of(context).providers}",
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/new-ui/widgets/wallet_deprecation_popup.dart b/lib/new-ui/widgets/wallet_deprecation_popup.dart
index 2b233c0..a28c5d1 100644
--- a/lib/new-ui/widgets/wallet_deprecation_popup.dart
+++ b/lib/new-ui/widgets/wallet_deprecation_popup.dart
@@ -1,3 +1,4 @@
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
@@ -27,6 +28,7 @@ class WalletDeprecationPopup extends StatelessWidget {
ModalTopBar(
title: "",
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart
index 4cee265..df1b72b 100644
--- a/lib/src/screens/auth/auth_page.dart
+++ b/lib/src/screens/auth/auth_page.dart
@@ -141,6 +141,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
size: 36,
icon: Icon(Icons.close),
onPressed: Navigator.of(context).pop,
+ semanticLabel: S.of(context).close,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
: Container(),
backgroundColor: Theme.of(context).colorScheme.surface,
diff --git a/lib/src/screens/backup/edit_backup_password_page.dart b/lib/src/screens/backup/edit_backup_password_page.dart
index d3ac5e7..4abb460 100644
--- a/lib/src/screens/backup/edit_backup_password_page.dart
+++ b/lib/src/screens/backup/edit_backup_password_page.dart
@@ -36,6 +36,7 @@ class _EditBackupPasswordPageState extends State<EditBackupPasswordPage> {
ModalTopBar(
title: S.of(context).edit_backup_password,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart
index 4602c92..ef119fa 100644
--- a/lib/src/screens/base_page.dart
+++ b/lib/src/screens/base_page.dart
@@ -84,20 +84,16 @@ abstract class BasePage extends StatelessWidget {
return null;
}
- return MergeSemantics(
- child: Padding(
- padding: const EdgeInsets.only(top: 4.0),
- child: ButtonTheme(
- minWidth: double.minPositive,
- child: Semantics(
- label: S.of(context).seed_alert_back,
- child: ModernButton(
- size: 37,
- icon: Icon(CupertinoIcons.back),
- iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
- onPressed: () => onClose(context),
- ),
- ),
+ return Padding(
+ padding: const EdgeInsets.only(top: 4.0),
+ child: ButtonTheme(
+ minWidth: double.minPositive,
+ child: ModernButton(
+ size: 37,
+ icon: Icon(CupertinoIcons.back),
+ semanticLabel: S.of(context).seed_alert_back,
+ iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
+ onPressed: () => onClose(context),
),
),
);
diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart
index 5db5200..f893b53 100644
--- a/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart
+++ b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart
@@ -24,6 +24,7 @@ class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).passphrase_entry,
leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
diff --git a/lib/src/screens/dashboard/favorite_token_modal.dart b/lib/src/screens/dashboard/favorite_token_modal.dart
index ef46114..1b0c9ac 100644
--- a/lib/src/screens/dashboard/favorite_token_modal.dart
+++ b/lib/src/screens/dashboard/favorite_token_modal.dart
@@ -22,6 +22,7 @@ class FavoriteTokenModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).favorite_token,
leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/dashboard/sign_page.dart b/lib/src/screens/dashboard/sign_page.dart
index 5e805c3..4a6f70b 100644
--- a/lib/src/screens/dashboard/sign_page.dart
+++ b/lib/src/screens/dashboard/sign_page.dart
@@ -59,6 +59,7 @@ class _SignPageState extends State<SignPage> {
ModalTopBar(
title: S.current.sign_verify_title,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart
index 1ac1d5e..78a76f8 100644
--- a/lib/src/screens/nodes/node_create_or_edit_page.dart
+++ b/lib/src/screens/nodes/node_create_or_edit_page.dart
@@ -93,8 +93,10 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
ModalTopBar(
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
trailingIcon: Icon(Icons.check),
+ trailingSemanticLabel: S.current.save,
onTrailingPressed: () async {
if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
return;
diff --git a/lib/src/screens/nodes/node_share_page.dart b/lib/src/screens/nodes/node_share_page.dart
index bb96b5b..1258f12 100644
--- a/lib/src/screens/nodes/node_share_page.dart
+++ b/lib/src/screens/nodes/node_share_page.dart
@@ -24,6 +24,7 @@ class NodeSharePage extends StatelessWidget {
ModalTopBar(
title: S.of(context).share,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart
index 0763fc8..0e98960 100644
--- a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart
+++ b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart
@@ -75,6 +75,7 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
ModalTopBar(
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
trailingIcon: CakeImageWidget(
imageUrl: "assets/new-ui/scan.svg",
@@ -83,6 +84,7 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
+ trailingSemanticLabel: S.current.scan,
onTrailingPressed: () =>
widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
),
diff --git a/lib/src/screens/receive/address_list_page.dart b/lib/src/screens/receive/address_list_page.dart
index abcf9e1..f310a55 100644
--- a/lib/src/screens/receive/address_list_page.dart
+++ b/lib/src/screens/receive/address_list_page.dart
@@ -22,6 +22,7 @@ class AddressListPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).accounts_subaddresses,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart
index e678e01..8d73950 100644
--- a/lib/src/screens/rescan/rescan_page.dart
+++ b/lib/src/screens/rescan/rescan_page.dart
@@ -114,6 +114,7 @@ class _RescanPageState extends State<RescanPage> {
? S.current.silent_payments_scanning
: S.current.rescan,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop),
Expanded(
child: Padding(
diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart
index 6406f58..fb251f5 100644
--- a/lib/src/screens/settings/connection_sync_page.dart
+++ b/lib/src/screens/settings/connection_sync_page.dart
@@ -34,6 +34,7 @@ class ConnectionSyncPage extends BasePage {
topBar: ModalTopBar(
title: "",
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop()),
header: ModalHeader(
iconPath: "assets/new-ui/settings_row_icons/connections.svg",
diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart
index 6a3fa6f..392a622 100644
--- a/lib/src/screens/settings/display_settings_page.dart
+++ b/lib/src/screens/settings/display_settings_page.dart
@@ -35,6 +35,7 @@ class DisplaySettingsPage extends StatelessWidget {
topBar: ModalTopBar(
title: S.of(context).display,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
content: Column(
diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart
index c750397..f867ef2 100644
--- a/lib/src/screens/settings/manage_nodes_page.dart
+++ b/lib/src/screens/settings/manage_nodes_page.dart
@@ -40,6 +40,7 @@ class _ManageNodesPageState extends State<ManageNodesPage> {
topBar: ModalTopBar(
title: S.of(context).manage_nodes,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
trailingWidget: Row(
spacing: 8,
@@ -50,11 +51,13 @@ class _ManageNodesPageState extends State<ManageNodesPage> {
icon: widget.nodeListViewModel.isTestingNodeSpeed
? CupertinoActivityIndicator()
: Icon(Icons.refresh),
+ semanticLabel: S.of(context).test_node_speeds,
onPressed: () => widget.nodeListViewModel.speedTestNodes()),
),
ModernButton(
size: 36,
icon: Icon(Icons.add),
+ semanticLabel: S.of(context).add_new_node,
onPressed: () async {
final res = await Navigator.of(context)
.pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode);
diff --git a/lib/src/screens/settings/mweb_node_page.dart b/lib/src/screens/settings/mweb_node_page.dart
index f5be67e..a05feed 100644
--- a/lib/src/screens/settings/mweb_node_page.dart
+++ b/lib/src/screens/settings/mweb_node_page.dart
@@ -34,6 +34,7 @@ class _MwebNodePageState extends State<MwebNodePage> {
topBar: ModalTopBar(
title: S.current.litecoin_mweb_settings,
onLeadingPressed: Navigator.of(context).pop,
+ leadingSemanticLabel: S.current.seed_alert_back,
leadingIcon: Icon(Icons.arrow_back_ios_new)),
content: Container(
child: NewListSections(controllers: {
diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart
index 7a08d6d..3f52919 100644
--- a/lib/src/screens/settings/other_settings_page.dart
+++ b/lib/src/screens/settings/other_settings_page.dart
@@ -42,6 +42,7 @@ class OtherSettingsPage extends BasePage {
topBar: ModalTopBar(
title: S.of(context).other,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
// header: ModalHeader(
diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart
index 3d8b651..3e35f08 100644
--- a/lib/src/screens/settings/privacy_page.dart
+++ b/lib/src/screens/settings/privacy_page.dart
@@ -26,6 +26,7 @@ class PrivacyPage extends BasePage {
topBar: ModalTopBar(
title: "",
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
header: ModalHeader(
diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart
index fc8f5a1..9161895 100644
--- a/lib/src/screens/settings/security_backup_page.dart
+++ b/lib/src/screens/settings/security_backup_page.dart
@@ -45,6 +45,7 @@ class SecurityBackupPage extends BasePage {
topBar: ModalTopBar(
title: "",
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop()),
header: ModalHeader(
iconPath: "assets/new-ui/settings_row_icons/security.svg",
diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart
index 15184d5..4505a4e 100644
--- a/lib/src/screens/settings/silent_payments_settings.dart
+++ b/lib/src/screens/settings/silent_payments_settings.dart
@@ -22,6 +22,7 @@ class SilentPaymentsSettingsPage extends StatelessWidget {
ModalTopBar(
title: S.current.silent_payments_settings,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
index a9f9927..8a6df6f 100644
--- a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
+++ b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
@@ -144,6 +144,7 @@ class _TOTPEnterCodeState extends State<TOTPEnterCode> {
ModalTopBar(
title: widget.isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
diff --git a/lib/src/screens/support_chat/support_chat_page.dart b/lib/src/screens/support_chat/support_chat_page.dart
index d2a3cae..88079ad 100644
--- a/lib/src/screens/support_chat/support_chat_page.dart
+++ b/lib/src/screens/support_chat/support_chat_page.dart
@@ -22,6 +22,7 @@ class SupportChatPage extends StatelessWidget {
ModalTopBar(
title: S.current.settings_support,
leadingIcon: Icon(Icons.arrow_back_ios_new),
+ leadingSemanticLabel: S.current.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop),
FutureBuilder<String>(
future: getCookie(),
diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart
index 1a2e4f4..b422b80 100644
--- a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart
+++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart
@@ -131,6 +131,7 @@ class WalletConnectConnectionsView extends StatelessWidget {
size: 40,
onPressed: () => Navigator.of(context).maybePop(),
icon: Icon(Icons.arrow_back_ios_new, size: 16),
+ semanticLabel: S.of(context).seed_alert_back,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index 97fac4c..ea39386 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -918,6 +918,7 @@
"return_address_desc": "In the rare case of a failed swap, our partners can automatically refund your payment to the provided refund address.",
"return_address_desc_external": "Since you chose Send From External, please set a refund address manually below.",
"robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.",
+ "rotate_address": "Get new address",
"router_no_route": "No route defined for ${name}",
"salary": "Salary",
"save": "Save",
@@ -1147,10 +1148,13 @@
"swap_id": "Swap ID",
"swap_provider_initial_desc": "Select the type of swap provider you prefer to use when swapping in",
"swap_providers": "Swap providers",
+ "swap_reverse_direction": "Reverse swap direction",
"sweeping_wallet": "Sweeping wallet",
"sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
"swipe_to_bridge": "Swipe to bridge",
"swipe_to_send": "Swipe to send",
+ "switch_camera": "Switch camera",
+ "switch_input_currency": "Switch between crypto and fiat entry",
"switch_wallet": "Switch Wallet",
"switchToETHWallet": "Please switch to an Ethereum wallet and try again",
"switchToWCCompatibleWallet": "Please switch to a WalletConnect compatible wallet and try again (${chains}).",
@@ -1187,6 +1191,7 @@
"tap_to_copy": "Tap to copy",
"template": "Template",
"template_name": "Template Name",
+ "test_node_speeds": "Test node speeds",
"testnet_coins_no_value": "Testnet coins have no value",
"third_intro_content": "Yats live outside of Cake Wallet, too. Any wallet address on earth can be replaced with a Yat!",
"third_intro_title": "Yat plays nicely with others",
@@ -1285,6 +1290,8 @@
"trusted": "Trusted",
"try_again": "Try again",
"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",
+ "turn_flash_off": "Turn flash off",
+ "turn_flash_on": "Turn flash on",
"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.",
"tx_commit_failed": "Transaction commit failed. Please contact support.",
"tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second",
Why this scored 21/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.