@@ -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,
0 commit comments