diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart index c4803098ee0..f0ba1d480a4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -51,6 +51,15 @@ class AccessibilityAnnouncements { /// accouncements assertively. final DomHTMLElement _assertiveElement; + /// Whether to append a non-breaking space to the end of the message + /// before outputting it. + /// + /// It's used to work around a VoiceOver bug where announcing the same message + /// repeatedly results in subsequent messages not being announced despite the + /// fact that the previous announcement was already removed from the DOM a + /// long while back. See https://github.com/flutter/flutter/issues/142250. + bool _appendSpace = false; + /// Looks up the element used to announce messages of the given [assertiveness]. DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) { switch (assertiveness) { @@ -84,7 +93,9 @@ class AccessibilityAnnouncements { final DomHTMLElement ariaLiveElement = ariaLiveElementFor(assertiveness); final DomHTMLDivElement messageElement = createDomHTMLDivElement(); - messageElement.text = message; + // See the doc-comment for [_appendSpace] for the rationale. + messageElement.text = _appendSpace ? '$message\u00A0' : message; + _appendSpace = !_appendSpace; ariaLiveElement.append(messageElement); Timer(liveMessageDuration, () => messageElement.remove()); } diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_announcement_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_announcement_test.dart index 0ac51c51c5d..a9c4e9fa945 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_announcement_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_announcement_test.dart @@ -91,21 +91,38 @@ void testMain() { expectNoMessages(); }); - test('Rapid-fire messages are each announced.', () async { + test('Rapid-fire messages are each announced', () async { sendAnnouncementMessage(message: 'Hello'); expectMessages(polite: 'Hello'); await Future.delayed(liveMessageDuration * 0.5); sendAnnouncementMessage(message: 'There'); - expectMessages(polite: 'HelloThere'); + expectMessages(polite: 'HelloThere\u00A0'); await Future.delayed(liveMessageDuration * 0.6); - expectMessages(polite: 'There'); + expectMessages(polite: 'There\u00A0'); await Future.delayed(liveMessageDuration * 0.5); expectNoMessages(); }); + test('Repeated announcements are modified to ensure screen readers announce them', () async { + sendAnnouncementMessage(message: 'Hello'); + expectMessages(polite: 'Hello'); + await Future.delayed(liveMessageDuration); + expectNoMessages(); + + sendAnnouncementMessage(message: 'Hello'); + expectMessages(polite: 'Hello\u00A0'); + await Future.delayed(liveMessageDuration); + expectNoMessages(); + + sendAnnouncementMessage(message: 'Hello'); + expectMessages(polite: 'Hello'); + await Future.delayed(liveMessageDuration); + expectNoMessages(); + }); + test('announce() polite', () async { accessibilityAnnouncements.announce('polite message', Assertiveness.polite); expectMessages(polite: 'polite message');