Skip to content

Commit b29b97c

Browse files
Address review: replace deprecated copy announcements with semantic state
1 parent 54db9ff commit b29b97c

3 files changed

Lines changed: 34 additions & 7 deletions

File tree

lib/new-ui/widgets/copy_wrapper.dart

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ class CopyWrapper extends StatefulWidget {
2727
/// detector. Receives the transient "copied" state.
2828
final Widget Function(BuildContext, bool)? builder;
2929

30-
/// Builds a control that performs the copy itself, so no extra gesture or
31-
/// semantics layer is stacked on top of it. [onCopy] is null when there is
32-
/// nothing to copy.
30+
/// Builds a control that performs the copy itself, so no extra gesture
31+
/// detector or actionable semantics node is stacked on top of it. [onCopy] is
32+
/// null when there is nothing to copy.
3333
final Widget Function(BuildContext context, bool copied, VoidCallback? onCopy)? controlBuilder;
3434

3535
final Duration duration;
@@ -45,7 +45,11 @@ class _CopyWrapperState extends State<CopyWrapper> {
4545
if (widget.data == null) return;
4646
ClipboardUtil.setSensitiveDataToClipboard(widget.data!, isSensitive: widget.isSensitive);
4747
HapticFeedback.mediumImpact();
48-
SemanticsService.announce(S.of(context).copied, Directionality.of(context));
48+
// Screen readers learn about the copy from the transient "copied" state
49+
// below, which build() marks as a live region, instead of from a push
50+
// announcement: those are deprecated on Android and make TalkBack drop
51+
// whatever it was saying. Where we don't render that state, android 13+
52+
// already shows its own clipboard confirmation.
4953
if (await shouldShowCopied()) {
5054
setState(() => copied = true);
5155
Future.delayed(widget.duration, () {
@@ -58,7 +62,17 @@ class _CopyWrapperState extends State<CopyWrapper> {
5862
Widget build(BuildContext context) {
5963
final VoidCallback? onCopy = widget.data == null ? null : handleCopy;
6064

61-
if (widget.controlBuilder != null) return widget.controlBuilder!(context, copied, onCopy);
65+
// The control's own label carries the copied state (Copy -> Copied), so
66+
// merging it into one node and marking that node a live region while copied
67+
// is enough for a screen reader to pick the change up.
68+
if (widget.controlBuilder != null) {
69+
return MergeSemantics(
70+
child: Semantics(
71+
liveRegion: copied,
72+
child: widget.controlBuilder!(context, copied, onCopy),
73+
),
74+
);
75+
}
6276

6377
final content = widget.builder!(context, copied);
6478

@@ -69,6 +83,7 @@ class _CopyWrapperState extends State<CopyWrapper> {
6983
return MergeSemantics(
7084
child: Semantics(
7185
button: !widget.requireLongPress,
86+
liveRegion: copied,
7287
hint: widget.requireLongPress ? S.of(context).long_press_to_copy : S.of(context).copy,
7388
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
7489
CustomSemanticsAction(label: S.of(context).copy): onCopy,

lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ class PayjoinCopyModal extends StatelessWidget {
6666
);
6767
}
6868

69-
void _announceCopied(BuildContext context) =>
70-
SemanticsService.announce(S.of(context).copied, Directionality.of(context));
69+
// Copying pops this sheet right away, so no widget survives to carry a
70+
// semantics state change: this path still needs a direct announcement. It is
71+
// skipped on platforms that deprecate announcements (android, where they
72+
// clear TalkBack's speech queue and the system shows its own clipboard
73+
// confirmation anyway).
74+
void _announceCopied(BuildContext context) {
75+
if (!MediaQuery.supportsAnnounceOf(context)) return;
76+
SemanticsService.sendAnnouncement(
77+
View.of(context), S.of(context).copied, Directionality.of(context));
78+
}
7179
}

lib/new-ui/widgets/swap_page/swap_send_external_modal.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
211211
child: Icon(
212212
copied ? Icons.check : Icons.copy,
213213
key: ValueKey(copied),
214+
// Only the icon changes here, so label
215+
// the copied state for CopyWrapper's
216+
// live region to announce.
217+
semanticLabel: copied ? S.of(context).copied : null,
214218
size: 18,
215219
color: Theme.of(context).colorScheme.primary,
216220
),

0 commit comments

Comments
 (0)