a11y: dashboard and navigation semantics (navbar, cards, sync, switchers) (#3468)

* Label new-UI dashboard navigation and sync status for screen readers Each bottom-nav tab becomes one button node carrying its localized name, its selected state and mutual exclusion; the pill text no longer duplicates the selected tab. The assets/history tabs gain selected state, the Bitcoin/Lightning switcher becomes a labeled toggle, and the sync bar becomes a single button whose label is the localized status (plus Tor/MWEB/Silent Payments badges) with a hint pointing at node management. Compact mode's pulsing dot and the chain icon's progress ring now have text equivalents, and the invisible pointer-absorbing strip behind the nav bar is excluded from traversal. * Make new-UI dashboard cards, lists and menus operable by screen readers Collapse the wallet-name row into one labeled button (hardware-wallet glyph folded into the label, inner accounts button no longer a second stop) and give balance cards a labeled, selectable node with a long-press action for show/hide balance. Balances hidden behind AnimatedOpacity, card artwork and decorative glyphs leave the semantics tree; the 3-dots customize target and the card action buttons are now named buttons. Assets/history rows, the history header, the MWEB promo, long-press menu items, the account cards and the add-account and generate-name controls become single button nodes, the phantom action chip in the assets header is built conditionally instead of drawn at opacity 0, and app cards get a close tooltip plus an 'opens externally' hint. * Use expression bodies for the new a11y helpers * Restore untouched expressions in cards_view * Address review: state-aware balance hint, tooltip-free close label * Address review: keep header height without the chip, name the selected index

Seth For Privacy committed Aug 3, 2026 at 13:58 UTC a6bcf7c97ade63437015a86c93ac68ef2e6b86a3
19 files changed +1010 -750
lib/new-ui/new_dashboard.dart
+12 -8
@@ -90,14 +90,18 @@ class _NewDashboardState extends State<NewDashboard> {
90 ),
91 ),
92 ),
93 - SafeArea(
94 - bottom: !(Platform.isIOS),
95 - child: SizedBox(
96 - width: double.infinity,
97 - height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
98 - child: AbsorbPointer(
99 - absorbing: true,
100 - child: Container(color: Colors.transparent),
93 + // Invisible pointer-absorbing strip behind the nav bar: it must not
94 + // be reachable by screen-reader traversal either.
95 + ExcludeSemantics(
96 + child: SafeArea(
97 + bottom: !(Platform.isIOS),
98 + child: SizedBox(
99 + width: double.infinity,
100 + height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
101 + child: AbsorbPointer(
102 + absorbing: true,
103 + child: Container(color: Colors.transparent),
104 + ),
105 ),
106 ),
107 ),
lib/new-ui/pages/account_customizer.dart
+68 -44
@@ -174,17 +174,29 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
174 itemCount: _items.length,
175 itemBuilder: (BuildContext context, int index) {
176 final card = _items[index].card;
177 + // The stack is ordered bottom to top, so the last item
178 + // is the account currently in front — the selected one.
179 + final selectedItemIndex = _items.length - 1;
180
181 return Container(
182 key: ValueKey(index),
180 - child: GestureDetector(
181 - onTap: () {
182 - reorder(index, _items.length);
183 - },
184 - child: Align(
185 - alignment: Alignment.topCenter,
186 - heightFactor: _kStackVisibleFactor,
187 - child: card,
183 + // One labeled, selectable node per account; the card's own
184 + // texts stay reachable underneath it.
185 + child: Semantics(
186 + button: true,
187 + selected: selectedItemIndex == index,
188 + label: _items[index].accountListItem.label,
189 + onTap: () => reorder(index, _items.length),
190 + child: GestureDetector(
191 + excludeFromSemantics: true,
192 + onTap: () {
193 + reorder(index, _items.length);
194 + },
195 + child: Align(
196 + alignment: Alignment.topCenter,
197 + heightFactor: _kStackVisibleFactor,
198 + child: card,
199 + ),
200 ),
201 ),
202 );
@@ -199,31 +211,36 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
211 padding: const EdgeInsets.symmetric(horizontal: 24.0),
212 child: Material(
213 color: Colors.transparent,
202 - child: InkWell(
203 - borderRadius: BorderRadius.circular(999999),
204 - onTap: _showAddAccountModal,
205 - child: Container(
206 - decoration: BoxDecoration(
207 - color: Theme.of(context).colorScheme.surfaceContainer,
208 - borderRadius: BorderRadius.circular(999999)),
209 - child: Padding(
210 - padding: const EdgeInsets.symmetric(vertical: 18.0),
211 - child: Row(
212 - mainAxisAlignment: MainAxisAlignment.center,
213 - spacing: 8,
214 - children: [
215 - Icon(
216 - Icons.add,
217 - size: 28,
218 - color: Theme.of(context).colorScheme.primary,
219 - ),
220 - Text(
221 - S.of(context).add_account,
222 - style: TextStyle(
214 + child: MergeSemantics(
215 + child: Semantics(
216 + button: true,
217 + child: InkWell(
218 + borderRadius: BorderRadius.circular(999999),
219 + onTap: _showAddAccountModal,
220 + child: Container(
221 + decoration: BoxDecoration(
222 + color: Theme.of(context).colorScheme.surfaceContainer,
223 + borderRadius: BorderRadius.circular(999999)),
224 + child: Padding(
225 + padding: const EdgeInsets.symmetric(vertical: 18.0),
226 + child: Row(
227 + mainAxisAlignment: MainAxisAlignment.center,
228 + spacing: 8,
229 + children: [
230 + Icon(
231 + Icons.add,
232 + size: 28,
233 color: Theme.of(context).colorScheme.primary,
224 - fontWeight: FontWeight.w500),
225 - )
226 - ],
234 + ),
235 + Text(
236 + S.of(context).add_account,
237 + style: TextStyle(
238 + color: Theme.of(context).colorScheme.primary,
239 + fontWeight: FontWeight.w500),
240 + )
241 + ],
242 + ),
243 + ),
244 ),
245 ),
246 ),
@@ -406,6 +423,8 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
423 final TextEditingController _controller = TextEditingController();
424 bool _loading = false;
425
426 + Future<void> _generateAccountName() async => _controller.text = await generateName();
427 +
428 @override
429 Widget build(BuildContext context) {
430 return Container(
@@ -441,18 +460,23 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
460 ),
461 Padding(
462 padding: const EdgeInsets.all(12.0),
444 - child: GestureDetector(
445 - onTap: () async {
446 - _controller.text = await generateName();
447 - },
448 - child: Container(
449 - decoration: BoxDecoration(
450 - color: Theme.of(context).colorScheme.surfaceContainerHigh,
451 - borderRadius: BorderRadius.circular(5)),
452 - child: CakeImageWidget(
453 - imageUrl: "assets/new-ui/randomize.svg",
454 - colorFilter: ColorFilter.mode(
455 - Theme.of(context).colorScheme.primary, BlendMode.srcIn),
463 + child: Semantics(
464 + button: true,
465 + label: S.of(context).generate_name,
466 + onTap: _generateAccountName,
467 + child: ExcludeSemantics(
468 + child: GestureDetector(
469 + onTap: _generateAccountName,
470 + child: Container(
471 + decoration: BoxDecoration(
472 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
473 + borderRadius: BorderRadius.circular(5)),
474 + child: CakeImageWidget(
475 + imageUrl: "assets/new-ui/randomize.svg",
476 + colorFilter: ColorFilter.mode(
477 + Theme.of(context).colorScheme.primary, BlendMode.srcIn),
478 + ),
479 + ),
480 ),
481 ),
482 ),
lib/new-ui/widgets/apps_widget.dart
+79 -57
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:cake_wallet/themes/core/theme_extension.dart';
4 import 'package:flutter/material.dart';
@@ -60,71 +61,86 @@ class AppsWidget extends StatelessWidget {
61 end: Alignment.bottomCenter,
62 ),
63 ),
63 - child: TextButton(
64 - onPressed: onTap,
65 - style: TextButton.styleFrom(
66 - shape: RoundedSuperellipseBorder(
67 - borderRadius: BorderRadius.circular(18),
68 - side: BorderSide(
69 - width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
70 - ),
71 - padding: EdgeInsets.all(24),
72 - ),
73 - child: Column(
74 - children: [
75 - Row(
64 + // The card is one node; the arrow glyph's "leaves the app" meaning
65 + // becomes a hint instead of an unlabeled icon.
66 + child: MergeSemantics(
67 + child: Semantics(
68 + hint: isLink == true ? S.of(context).opens_externally : null,
69 + child: TextButton(
70 + onPressed: onTap,
71 + style: TextButton.styleFrom(
72 + shape: RoundedSuperellipseBorder(
73 + borderRadius: BorderRadius.circular(18),
74 + side: BorderSide(
75 + width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
76 + ),
77 + padding: EdgeInsets.all(24),
78 + ),
79 + child: Column(
80 children: [
77 - Padding(
78 - padding: const EdgeInsets.only(right: 20),
79 - child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
80 - ),
81 - Expanded(
82 - child: Column(
83 - crossAxisAlignment: CrossAxisAlignment.start,
84 - children: [
85 - Row(
86 - spacing: 6.0,
81 + Row(
82 + children: [
83 + Padding(
84 + padding: const EdgeInsets.only(right: 20),
85 + child: ExcludeSemantics(
86 + child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
87 + ),
88 + ),
89 + Expanded(
90 + child: Column(
91 + crossAxisAlignment: CrossAxisAlignment.start,
92 children: [
93 + Row(
94 + spacing: 6.0,
95 + children: [
96 + Text(
97 + title,
98 + style: Theme.of(context).textTheme.titleMedium?.copyWith(
99 + color: Theme.of(context).colorScheme.onSurface,
100 + fontWeight: FontWeight.w800,
101 + fontSize: 20,
102 + ),
103 + softWrap: true,
104 + ),
105 + isCake == true
106 + ? ExcludeSemantics(
107 + child: CakeImageWidget(
108 + imageUrl: "assets/new-ui/cakelabs-icon.svg",
109 + color: Theme.of(context)
110 + .colorScheme
111 + .onSurfaceVariant),
112 + )
113 + : SizedBox(),
114 + ],
115 + ),
116 + SizedBox(height: 5),
117 Text(
89 - title,
90 - style: Theme.of(context).textTheme.titleMedium?.copyWith(
91 - color: Theme.of(context).colorScheme.onSurface,
92 - fontWeight: FontWeight.w800,
93 - fontSize: 20,
118 + subTitle,
119 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
120 + color: Theme.of(context).colorScheme.onSurfaceVariant,
121 + fontWeight: FontWeight.w500,
122 ),
123 softWrap: true,
124 ),
97 - isCake == true
98 - ? CakeImageWidget(
99 - imageUrl: "assets/new-ui/cakelabs-icon.svg",
100 - color: Theme.of(context).colorScheme.onSurfaceVariant)
101 - : SizedBox(),
125 ],
126 ),
104 - SizedBox(height: 5),
105 - Text(
106 - subTitle,
107 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
108 - color: Theme.of(context).colorScheme.onSurfaceVariant,
109 - fontWeight: FontWeight.w500,
110 - ),
111 - softWrap: true,
127 + ),
128 + ExcludeSemantics(
129 + child: Icon(
130 + isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
131 + color: Theme.of(context).colorScheme.onSurfaceVariant,
132 + size: 20,
133 ),
113 - ],
114 - ),
134 + )
135 + ],
136 ),
116 - Icon(
117 - isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
118 - color: Theme.of(context).colorScheme.onSurfaceVariant,
119 - size: 20,
120 - )
137 + if (hint != null) ...[
138 + SizedBox(height: 10),
139 + hint!,
140 + ]
141 ],
142 ),
123 - if (hint != null) ...[
124 - SizedBox(height: 10),
125 - hint!,
126 - ]
127 - ],
143 + ),
144 ),
145 ),
146 ),
@@ -132,10 +148,16 @@ class AppsWidget extends StatelessWidget {
148 Positioned(
149 top: 10,
150 right: 10,
135 - child: IconButton(
136 - icon: Icon(Icons.close),
137 - onPressed: onClose,
138 - //color: Theme.of(context).colorScheme.onSurface,
151 + // Label the icon-only button without adding a visible tooltip.
152 + child: MergeSemantics(
153 + child: Semantics(
154 + label: S.of(context).close,
155 + child: IconButton(
156 + icon: Icon(Icons.close),
157 + onPressed: onClose,
158 + //color: Theme.of(context).colorScheme.onSurface,
159 + ),
160 + ),
161 ),
162 ),
163 ],
lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart
+123 -110
@@ -39,125 +39,138 @@ class AssetTile extends StatelessWidget {
39 Widget build(BuildContext context) {
40 final iconPath = balance.asset.iconPath ?? "";
41
42 - return GestureDetector(
43 - onTap: () {
44 - showModalBottomSheet(
45 - context: context,
46 - isScrollControlled: true,
47 - builder: (context) {
48 - return AssetDetailsModal(
49 - showSwap: showSwap,
50 - showBridgeButton: showBridgeButton,
51 - asset: balance.asset,
52 - title: title ?? balance.asset.fullName ?? balance.asset.name,
53 - chainTitle: "",
54 - subtitle: trailingText ?? _getChainTitle(),
55 - amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
56 - currencyTitle: balance.asset.title,
57 - fiatAmount: showSecondary
58 - ? balance.fiatSecondAvailableBalance
59 - : balance.fiatAvailableBalance,
60 - iconPath: balance.asset.iconPath ?? "",
61 - chainIconPath: chainIconPath,
62 - mode: modalMode,
63 - wallet: wallet,
64 - );
65 - });
66 - },
67 - child: Padding(
68 - padding: const EdgeInsets.symmetric(horizontal: 18.0),
69 - child: Container(
70 - width: double.infinity,
71 - height: 72,
72 - decoration: BoxDecoration(
73 - color: Theme.of(context).colorScheme.surfaceContainer,
74 - borderRadius: BorderRadius.vertical(
75 - top: isFirst ? Radius.circular(18) : Radius.zero,
76 - bottom: isLast ? Radius.circular(18) : Radius.zero,
77 - ),
78 - ),
42 + // The row is one control: name, amount and fiat value merge into a single
43 + // button node that opens the asset details sheet.
44 + return MergeSemantics(
45 + child: Semantics(
46 + button: true,
47 + child: GestureDetector(
48 + onTap: () {
49 + showModalBottomSheet(
50 + context: context,
51 + isScrollControlled: true,
52 + builder: (context) {
53 + return AssetDetailsModal(
54 + showSwap: showSwap,
55 + showBridgeButton: showBridgeButton,
56 + asset: balance.asset,
57 + title: title ?? balance.asset.fullName ?? balance.asset.name,
58 + chainTitle: "",
59 + subtitle: trailingText ?? _getChainTitle(),
60 + amount:
61 + showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
62 + currencyTitle: balance.asset.title,
63 + fiatAmount: showSecondary
64 + ? balance.fiatSecondAvailableBalance
65 + : balance.fiatAvailableBalance,
66 + iconPath: balance.asset.iconPath ?? "",
67 + chainIconPath: chainIconPath,
68 + mode: modalMode,
69 + wallet: wallet,
70 + );
71 + });
72 + },
73 child: Padding(
80 - padding: const EdgeInsets.symmetric(horizontal: 12.0),
81 - child: Row(
82 - mainAxisSize: MainAxisSize.max,
83 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
84 - children: [
85 - Expanded(
86 - child: Row(
87 - mainAxisSize: MainAxisSize.min,
88 - children: [
89 - iconPath.isNotEmpty
90 - ? TokenImageWidget(
91 - imageUrl: iconPath,
92 - size: 36,
93 - )
94 - : Container(
95 - width: 36,
96 - height: 36,
97 - decoration: BoxDecoration(
98 - color: Theme.of(context).colorScheme.primary,
99 - shape: BoxShape.circle,
100 - ),
101 - child: Center(
102 - child: Text(
103 - balance.asset.name
104 - .substring(0, min(2, balance.asset.name.length)),
105 - style: TextStyle(
106 - fontSize: 20,
107 - color: Theme.of(context).colorScheme.onPrimary,
74 + padding: const EdgeInsets.symmetric(horizontal: 18.0),
75 + child: Container(
76 + width: double.infinity,
77 + height: 72,
78 + decoration: BoxDecoration(
79 + color: Theme.of(context).colorScheme.surfaceContainer,
80 + borderRadius: BorderRadius.vertical(
81 + top: isFirst ? Radius.circular(18) : Radius.zero,
82 + bottom: isLast ? Radius.circular(18) : Radius.zero,
83 + ),
84 + ),
85 + child: Padding(
86 + padding: const EdgeInsets.symmetric(horizontal: 12.0),
87 + child: Row(
88 + mainAxisSize: MainAxisSize.max,
89 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
90 + children: [
91 + Expanded(
92 + child: Row(
93 + mainAxisSize: MainAxisSize.min,
94 + children: [
95 + // Decorative: the asset name is already in the row text.
96 + ExcludeSemantics(
97 + child: iconPath.isNotEmpty
98 + ? TokenImageWidget(
99 + imageUrl: iconPath,
100 + size: 36,
101 + )
102 + : Container(
103 + width: 36,
104 + height: 36,
105 + decoration: BoxDecoration(
106 + color: Theme.of(context).colorScheme.primary,
107 + shape: BoxShape.circle,
108 + ),
109 + child: Center(
110 + child: Text(
111 + balance.asset.name
112 + .substring(0, min(2, balance.asset.name.length)),
113 + style: TextStyle(
114 + fontSize: 20,
115 + color: Theme.of(context).colorScheme.onPrimary,
116 + ),
117 + ),
118 + ),
119 ),
109 - ),
110 - ),
111 - ),
112 - SizedBox(width: 12.0),
113 - Expanded(
114 - child: Column(
115 - spacing: 4.0,
116 - mainAxisAlignment: MainAxisAlignment.center,
117 - crossAxisAlignment: CrossAxisAlignment.start,
118 - children: [
119 - Row(
120 - spacing: 4,
120 + ),
121 + SizedBox(width: 12.0),
122 + Expanded(
123 + child: Column(
124 + spacing: 4.0,
125 + mainAxisAlignment: MainAxisAlignment.center,
126 + crossAxisAlignment: CrossAxisAlignment.start,
127 children: [
122 - Text(
123 - title ?? balance.asset.fullName ?? balance.asset.name,
124 - style: TextStyle(fontWeight: FontWeight.w500),
128 + Row(
129 + spacing: 4,
130 + children: [
131 + Text(
132 + title ?? balance.asset.fullName ?? balance.asset.name,
133 + style: TextStyle(fontWeight: FontWeight.w500),
134 + ),
135 + if (trailingText != null)
136 + Text(
137 + trailingText!,
138 + style: TextStyle(
139 + color: Theme.of(context).colorScheme.onSurfaceVariant),
140 + ),
141 + ],
142 ),
126 - if (trailingText != null)
127 - Text(
128 - trailingText!,
129 - style: TextStyle(
130 - color: Theme.of(context).colorScheme.onSurfaceVariant),
131 - ),
132 - ],
133 - ),
134 - Padding(
135 - padding: const EdgeInsets.only(right: 4.0),
136 - child: FittedBox(
137 - fit: BoxFit.scaleDown,
138 - alignment: Alignment.centerLeft,
139 - child: Text(
140 - "${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
141 - maxLines: 1,
142 - style: TextStyle(
143 - color: Theme.of(context).colorScheme.onSurfaceVariant,
143 + Padding(
144 + padding: const EdgeInsets.only(right: 4.0),
145 + child: FittedBox(
146 + fit: BoxFit.scaleDown,
147 + alignment: Alignment.centerLeft,
148 + child: Text(
149 + "${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
150 + maxLines: 1,
151 + style: TextStyle(
152 + color: Theme.of(context).colorScheme.onSurfaceVariant,
153 + ),
154 + ),
155 ),
156 ),
146 - ),
157 + ],
158 ),
148 - ],
149 - ),
159 + ),
160 + ],
161 ),
151 - ],
152 - ),
153 - ),
154 - Text(
155 - showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance,
156 - style: TextStyle(
157 - color: Theme.of(context).colorScheme.onSurface,
158 - ),
162 + ),
163 + Text(
164 + showSecondary
165 + ? balance.fiatSecondAvailableBalance
166 + : balance.fiatAvailableBalance,
167 + style: TextStyle(
168 + color: Theme.of(context).colorScheme.onSurface,
169 + ),
170 + ),
171 + ],
172 ),
160 - ],
173 + ),
174 ),
175 ),
176 ),
lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart
+41 -30
@@ -22,6 +22,8 @@ class AssetsTopBar extends StatelessWidget {
22
23 @override
24 Widget build(BuildContext context) {
25 + final actionButton = tabs[selectedTab].actionButton;
26 +
27 return SliverToBoxAdapter(
28 child: Padding(
29 padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
@@ -49,40 +51,49 @@ class AssetsTopBar extends StatelessWidget {
51 key: ValueKey(selectedTab),
52 spacing: 8,
53 children: [
52 - Opacity(
53 - opacity: tabs[selectedTab].actionButton != null ? 1 : 0,
54 - child: GestureDetector(
55 - onTap: () {
56 - if (tabs[selectedTab].actionButton != null) {
57 - tabs[selectedTab].actionButton?.onPressed();
58 - }
59 - },
60 - child: Container(
61 - height: 40,
62 - decoration: BoxDecoration(
63 - borderRadius: BorderRadius.circular(999999),
64 - color: Theme.of(context).colorScheme.surfaceContainer,
65 - ),
66 - child: Padding(
67 - padding: const EdgeInsets.symmetric(horizontal: 12.0),
68 - child: Row(
69 - spacing: 6,
70 - children: [
71 - if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty)
72 - Text(
73 - tabs[selectedTab].actionButton?.title ?? "",
74 - style: TextStyle(color: Theme.of(context).colorScheme.primary),
75 - ),
76 - CakeImageWidget(
77 - imageUrl: tabs[selectedTab].actionButton?.iconPath,
78 - colorFilter: ColorFilter.mode(
79 - Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
80 - ],
54 + // Built conditionally rather than rendered at opacity 0: an
55 + // invisible chip stayed focusable for screen readers. The
56 + // SizedBox keeps the 40px header height the invisible chip
57 + // used to occupy.
58 + if (actionButton == null)
59 + const SizedBox(height: 40)
60 + else
61 + MergeSemantics(
62 + child: Semantics(
63 + button: true,
64 + child: GestureDetector(
65 + onTap: actionButton.onPressed,
66 + child: Container(
67 + height: 40,
68 + decoration: BoxDecoration(
69 + borderRadius: BorderRadius.circular(999999),
70 + color: Theme.of(context).colorScheme.surfaceContainer,
71 + ),
72 + child: Padding(
73 + padding: const EdgeInsets.symmetric(horizontal: 12.0),
74 + child: Row(
75 + spacing: 6,
76 + children: [
77 + if (actionButton.title.isNotEmpty)
78 + Text(
79 + actionButton.title,
80 + style:
81 + TextStyle(color: Theme.of(context).colorScheme.primary),
82 + ),
83 + ExcludeSemantics(
84 + child: CakeImageWidget(
85 + imageUrl: actionButton.iconPath,
86 + colorFilter: ColorFilter.mode(
87 + Theme.of(context).colorScheme.primary,
88 + BlendMode.srcIn)),
89 + ),
90 + ],
91 + ),
92 + ),
93 ),
94 ),
95 ),
96 ),
85 - ),
97 ],
98 ),
99 ),
lib/new-ui/widgets/coins_page/assets_history/history_section.dart
+14 -5
@@ -36,6 +36,15 @@ class HistorySection extends StatelessWidget {
36 final bool roundedTopSection;
37 final bool detailsAsPage;
38
39 + /// A history row is a single button node: every text inside it (direction,
40 + /// date, amounts) merges into one label.
41 + Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics(
42 + child: Semantics(
43 + button: true,
44 + child: GestureDetector(onTap: onTap, child: child),
45 + ),
46 + );
47 +
48 @override
49 Widget build(BuildContext context) {
50 return SliverPadding(
@@ -90,7 +99,7 @@ class HistorySection extends StatelessWidget {
99 else
100 asset = item.assetOfTransaction;
101
93 - return GestureDetector(
102 + return _historyRow(
103 onTap: () {
104 final page =
105 getIt.get<TransactionDetailsModal>(param1: transaction);
@@ -125,7 +134,7 @@ class HistorySection extends StatelessWidget {
134 final tradeFrom = trade.from;
135 final tradeTo = trade.to;
136
128 - return GestureDetector(
137 + return _historyRow(
138 onTap: () => Navigator.of(context)
139 .pushNamed(Routes.tradeDetails, arguments: trade),
140 child: HistoryTradeTile(
@@ -161,7 +170,7 @@ class HistorySection extends StatelessWidget {
170 style: TextStyle(
171 color: Theme.of(context).colorScheme.onSurfaceVariant)));
172 } else if (item is OrderListItem) {
164 - return GestureDetector(
173 + return _historyRow(
174 onTap: () => Navigator.of(context)
175 .pushNamed(Routes.orderDetails, arguments: item.order),
176 child: HistoryOrderTile(
@@ -176,7 +185,7 @@ class HistorySection extends StatelessWidget {
185 } else if (item is PayjoinTransactionListItem) {
186 final session = item.session;
187
179 - return GestureDetector(
188 + return _historyRow(
189 onTap: () => Navigator.of(context).pushNamed(
190 Routes.payjoinDetails,
191 arguments: [item.sessionId, item.transaction],
@@ -196,7 +205,7 @@ class HistorySection extends StatelessWidget {
205 } else if (item is AnonpayTransactionListItem) {
206 final transactionInfo = item.transaction;
207
199 - return GestureDetector(
208 + return _historyRow(
209 onTap: () => Navigator.of(context).pushNamed(
210 Routes.anonPayDetailsPage,
211 arguments: transactionInfo),
lib/new-ui/widgets/coins_page/assets_history/history_tile.dart
+2 -1
@@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget {
142 date: date,
143 amount: amount,
144 amountFiat: amountFiat,
145 - leadingIcon: _getLeadingIcon(context),
145 + // Decorative: `title` already reads out sent/received/pending.
146 + leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)),
147 primaryTextColor: _getPrimaryTextColor(),
148 roundedTop: roundedTop,
149 roundedBottom: roundedBottom,
lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart
+42 -35
@@ -11,46 +11,53 @@ class HistoryTopBar extends StatelessWidget {
11 @override
12 Widget build(BuildContext context) {
13 return SliverToBoxAdapter(
14 - child: GestureDetector(
14 + child: Semantics(
15 + button: true,
16 + label: S.of(context).history,
17 onTap: onTap,
16 - behavior: HitTestBehavior.opaque,
17 - child: Padding(
18 - padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
19 - child: Container(
20 - decoration: BoxDecoration(
21 - borderRadius: BorderRadius.vertical(
22 - top: Radius.circular(18),
23 - bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
24 - color: Theme.of(context).colorScheme.surfaceContainer),
18 + child: ExcludeSemantics(
19 + child: GestureDetector(
20 + onTap: onTap,
21 + behavior: HitTestBehavior.opaque,
22 child: Padding(
26 - padding: EdgeInsets.symmetric(
27 - vertical: 4,
28 - horizontal: 12,
29 - ),
30 - child: Column(
31 - spacing: 12,
32 - children: [
33 - SizedBox.shrink(),
34 - Row(
35 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
23 + padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
24 + child: Container(
25 + decoration: BoxDecoration(
26 + borderRadius: BorderRadius.vertical(
27 + top: Radius.circular(18),
28 + bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
29 + color: Theme.of(context).colorScheme.surfaceContainer),
30 + child: Padding(
31 + padding: EdgeInsets.symmetric(
32 + vertical: 4,
33 + horizontal: 12,
34 + ),
35 + child: Column(
36 + spacing: 12,
37 children: [
37 - Text(S.of(context).history),
38 - CakeImageWidget(
39 - imageUrl: "assets/new-ui/arrow_right.svg",
40 - colorFilter: ColorFilter.mode(
41 - Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
42 - )
38 + SizedBox.shrink(),
39 + Row(
40 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
41 + children: [
42 + Text(S.of(context).history),
43 + CakeImageWidget(
44 + imageUrl: "assets/new-ui/arrow_right.svg",
45 + colorFilter: ColorFilter.mode(
46 + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
47 + )
48 + ],
49 + ),
50 + if (!roundedBottom)
51 + Container(
52 + height: 1,
53 + width: double.infinity,
54 + color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
55 + )
56 + else
57 + Container(height: 2)
58 ],
59 ),
45 - if (!roundedBottom)
46 - Container(
47 - height: 1,
48 - width: double.infinity,
49 - color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
50 - )
51 - else
52 - Container(height: 2)
53 - ],
60 + ),
61 ),
62 ),
63 ),
lib/new-ui/widgets/coins_page/cards/balance_card.dart
+106 -73
@@ -91,11 +91,14 @@ class BalanceCard extends StatelessWidget {
91 ? ClipRSuperellipse(
92 borderRadius: BorderRadius.circular(borderRadius),
93 key: ValueKey(design.imagePath),
94 - child: CakeImageWidget(
95 - imageUrl: design.imagePath,
96 - width: width,
97 - height: height,
98 - fit: BoxFit.fill,
94 + // Purely decorative card artwork.
95 + child: ExcludeSemantics(
96 + child: CakeImageWidget(
97 + imageUrl: design.imagePath,
98 + width: width,
99 + height: height,
100 + fit: BoxFit.fill,
101 + ),
102 ),
103 )
104 : const SizedBox.shrink(
@@ -146,9 +149,14 @@ class BalanceCard extends StatelessWidget {
149 AnimatedOpacity(
150 opacity: selected ? 0 : 1,
151 duration: textFadeDuration,
149 - child: Text(
150 - accountBalance,
151 - style: TextStyle(color: design.colors.textColor, fontSize: 14),
152 + // Opacity alone keeps the text readable by screen
153 + // readers, so drop it while it is invisible.
154 + child: ExcludeSemantics(
155 + excluding: selected,
156 + child: Text(
157 + accountBalance,
158 + style: TextStyle(color: design.colors.textColor, fontSize: 14),
159 + ),
160 ),
161 ),
162 ],
@@ -156,40 +164,45 @@ class BalanceCard extends StatelessWidget {
164 AnimatedOpacity(
165 opacity: selected ? 1 : 0,
166 duration: textFadeDuration,
159 - child: AnimatedSwitcher(
160 - duration: designSwitchDuration,
161 - layoutBuilder: (currentChild, previousChildren) {
162 - return Stack(
163 - alignment: Alignment.centerLeft,
164 - children: <Widget>[
165 - ...previousChildren,
166 - if (currentChild != null) currentChild,
167 + // Only the selected card's balance is visible, so only it
168 + // may be announced.
169 + child: ExcludeSemantics(
170 + excluding: !selected,
171 + child: AnimatedSwitcher(
172 + duration: designSwitchDuration,
173 + layoutBuilder: (currentChild, previousChildren) {
174 + return Stack(
175 + alignment: Alignment.centerLeft,
176 + children: <Widget>[
177 + ...previousChildren,
178 + if (currentChild != null) currentChild,
179 + ],
180 + );
181 + },
182 + child: Row(
183 + key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
184 + spacing: 8.0,
185 + children: [
186 + AnimatedDefaultTextStyle(
187 + duration: designSwitchDuration,
188 + style: DefaultTextStyle.of(context).style.copyWith(
189 + color: design.colors.textColor,
190 + fontSize: 28,
191 + fontWeight: FontWeight.w500,
192 + letterSpacing: -0.4),
193 + child: Text(fiatFirst ? fiatBalance : balance),
194 + ),
195 + AnimatedDefaultTextStyle(
196 + duration: designSwitchDuration,
197 + style: DefaultTextStyle.of(context).style.copyWith(
198 + color: design.colors.textColorSecondary,
199 + fontSize: 28,
200 + fontWeight: FontWeight.w400,
201 + letterSpacing: -0.4),
202 + child: Text(resolvedAssetName),
203 + ),
204 ],
168 - );
169 - },
170 - child: Row(
171 - key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
172 - spacing: 8.0,
173 - children: [
174 - AnimatedDefaultTextStyle(
175 - duration: designSwitchDuration,
176 - style: DefaultTextStyle.of(context).style.copyWith(
177 - color: design.colors.textColor,
178 - fontSize: 28,
179 - fontWeight: FontWeight.w500,
180 - letterSpacing: -0.4),
181 - child: Text(fiatFirst ? fiatBalance : balance),
182 - ),
183 - AnimatedDefaultTextStyle(
184 - duration: designSwitchDuration,
185 - style: DefaultTextStyle.of(context).style.copyWith(
186 - color: design.colors.textColorSecondary,
187 - fontSize: 28,
188 - fontWeight: FontWeight.w400,
189 - letterSpacing: -0.4),
190 - child: Text(resolvedAssetName),
191 - ),
192 - ],
205 + ),
206 ),
207 ),
208 ),
@@ -250,7 +263,10 @@ class BalanceCard extends StatelessWidget {
263 switchInCurve: Curves.easeInOut,
264 switchOutCurve: Curves.easeInOut,
265 child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
253 - ? _CornerSvgIcon(design: design, iconWidth: iconWidth)
266 + // Purely decorative card artwork.
267 + ? ExcludeSemantics(
268 + child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
269 + )
270 : const SizedBox.shrink(
271 key: ValueKey('svgIconOff'),
272 ),
@@ -266,18 +282,28 @@ class BalanceCard extends StatelessWidget {
282 child: AnimatedOpacity(
283 duration: designSwitchDuration,
284 opacity: onCustomizeTapped == null ? 0 : 1,
269 - child: GestureDetector(
270 - behavior: HitTestBehavior.opaque,
271 - onTap: onCustomizeTapped,
272 - child: Container(
273 - height: 40,
274 - width: 40,
275 - child: Center(
276 - child: CakeImageWidget(
277 - imageUrl: "assets/new-ui/3dots_vertical.svg",
278 - alignment: Alignment.topRight,
279 - colorFilter:
280 - ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
285 + // Faded out and inert: must not be a focusable phantom control.
286 + child: ExcludeSemantics(
287 + excluding: onCustomizeTapped == null,
288 + child: Semantics(
289 + button: true,
290 + label: S.of(context).wallet_menu,
291 + onTap: onCustomizeTapped,
292 + child: GestureDetector(
293 + excludeFromSemantics: true,
294 + behavior: HitTestBehavior.opaque,
295 + onTap: onCustomizeTapped,
296 + child: Container(
297 + height: 40,
298 + width: 40,
299 + child: Center(
300 + child: CakeImageWidget(
301 + imageUrl: "assets/new-ui/3dots_vertical.svg",
302 + alignment: Alignment.topRight,
303 + colorFilter:
304 + ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
305 + ),
306 + ),
307 ),
308 ),
309 ),
@@ -289,27 +315,34 @@ class BalanceCard extends StatelessWidget {
315 );
316 }
317
292 - Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector(
318 + Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics(
319 + button: true,
320 + label: action.label,
321 onTap: action.onTap,
294 - child: Container(
295 - decoration: BoxDecoration(
296 - color: design.colors.backgroundImageColor.withAlpha(75),
297 - borderRadius: BorderRadius.circular(10000000),
298 - ),
299 - margin: const EdgeInsets.only(right: 10),
300 - padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
301 - child: Row(
302 - mainAxisSize: MainAxisSize.min,
303 - children: [
304 - Padding(
305 - padding: const EdgeInsets.only(right: 6),
306 - child: Text(
307 - action.label,
308 - style: TextStyle(color: design.colors.textColor, fontSize: 16),
309 - ),
322 + child: ExcludeSemantics(
323 + child: GestureDetector(
324 + onTap: action.onTap,
325 + child: Container(
326 + decoration: BoxDecoration(
327 + color: design.colors.backgroundImageColor.withAlpha(75),
328 + borderRadius: BorderRadius.circular(10000000),
329 ),
311 - Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
312 - ],
330 + margin: const EdgeInsets.only(right: 10),
331 + padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
332 + child: Row(
333 + mainAxisSize: MainAxisSize.min,
334 + children: [
335 + Padding(
336 + padding: const EdgeInsets.only(right: 6),
337 + child: Text(
338 + action.label,
339 + style: TextStyle(color: design.colors.textColor, fontSize: 16),
340 + ),
341 + ),
342 + Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
343 + ],
344 + ),
345 + ),
346 ),
347 ),
348 );
lib/new-ui/widgets/coins_page/cards/cards_view.dart
+140 -116
@@ -76,6 +76,32 @@ class _CardsViewState extends State<CardsView> {
76
77 final left = (parentWidth - effectiveCardWidth) / 2.0;
78
79 + final isSelected = _selectedIndex == visualIndex;
80 + final accounts = widget.accountListViewModel?.accounts;
81 + final cardLabel = (accounts != null && realIndex < accounts.length)
82 + ? accounts[realIndex].label
83 + : S.of(context).balance;
84 +
85 + void onCardTap() {
86 + // printV(visualIndex);
87 + if (compactMode && visualIndex != 0) {
88 + widget.onCompactModeBackgroundCardsTapped();
89 + } else if (!compactMode) {
90 + setState(() {
91 + if (widget.accountListViewModel != null)
92 + widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
93 + _selectedIndex = visualIndex;
94 + });
95 + }
96 + }
97 +
98 + void onCardLongPress() {
99 + if (_selectedIndex == visualIndex) {
100 + widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
101 + }
102 + HapticFeedback.heavyImpact();
103 + }
104 +
105 return AnimatedPositioned(
106 key: ValueKey("$visualIndex $realIndex"),
107 duration: animDuration,
@@ -86,124 +112,122 @@ class _CardsViewState extends State<CardsView> {
112 duration: animDuration,
113 curve: Curves.easeOut,
114 scale: scale,
89 - child: GestureDetector(
90 - onTap: () {
91 - // printV(visualIndex);
92 - if (compactMode && visualIndex != 0) {
93 - widget.onCompactModeBackgroundCardsTapped();
94 - } else if (!compactMode) {
95 - setState(() {
96 - if (widget.accountListViewModel != null)
97 - widget.accountListViewModel!
98 - .select(widget.accountListViewModel!.accounts[realIndex]);
99 - _selectedIndex = visualIndex;
100 - });
101 - }
102 - },
103 - onLongPress: () {
104 - if (_selectedIndex == visualIndex) {
105 - widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
106 - }
107 - ;
108 - HapticFeedback.heavyImpact();
109 - },
110 - child: Observer(builder: (_) {
111 - if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
112 - return Container();
113 - }
114 - final account = widget.accountListViewModel?.accounts[realIndex];
115 -
116 - // The second balance should always be the lightning balance
117 - // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
118 - final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
119 - .getMainBalanceRecord(widget.lightningMode);
120 -
121 - late final String walletBalance;
122 - late final String walletFiatBalance;
123 - if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
124 - if (widget.dashboardViewModel.balanceViewModel.displayMode ==
125 - BalanceDisplayMode.hiddenBalance) {
126 - walletBalance = '●●●●●●';
127 - walletFiatBalance = '●●●●●●';
115 + // The card is the tap target; the balances and the card's own buttons stay
116 + // reachable as children of this node.
117 + child: Semantics(
118 + button: true,
119 + selected: isSelected,
120 + label: cardLabel,
121 + hint: isSelected
122 + ? (widget.dashboardViewModel.balanceViewModel.displayMode ==
123 + BalanceDisplayMode.hiddenBalance
124 + ? S.of(context).long_press_show_balance
125 + : S.of(context).long_press_hide_balance)
126 + : null,
127 + onTap: onCardTap,
128 + onLongPress: isSelected ? onCardLongPress : null,
129 + child: GestureDetector(
130 + excludeFromSemantics: true,
131 + onTap: onCardTap,
132 + onLongPress: onCardLongPress,
133 + child: Observer(builder: (_) {
134 + if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
135 + return Container();
136 + }
137 + final account = widget.accountListViewModel?.accounts[realIndex];
138 +
139 + // The second balance should always be the lightning balance
140 + // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
141 + final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
142 + .getMainBalanceRecord(widget.lightningMode);
143 +
144 + late final String walletBalance;
145 + late final String walletFiatBalance;
146 + if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
147 + if (widget.dashboardViewModel.balanceViewModel.displayMode ==
148 + BalanceDisplayMode.hiddenBalance) {
149 + walletBalance = '●●●●●●';
150 + walletFiatBalance = '●●●●●●';
151 + } else {
152 + walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
153 + walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
154 + }
155 + } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
156 + walletBalance = "";
157 + walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
158 } else {
129 - walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
130 - walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
159 + walletBalance = walletBalanceRecord?.availableBalance ?? "0";
160 + walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
161 }
132 - } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
133 - walletBalance = "";
134 - walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
135 - } else {
136 - walletBalance = walletBalanceRecord?.availableBalance ?? "0";
137 - walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
138 - }
139 -
140 - // the card designs is empty if widget gets built before it loads.
141 - // should get populated before user sees anything
142 - final CardDesign cardDesign;
143 - if (widget.dashboardViewModel.cardDesigns.isEmpty ||
144 - realIndex >= widget.dashboardViewModel.cardDesigns.length)
145 - cardDesign = CardDesign.genericDefault;
146 - else if (widget.lightningMode)
147 - cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
148 - else
149 - cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
150 -
151 - final String accountName;
152 - final String accountBalance;
153 - if (account == null) {
154 - accountName = "";
155 - accountBalance = "";
156 - } else {
157 - accountName = account.label;
158 - accountBalance = account.balance ?? "0.00";
159 - }
160 -
161 - final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
162 - ? ""
163 - : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
164 -
165 - final List<BalanceCardAction> actions = widget.lightningMode
166 - ? [
167 - BalanceCardAction(
168 - label: S.current.bitcoin_lightning_deposit,
169 - icon: Icons.arrow_downward,
170 - onTap: depositToL2,
171 - ),
172 - BalanceCardAction(
173 - label: S.current.bitcoin_lightning_withdraw,
174 - icon: Icons.arrow_upward,
175 - onTap: withdrawFromL2,
176 - )
177 - ]
178 - : widget.dashboardViewModel.isEnabledTradeAction
179 - ? [
180 - BalanceCardAction(
181 - label: S.current.buy,
182 - icon: Icons.arrow_forward_ios_rounded,
183 - iconSize: 12,
184 - onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
185 - )
186 - ]
187 - : [];
188 -
189 - return BalanceCard(
190 - width: effectiveCardWidth,
191 - accountName: accountName,
192 - accountBalance: accountBalance,
193 - designSwitchDuration: Duration(milliseconds: 150),
194 - assetName: assetName,
195 - capitalizeAssetName: _shouldCapitalizeAssetName(),
196 - balance: walletBalance,
197 - fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
198 - widget.dashboardViewModel.settingsStore.fiatCurrency.title,
199 - fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
200 - fiatBalance: walletFiatBalance,
201 - selected: _selectedIndex == visualIndex,
202 - onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
203 - design: cardDesign,
204 - actions: actions,
205 - );
206 - }),
162 +
163 + // the card designs is empty if widget gets built before it loads.
164 + // should get populated before user sees anything
165 + final CardDesign cardDesign;
166 + if (widget.dashboardViewModel.cardDesigns.isEmpty ||
167 + realIndex >= widget.dashboardViewModel.cardDesigns.length)
168 + cardDesign = CardDesign.genericDefault;
169 + else if (widget.lightningMode)
170 + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
171 + else
172 + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
173 +
174 + final String accountName;
175 + final String accountBalance;
176 + if (account == null) {
177 + accountName = "";
178 + accountBalance = "";
179 + } else {
180 + accountName = account.label;
181 + accountBalance = account.balance ?? "0.00";
182 + }
183 +
184 + final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
185 + ? ""
186 + : walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
187 +
188 + final List<BalanceCardAction> actions = widget.lightningMode
189 + ? [
190 + BalanceCardAction(
191 + label: S.current.bitcoin_lightning_deposit,
192 + icon: Icons.arrow_downward,
193 + onTap: depositToL2,
194 + ),
195 + BalanceCardAction(
196 + label: S.current.bitcoin_lightning_withdraw,
197 + icon: Icons.arrow_upward,
198 + onTap: withdrawFromL2,
199 + )
200 + ]
201 + : widget.dashboardViewModel.isEnabledTradeAction
202 + ? [
203 + BalanceCardAction(
204 + label: S.current.buy,
205 + icon: Icons.arrow_forward_ios_rounded,
206 + iconSize: 12,
207 + onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
208 + )
209 + ]
210 + : [];
211 +
212 + return BalanceCard(
213 + width: effectiveCardWidth,
214 + accountName: accountName,
215 + accountBalance: accountBalance,
216 + designSwitchDuration: Duration(milliseconds: 150),
217 + assetName: assetName,
218 + capitalizeAssetName: _shouldCapitalizeAssetName(),
219 + balance: walletBalance,
220 + fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
221 + widget.dashboardViewModel.settingsStore.fiatCurrency.title,
222 + fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
223 + fiatBalance: walletFiatBalance,
224 + selected: _selectedIndex == visualIndex,
225 + onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
226 + design: cardDesign,
227 + actions: actions,
228 + );
229 + }),
230 + ),
231 ),
232 ),
233 );
lib/new-ui/widgets/coins_page/mweb_ad.dart
+51 -39
@@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget {
21 child: Column(
22 spacing: 12,
23 children: [
24 - GestureDetector(
25 - onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
26 - child: Container(
27 - height: 64,
28 - decoration: BoxDecoration(
29 - borderRadius: BorderRadius.circular(18),
30 - color: Theme.of(context).colorScheme.surfaceContainer),
31 - child: Padding(
32 - padding: const EdgeInsets.symmetric(horizontal: 12.0),
33 - child: Row(
34 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
35 - children: [
36 - CakeImageWidget(
37 - imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
38 - width: 24,
39 - height: 24,
40 - ),
41 - Expanded(
42 - child: Padding(
43 - padding: const EdgeInsets.symmetric(horizontal: 12.0),
44 - child: Text(
45 - S.of(context).mweb_ad,
46 - softWrap: true,
47 - style: TextStyle(fontSize: 12),
24 + MergeSemantics(
25 + child: Semantics(
26 + button: true,
27 + child: GestureDetector(
28 + onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
29 + child: Container(
30 + height: 64,
31 + decoration: BoxDecoration(
32 + borderRadius: BorderRadius.circular(18),
33 + color: Theme.of(context).colorScheme.surfaceContainer),
34 + child: Padding(
35 + padding: const EdgeInsets.symmetric(horizontal: 12.0),
36 + child: Row(
37 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
38 + children: [
39 + ExcludeSemantics(
40 + child: CakeImageWidget(
41 + imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
42 + width: 24,
43 + height: 24,
44 + ),
45 + ),
46 + Expanded(
47 + child: Padding(
48 + padding: const EdgeInsets.symmetric(horizontal: 12.0),
49 + child: Text(
50 + S.of(context).mweb_ad,
51 + softWrap: true,
52 + style: TextStyle(fontSize: 12),
53 + ),
54 + ),
55 ),
49 - ),
56 + Icon(
57 + size: 16,
58 + Icons.arrow_forward_ios,
59 + color: Theme.of(context).colorScheme.primary,
60 + )
61 + ],
62 ),
51 - Icon(
52 - size: 16,
53 - Icons.arrow_forward_ios,
54 - color: Theme.of(context).colorScheme.primary,
55 - )
56 - ],
63 + ),
64 ),
65 ),
66 ),
67 ),
61 - GestureDetector(
62 - onTap: () => dashboardViewModel.dismissMwebAd(false),
63 - child: Text(
64 - S.of(context).do_not_show_anymore,
65 - style: TextStyle(
66 - fontSize: 12,
67 - fontWeight: FontWeight.w500,
68 - color: Theme.of(context).colorScheme.primary),
68 + MergeSemantics(
69 + child: Semantics(
70 + button: true,
71 + child: GestureDetector(
72 + onTap: () => dashboardViewModel.dismissMwebAd(false),
73 + child: Text(
74 + S.of(context).do_not_show_anymore,
75 + style: TextStyle(
76 + fontSize: 12,
77 + fontWeight: FontWeight.w500,
78 + color: Theme.of(context).colorScheme.primary),
79 + ),
80 + ),
81 ),
82 )
83 ],
lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart
+11 -4
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
4 import 'package:flutter/material.dart';
@@ -27,10 +28,16 @@ class ChainIcon extends StatelessWidget {
28 AnimatedOpacity(
29 duration: Duration(milliseconds: 100),
30 opacity: done ? 0 : 1,
30 - child: CircularProgressIndicator(
31 - value: progress,
32 - color: Color(0xFFFFB84E),
33 - strokeWidth: 2,
31 + // Faded out means "nothing to report", so it must leave the tree too.
32 + child: ExcludeSemantics(
33 + excluding: done,
34 + child: CircularProgressIndicator(
35 + value: progress,
36 + color: Color(0xFFFFB84E),
37 + strokeWidth: 2,
38 + semanticsLabel: S.of(context).synchronizing,
39 + semanticsValue: "${(progress * 100).round()}%",
40 + ),
41 ),
42 ),
43 AnimatedScale(
lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart
+74 -65
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart';
@@ -12,76 +13,84 @@ class LightningSwitcher extends StatelessWidget {
13
14 @override
15 Widget build(BuildContext context) {
15 - return SizedBox(
16 - child: InkWell(
17 - onTap: () {
18 - HapticFeedback.mediumImpact();
19 - onLightningSwitchPress();
20 - },
21 - child: Container(
22 - decoration: ShapeDecoration(
23 - shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
24 - color: Theme.of(context).colorScheme.surfaceContainer),
25 - width: 70,
26 - height: 36,
27 - padding: EdgeInsets.symmetric(vertical: 2),
28 - child: Stack(
29 - children: [
30 - AnimatedContainer(
31 - alignment: Alignment.centerRight,
32 - margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
33 - duration: Duration(milliseconds: 250),
34 - curve: Curves.easeOutCubic,
35 - width: 32,
36 - height: 32,
37 - // height: double.infinity,
38 - decoration: BoxDecoration(
39 - borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
40 - color: Theme.of(context).colorScheme.primary),
41 - ),
42 - Container(
43 - child: Row(
44 - spacing: 2.0,
45 - children: [
46 - SizedBox(),
47 - AnimatedSwitcher(
48 - duration: Duration(milliseconds: 150),
49 - transitionBuilder: (child, animation) =>
50 - FadeTransition(opacity: animation, child: child),
51 - child: CakeImageWidget(
52 - imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
53 - key: ValueKey(lightningMode),
54 - width: 32,
55 - height: 32,
56 - colorFilter: ColorFilter.mode(
57 - lightningMode
58 - ? Theme.of(context).colorScheme.primary
59 - : Theme.of(context).colorScheme.surfaceContainer,
60 - BlendMode.srcIn,
16 + // One toggle node: the knob position and the coloured glyphs are the only
17 + // visual cue for which mode is active.
18 + return Semantics(
19 + button: true,
20 + toggled: lightningMode,
21 + label: S.of(context).lightning_mode,
22 + child: SizedBox(
23 + child: InkWell(
24 + onTap: () {
25 + HapticFeedback.mediumImpact();
26 + onLightningSwitchPress();
27 + },
28 + child: Container(
29 + decoration: ShapeDecoration(
30 + shape:
31 + RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
32 + color: Theme.of(context).colorScheme.surfaceContainer),
33 + width: 70,
34 + height: 36,
35 + padding: EdgeInsets.symmetric(vertical: 2),
36 + child: Stack(
37 + children: [
38 + AnimatedContainer(
39 + alignment: Alignment.centerRight,
40 + margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
41 + duration: Duration(milliseconds: 250),
42 + curve: Curves.easeOutCubic,
43 + width: 32,
44 + height: 32,
45 + // height: double.infinity,
46 + decoration: BoxDecoration(
47 + borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
48 + color: Theme.of(context).colorScheme.primary),
49 + ),
50 + Container(
51 + child: Row(
52 + spacing: 2.0,
53 + children: [
54 + SizedBox(),
55 + AnimatedSwitcher(
56 + duration: Duration(milliseconds: 150),
57 + transitionBuilder: (child, animation) =>
58 + FadeTransition(opacity: animation, child: child),
59 + child: CakeImageWidget(
60 + imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
61 + key: ValueKey(lightningMode),
62 + width: 32,
63 + height: 32,
64 + colorFilter: ColorFilter.mode(
65 + lightningMode
66 + ? Theme.of(context).colorScheme.primary
67 + : Theme.of(context).colorScheme.surfaceContainer,
68 + BlendMode.srcIn,
69 + ),
70 ),
71 ),
63 - ),
64 - AnimatedSwitcher(
65 - duration: Duration(milliseconds: 150),
66 - transitionBuilder: (child, animation) =>
67 - FadeTransition(opacity: animation, child: child),
68 - child: CakeImageWidget(
69 - imageUrl: 'assets/new-ui/switcher-lightning.svg',
70 - key: ValueKey(lightningMode),
71 - width: 32,
72 - height: 32,
73 - colorFilter: ColorFilter.mode(
74 - lightningMode
75 - ? Theme.of(context).colorScheme.surfaceContainer
76 - : Theme.of(context).colorScheme.primary,
77 - BlendMode.srcIn,
72 + AnimatedSwitcher(
73 + duration: Duration(milliseconds: 150),
74 + transitionBuilder: (child, animation) =>
75 + FadeTransition(opacity: animation, child: child),
76 + child: CakeImageWidget(
77 + imageUrl: 'assets/new-ui/switcher-lightning.svg',
78 + key: ValueKey(lightningMode),
79 + width: 32,
80 + height: 32,
81 + colorFilter: ColorFilter.mode(
82 + lightningMode
83 + ? Theme.of(context).colorScheme.surfaceContainer
84 + : Theme.of(context).colorScheme.primary,
85 + BlendMode.srcIn,
86 + ),
87 ),
88 ),
80 - ),
81 - ],
89 + ],
90 + ),
91 ),
83 - ),
84 - ],
92 + ],
93 + ),
94 ),
95 ),
96 ),
lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart
+97 -57
@@ -1,5 +1,6 @@
1 import 'package:cake_wallet/core/sync_status_title.dart';
2 import 'package:cake_wallet/di.dart';
3 +import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
5 import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
6 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -47,65 +48,55 @@ class SyncBar extends StatelessWidget {
48 child: Stack(
49 alignment: Alignment.centerLeft,
50 children: [
50 - if (!_showFullBar())
51 - Row(
52 - mainAxisSize: MainAxisSize.min,
53 - spacing: 6,
54 - children: [
55 - if (dashboardViewModel.isTorEnabled)
56 - CakeImageWidget(
57 - imageUrl: "assets/new-ui/tor.svg",
58 - width: 20,
59 - height: 20,
60 - ),
61 - if (_showDot()) PulsingDot(),
62 - ],
63 - ),
51 + if (!_showFullBar()) _buildCompactBar(context),
52 if (_showFullBar())
65 - GestureDetector(
66 - onTap: () {
67 - CupertinoScaffold.showCupertinoModalBottomSheet(
68 - context: context,
69 - barrierColor: Colors.black.withAlpha(85),
70 - builder: (context) => FractionallySizedBox(
71 - child: Material(
72 - child: getIt.get<ManageNodesPage>(param1: false),
73 - )));
74 - },
75 - child: AnimatedSwitcher(
76 - duration: Duration(milliseconds: 100),
77 - child: Container(
78 - key: ValueKey(status.runtimeType),
79 - height: 36,
80 - decoration: BoxDecoration(
81 - borderRadius: BorderRadius.circular(9999),
82 - border: _getBorder(context, status.runtimeType),
83 - color: _getBackgroundColor(context, status.runtimeType),
84 - ),
85 - child: Row(
86 - spacing: 10,
87 - mainAxisAlignment: MainAxisAlignment.center,
88 - crossAxisAlignment: CrossAxisAlignment.center,
89 - mainAxisSize: MainAxisSize.max,
90 - children: [
91 - if (icon != null) icon,
92 - // if (dashboardViewModel.silentPaymentsScanningActive &&
93 - // progressStatuses.contains(status.runtimeType)) ...[
94 - // Text(
95 - // "${(status.progress() * 100).toInt()}%",
96 - // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
97 - // ),
98 - // Text(
99 - // "·",
100 - // style: TextStyle(fontSize: 12),
101 - // )
102 - // ],
103 - Text(
104 - syncStatusTitle(
105 - status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
106 - style: _getTextStyle(context, status.runtimeType),
53 + // A single node: the localized status text (plus any active
54 + // Tor/MWEB/Silent Payments badge) is the label, and the hint says
55 + // where tapping leads. Everything inside is redundant with it.
56 + Semantics(
57 + button: true,
58 + label: _statusSemanticsLabel(context, status),
59 + hint: S.of(context).manage_nodes,
60 + onTap: () => _openNodeManagement(context),
61 + child: ExcludeSemantics(
62 + child: GestureDetector(
63 + onTap: () => _openNodeManagement(context),
64 + child: AnimatedSwitcher(
65 + duration: Duration(milliseconds: 100),
66 + child: Container(
67 + key: ValueKey(status.runtimeType),
68 + height: 36,
69 + decoration: BoxDecoration(
70 + borderRadius: BorderRadius.circular(9999),
71 + border: _getBorder(context, status.runtimeType),
72 + color: _getBackgroundColor(context, status.runtimeType),
73 + ),
74 + child: Row(
75 + spacing: 10,
76 + mainAxisAlignment: MainAxisAlignment.center,
77 + crossAxisAlignment: CrossAxisAlignment.center,
78 + mainAxisSize: MainAxisSize.max,
79 + children: [
80 + if (icon != null) icon,
81 + // if (dashboardViewModel.silentPaymentsScanningActive &&
82 + // progressStatuses.contains(status.runtimeType)) ...[
83 + // Text(
84 + // "${(status.progress() * 100).toInt()}%",
85 + // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
86 + // ),
87 + // Text(
88 + // "·",
89 + // style: TextStyle(fontSize: 12),
90 + // )
91 + // ],
92 + Text(
93 + syncStatusTitle(status,
94 + dashboardViewModel.settingsStore.syncStatusDisplayMode),
95 + style: _getTextStyle(context, status.runtimeType),
96 + ),
97 + ],
98 ),
108 - ],
99 + ),
100 ),
101 ),
102 ),
@@ -117,6 +108,55 @@ class SyncBar extends StatelessWidget {
108 );
109 }
110
111 + void _openNodeManagement(BuildContext context) =>
112 + CupertinoScaffold.showCupertinoModalBottomSheet(
113 + context: context,
114 + barrierColor: Colors.black.withAlpha(85),
115 + builder: (context) => FractionallySizedBox(
116 + child: Material(
117 + child: getIt.get<ManageNodesPage>(param1: false),
118 + )));
119 +
120 + /// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so
121 + /// the whole row needs a text equivalent.
122 + Widget _buildCompactBar(BuildContext context) {
123 + final row = Row(
124 + mainAxisSize: MainAxisSize.min,
125 + spacing: 6,
126 + children: [
127 + if (dashboardViewModel.isTorEnabled)
128 + CakeImageWidget(
129 + imageUrl: "assets/new-ui/tor.svg",
130 + width: 20,
131 + height: 20,
132 + ),
133 + if (_showDot()) PulsingDot(),
134 + ],
135 + );
136 +
137 + final label = _joinLabels([
138 + if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
139 + if (_showDot()) S.of(context).synchronizing,
140 + ]);
141 +
142 + if (label.isEmpty) return row;
143 +
144 + return Semantics(label: label, child: ExcludeSemantics(child: row));
145 + }
146 +
147 + String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
148 + final isFailure = failStatuses.contains(status.runtimeType);
149 +
150 + return _joinLabels([
151 + syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
152 + if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
153 + if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb,
154 + if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments,
155 + ]);
156 + }
157 +
158 + String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
159 +
160 Color? _getBackgroundColor(BuildContext context, Type status) {
161 if (failStatuses.contains(status)) {
162 return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
lib/new-ui/widgets/coins_page/wallet_info.dart
+27 -13
@@ -21,15 +21,22 @@ class WalletInfoBar extends StatelessWidget {
21 final bool hasCustomize;
22 final VoidCallback onCustomizeButtonTap;
23
24 + void _openAccountCustomizer() {
25 + if (hasCustomize) {
26 + onCustomizeButtonTap();
27 + HapticFeedback.mediumImpact();
28 + }
29 + }
30 +
31 @override
32 Widget build(BuildContext context) {
26 - return GestureDetector(
27 - onTap: () {
28 - if (hasCustomize) {
29 - onCustomizeButtonTap();
30 - HapticFeedback.mediumImpact();
31 - }
32 - },
33 + // The row, the hardware-wallet glyph and the inner accounts button are one
34 + // control for a screen reader: a single labeled node opening the customizer.
35 + final semanticsLabel =
36 + hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
37 +
38 + final row = GestureDetector(
39 + onTap: _openAccountCustomizer,
40 child: Row(
41 mainAxisSize: MainAxisSize.min,
42 mainAxisAlignment: MainAxisAlignment.center,
@@ -70,12 +77,7 @@ class WalletInfoBar extends StatelessWidget {
77 SizedBox(width: 8),
78 ModernButton.svg(
79 size: 24,
73 - onPressed: () {
74 - if (hasCustomize) {
75 - onCustomizeButtonTap();
76 - HapticFeedback.mediumImpact();
77 - }
78 - },
80 + onPressed: _openAccountCustomizer,
81 svgPath: "assets/new-ui/icon-accounts.svg",
82 semanticLabel: S.of(context).wallet_accounts,
83 )
@@ -83,6 +85,18 @@ class WalletInfoBar extends StatelessWidget {
85 ],
86 ),
87 );
88 +
89 + if (!hasCustomize) {
90 + return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row));
91 + }
92 +
93 + return Semantics(
94 + button: true,
95 + label: semanticsLabel,
96 + hint: S.of(context).wallet_accounts,
97 + onTap: _openAccountCustomizer,
98 + child: ExcludeSemantics(child: row),
99 + );
100 }
101
102 String? get hardwareWalletIcon {
lib/new-ui/widgets/line_tab_switcher.dart
+33 -24
@@ -93,33 +93,42 @@ class _LineTabSwitcherState extends State<LineTabSwitcher> {
93 children: widget.tabs.map((item) {
94 final index = widget.tabs.indexOf(item);
95
96 - return GestureDetector(
97 - onTap: () {
98 - widget.onTabChange(index);
99 - },
100 - child: Column(
101 - mainAxisSize: MainAxisSize.min,
102 - mainAxisAlignment: MainAxisAlignment.center,
103 - children: [
104 - AnimatedDefaultTextStyle(
105 - duration: Duration(milliseconds: 150),
106 - style: DefaultTextStyle.of(context).style.copyWith(
107 - inherit: true,
108 - fontSize: 16,
109 - fontWeight: FontWeight.w500,
110 - color: widget.selectedTab == index
111 - ? Theme.of(context).colorScheme.onSurface
112 - : Theme.of(context).colorScheme.onSurfaceVariant,
96 + // One tab = one node: the visible label merges into a button node
97 + // that also carries the selected state of this tab.
98 + return MergeSemantics(
99 + child: Semantics(
100 + button: true,
101 + selected: widget.selectedTab == index,
102 + inMutuallyExclusiveGroup: true,
103 + child: GestureDetector(
104 + onTap: () {
105 + widget.onTabChange(index);
106 + },
107 + child: Column(
108 + mainAxisSize: MainAxisSize.min,
109 + mainAxisAlignment: MainAxisAlignment.center,
110 + children: [
111 + AnimatedDefaultTextStyle(
112 + duration: Duration(milliseconds: 150),
113 + style: DefaultTextStyle.of(context).style.copyWith(
114 + inherit: true,
115 + fontSize: 16,
116 + fontWeight: FontWeight.w500,
117 + color: widget.selectedTab == index
118 + ? Theme.of(context).colorScheme.onSurface
119 + : Theme.of(context).colorScheme.onSurfaceVariant,
120 + ),
121 + child: Padding(
122 + padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
123 + child: Text(
124 + item,
125 + key: textWidgetKeys[index],
126 + ),
127 ),
114 - child: Padding(
115 - padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
116 - child: Text(
117 - item,
118 - key: textWidgetKeys[index],
128 ),
120 - ),
129 + ],
130 ),
122 - ],
131 + ),
132 ),
133 );
134 }).toList())),
lib/new-ui/widgets/long_press_menu.dart
+31 -24
@@ -60,31 +60,38 @@ class _LongPressMenuState extends State<LongPressMenu> {
60 final color = item.color ?? Theme.of(context).colorScheme.onSurface;
61 return Material(
62 color: Colors.transparent,
63 - child: InkWell(
64 - onTap: item.onSelected,
65 - child: Padding(
66 - padding: EdgeInsets.only(
67 - left: 16,
68 - right: 16,
69 - top: 12,
70 - bottom: 12,
71 - ),
72 - child: Container(
73 - child: Row(
74 - crossAxisAlignment: CrossAxisAlignment.center,
75 - mainAxisAlignment: MainAxisAlignment.start,
76 - spacing: 8,
77 - children: [
78 - CakeImageWidget(
79 - imageUrl: item.iconPath,
80 - height: 20,
81 - width: 20,
82 - colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
63 + child: MergeSemantics(
64 + child: Semantics(
65 + button: true,
66 + child: InkWell(
67 + onTap: item.onSelected,
68 + child: Padding(
69 + padding: EdgeInsets.only(
70 + left: 16,
71 + right: 16,
72 + top: 12,
73 + bottom: 12,
74 + ),
75 + child: Container(
76 + child: Row(
77 + crossAxisAlignment: CrossAxisAlignment.center,
78 + mainAxisAlignment: MainAxisAlignment.start,
79 + spacing: 8,
80 + children: [
81 + ExcludeSemantics(
82 + child: CakeImageWidget(
83 + imageUrl: item.iconPath,
84 + height: 20,
85 + width: 20,
86 + colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
87 + ),
88 + ),
89 + Text(item.label,
90 + style: TextStyle(
91 + color: color, fontSize: 14, fontWeight: FontWeight.w500)),
92 + ],
93 ),
84 - Text(item.label,
85 - style: TextStyle(
86 - color: color, fontSize: 14, fontWeight: FontWeight.w500)),
87 - ],
94 + ),
95 ),
96 ),
97 ),
lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart
+55 -45
@@ -187,50 +187,56 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
187 ? iconHorizontalPadding / 100
188 : 0),
189 curve: Curves.easeOutCubic,
190 - child: InkWell(
191 - splashFactory: NoSplash.splashFactory,
192 - splashColor: Colors.transparent,
193 - borderRadius: BorderRadius.circular(pillBorderRadius),
194 - onTap: () => _onItemTap(i),
195 - child: AnimatedContainer(
196 - duration:
197 - _firstFrame ? Duration.zero : inactiveIconMoveDuration,
198 - curve: Curves.easeOutCubic,
199 - width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
200 - alignment: Alignment.center,
201 - child: AnimatedAlign(
202 - duration: inactiveIconFadeDuration,
190 + child: Semantics(
191 + button: true,
192 + selected: i == widget.selectedIndex,
193 + inMutuallyExclusiveGroup: true,
194 + label: visibleActions[i].name(context),
195 + child: InkWell(
196 + splashFactory: NoSplash.splashFactory,
197 + splashColor: Colors.transparent,
198 + borderRadius: BorderRadius.circular(pillBorderRadius),
199 + onTap: () => _onItemTap(i),
200 + child: AnimatedContainer(
201 + duration:
202 + _firstFrame ? Duration.zero : inactiveIconMoveDuration,
203 curve: Curves.easeOutCubic,
204 + width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
205 alignment: Alignment.center,
205 - child: AnimatedScale(
206 - duration: inactiveIconAppearDuration,
206 + child: AnimatedAlign(
207 + duration: inactiveIconFadeDuration,
208 curve: Curves.easeOutCubic,
208 - scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
209 - child: TweenAnimationBuilder<Color?>(
210 - tween: ColorTween(
211 - begin: (i == widget.selectedIndex)
212 - ? inactiveColor
213 - : activeColor,
214 - end: (i == widget.selectedIndex)
215 - ? activeColor
216 - : inactiveColor,
217 - ),
218 - duration: iconColorChangeDuration,
219 - builder: (context, value, child) {
220 - return Container(
221 - height: NewMainNavBar.barHeight,
222 - child: CakeImageWidget(
223 - imageUrl: visibleActions[i].image,
224 - width: iconWidth,
225 - height: iconHeight,
226 - //fit: BoxFit.scaleDown,
227 - colorFilter: ColorFilter.mode(
228 - value ?? inactiveColor,
229 - BlendMode.srcIn,
209 + alignment: Alignment.center,
210 + child: AnimatedScale(
211 + duration: inactiveIconAppearDuration,
212 + curve: Curves.easeOutCubic,
213 + scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
214 + child: TweenAnimationBuilder<Color?>(
215 + tween: ColorTween(
216 + begin: (i == widget.selectedIndex)
217 + ? inactiveColor
218 + : activeColor,
219 + end: (i == widget.selectedIndex)
220 + ? activeColor
221 + : inactiveColor,
222 + ),
223 + duration: iconColorChangeDuration,
224 + builder: (context, value, child) {
225 + return Container(
226 + height: NewMainNavBar.barHeight,
227 + child: CakeImageWidget(
228 + imageUrl: visibleActions[i].image,
229 + width: iconWidth,
230 + height: iconHeight,
231 + //fit: BoxFit.scaleDown,
232 + colorFilter: ColorFilter.mode(
233 + value ?? inactiveColor,
234 + BlendMode.srcIn,
235 + ),
236 ),
231 - ),
232 - );
233 - }),
237 + );
238 + }),
239 + ),
240 ),
241 ),
242 ),
@@ -306,11 +312,15 @@ class AnimatedPill extends StatelessWidget {
312 children: [
313 Padding(
314 padding: EdgeInsets.only(left: pillIconWidth + 2),
309 - child: Text(
310 - currentAction.name(context),
311 - style: pillTextStyle.copyWith(color: contentColor),
312 - overflow: TextOverflow.fade,
313 - softWrap: false,
315 + // The selected tab's InkWell already announces this name, so the
316 + // pill text must not become a second stop for screen readers.
317 + child: ExcludeSemantics(
318 + child: Text(
319 + currentAction.name(context),
320 + style: pillTextStyle.copyWith(color: contentColor),
321 + overflow: TextOverflow.fade,
322 + softWrap: false,
323 + ),
324 ),
325 ),
326 ],
res/values/strings_en.arb
+4
@@ -528,6 +528,7 @@
528 "got_it": "Got it",
529 "gross_balance": "Gross Balance",
530 "group_by_type": "Group by type",
531 + "hardware_wallet": "Hardware wallet",
532 "haven_app": "Haven by Cake Wallet",
533 "haven_app_wallet_text": "Awesome wallet for Haven",
534 "help": "help",
@@ -588,6 +589,7 @@
589 "lightning_deposit_desc": "When you deposit to Lightning, you are swapping your on-chain Bitcoin from this wallet to your Lightning account.",
590 "lightning_deposit_disclaimer": "The new Lightning balance will not be available until the Bitcoin transaction is fully confirmed.",
591 "lightning_external_disclaimer": "Don't close this page until you send the funds, or else the deposit won't proceed.",
592 + "lightning_mode": "Lightning mode",
593 "lightning_username_desc": "Easily receive Lightning payments with a Lightning username by choosing a custom name or using a randomly generated one below.",
594 "lightning_username_desc_completed": "You can use this username to receive payments from any Lightning wallet.",
595 "lightning_username_setup_later": "If you skip now, you can set this up later in the Settings.",
@@ -618,6 +620,7 @@
620 "login": "Login",
621 "logout": "Logout",
622 "long_press_edit_address": "Long press to edit address",
623 + "long_press_hide_balance": "Long press card to hide balance",
624 "long_press_show_balance": "Long press card to show balance",
625 "low_fee": "Low fee",
626 "low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.",
@@ -726,6 +729,7 @@
729 "onramper_option_description": "Quickly buy crypto with many payment methods. Available in most countries. Spreads and fees vary.",
730 "open_gift_card": "Open Gift Card",
731 "open_wallet": "Open Wallet",
732 + "opens_externally": "Opens externally",
733 "optional_description": "Optional description",
734 "optional_email_hint": "Optional payee notification email",
735 "optional_name": "Optional recipient name",