From 1ee98ba8802b01fbb6e2b0367d1d67ef7cc44791 Mon Sep 17 00:00:00 2001 From: Ashish Myles Date: Wed, 31 Jan 2024 17:30:15 -0500 Subject: [PATCH] Make screen reader announcement append a non-breaking space every other message. (flutter/engine#50151) This fixes an issue when the same message was sent to the screen reader again and was not subsequently announced by VoiceOver despite the prior message having long been removed from the DOM. Fixes https://github.com/flutter/flutter/issues/142250 --- .../src/engine/semantics/accessibility.dart | 13 ++++++++++- .../semantics_announcement_test.dart | 23 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) 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');