From ee03ee74b0120dc83492cad3c3d895a54a486046 Mon Sep 17 00:00:00 2001 From: Negar Date: Mon, 5 Dec 2022 13:21:03 -0800 Subject: [PATCH 1/4] use announce function in live region --- .../lib/src/engine/semantics/live_region.dart | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/live_region.dart b/lib/web_ui/lib/src/engine/semantics/live_region.dart index 6f51f6ce55290..38bc8d3c664f5 100644 --- a/lib/web_ui/lib/src/engine/semantics/live_region.dart +++ b/lib/web_ui/lib/src/engine/semantics/live_region.dart @@ -2,37 +2,39 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import '../dom.dart'; +import 'accessibility.dart'; import 'semantics.dart'; /// Manages semantics configurations that represent live regions. /// -/// "aria-live" attribute is added to communicate the live region to the -/// assistive technology. +/// Assistive technologies treat "aria-live" attribute differently. To keep +/// the behavior consistent, [accessibilityAnnouncements.announce] is used. /// -/// The usage of "aria-live" is browser-dependent. -/// -/// VoiceOver only supports "aria-live" with "polite" politeness setting. When -/// the inner html content is changed. It doesn't read the "aria-label". -/// -/// When there is an aria-live attribute added, assistive technologies read the +/// When there is an update to [LiveRegion], assistive technologies read the /// label of the element. See [LabelAndValue]. If there is no label provided -/// no content will be read, therefore DOM is cleaned. +/// no content will be read. class LiveRegion extends RoleManager { LiveRegion(SemanticsObject semanticsObject) : super(Role.labelAndValue, semanticsObject); + String? lastAnnouncement; + @override void update() { - if (semanticsObject.hasLabel) { - semanticsObject.element.setAttribute('aria-live', 'polite'); - } else { - _cleanupDom(); + // Avoid announcing the same message over and over. + if (lastAnnouncement != semanticsObject.label) + { + lastAnnouncement = semanticsObject.label; + if (lastAnnouncement != null) { + accessibilityAnnouncements.announce( + lastAnnouncement! , Assertiveness.polite + ); + } } } void _cleanupDom() { - semanticsObject.element.removeAttribute('aria-live'); + lastAnnouncement = null; } @override From 11d15485dddb3bf2ce6b1d4ed36534f6bfdf0bf1 Mon Sep 17 00:00:00 2001 From: Negar Date: Mon, 5 Dec 2022 15:57:10 -0800 Subject: [PATCH 2/4] unit tests --- .../lib/src/engine/semantics/live_region.dart | 12 ++++++------ .../test/engine/semantics/semantics_test.dart | 13 ++++--------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/live_region.dart b/lib/web_ui/lib/src/engine/semantics/live_region.dart index 38bc8d3c664f5..a19b613dac594 100644 --- a/lib/web_ui/lib/src/engine/semantics/live_region.dart +++ b/lib/web_ui/lib/src/engine/semantics/live_region.dart @@ -17,24 +17,24 @@ class LiveRegion extends RoleManager { LiveRegion(SemanticsObject semanticsObject) : super(Role.labelAndValue, semanticsObject); - String? lastAnnouncement; + String? _lastAnnouncement; @override void update() { // Avoid announcing the same message over and over. - if (lastAnnouncement != semanticsObject.label) + if (_lastAnnouncement != semanticsObject.label) { - lastAnnouncement = semanticsObject.label; - if (lastAnnouncement != null) { + _lastAnnouncement = semanticsObject.label; + if (_lastAnnouncement != null) { accessibilityAnnouncements.announce( - lastAnnouncement! , Assertiveness.polite + _lastAnnouncement! , Assertiveness.polite ); } } } void _cleanupDom() { - lastAnnouncement = null; + _lastAnnouncement = null; } @override diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index c5683b9afaed0..99dd1cd749c5e 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -1744,7 +1744,7 @@ void _testImage() { } void _testLiveRegion() { - test('renders a live region if there is a label', () async { + test('announces the label after an update', () async { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -1759,14 +1759,11 @@ void _testLiveRegion() { ); semantics().updateSemantics(builder.build()); - expectSemanticsTree(''' - -'''); - + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'This is a snackbar'); semantics().semanticsEnabled = false; }); - test('does not render a live region if there is no label', () async { + test('does not announce anything if there is no label', () async { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -1780,9 +1777,7 @@ void _testLiveRegion() { ); semantics().updateSemantics(builder.build()); - expectSemanticsTree(''' - -'''); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, ''); semantics().semanticsEnabled = false; }); From 86f5b7cb3047ceed99836a7c2b9a8284b01f9eea Mon Sep 17 00:00:00 2001 From: Negar Date: Mon, 5 Dec 2022 19:37:31 -0800 Subject: [PATCH 3/4] createtouch --- .../src/engine/semantics/accessibility.dart | 4 ++ .../lib/src/engine/semantics/live_region.dart | 10 +-- .../test/engine/semantics/semantics_test.dart | 66 ++++++++++++++++++- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/lib/web_ui/lib/src/engine/semantics/accessibility.dart index 9f95393eb2397..8062765f2ffff 100644 --- a/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -29,6 +29,10 @@ AccessibilityAnnouncements get accessibilityAnnouncements { } AccessibilityAnnouncements? _accessibilityAnnouncements; +void debugOverrideAccessibilityAnnouncements(AccessibilityAnnouncements override) { + _accessibilityAnnouncements = override; +} + /// Initializes the [accessibilityAnnouncements] singleton. /// /// It is an error to attempt to initialize the singleton more than once. Call diff --git a/lib/web_ui/lib/src/engine/semantics/live_region.dart b/lib/web_ui/lib/src/engine/semantics/live_region.dart index a19b613dac594..2d17cff0a15b7 100644 --- a/lib/web_ui/lib/src/engine/semantics/live_region.dart +++ b/lib/web_ui/lib/src/engine/semantics/live_region.dart @@ -22,10 +22,9 @@ class LiveRegion extends RoleManager { @override void update() { // Avoid announcing the same message over and over. - if (_lastAnnouncement != semanticsObject.label) - { + if (_lastAnnouncement != semanticsObject.label) { _lastAnnouncement = semanticsObject.label; - if (_lastAnnouncement != null) { + if (semanticsObject.hasLabel) { accessibilityAnnouncements.announce( _lastAnnouncement! , Assertiveness.polite ); @@ -33,12 +32,7 @@ class LiveRegion extends RoleManager { } } - void _cleanupDom() { - _lastAnnouncement = null; - } - @override void dispose() { - _cleanupDom(); } } diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 99dd1cd749c5e..2c38207d6df64 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -1743,12 +1743,38 @@ void _testImage() { }); } +class MockAccessibilityAnnouncements implements AccessibilityAnnouncements { + int announceInvoked = 0; + + @override + void announce(String message, Assertiveness assertiveness) { + announceInvoked += 1; + } + + @override + DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) { + return createDomHTMLDivElement(); + } + + @override + void dispose() { + } + + @override + void handleMessage(StandardMessageCodec codec, ByteData? data) { + } +} + void _testLiveRegion() { test('announces the label after an update', () async { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; + final MockAccessibilityAnnouncements mockAccessibilityAnnouncements = + MockAccessibilityAnnouncements(); + debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); + final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); updateNode( builder, @@ -1758,8 +1784,8 @@ void _testLiveRegion() { rect: const ui.Rect.fromLTRB(0, 0, 100, 50), ); semantics().updateSemantics(builder.build()); + expect(mockAccessibilityAnnouncements.announceInvoked, 1); - expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'This is a snackbar'); semantics().semanticsEnabled = false; }); @@ -1768,6 +1794,10 @@ void _testLiveRegion() { ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; + final MockAccessibilityAnnouncements mockAccessibilityAnnouncements = + MockAccessibilityAnnouncements(); + debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); + final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); updateNode( builder, @@ -1776,8 +1806,40 @@ void _testLiveRegion() { rect: const ui.Rect.fromLTRB(0, 0, 100, 50), ); semantics().updateSemantics(builder.build()); + expect(mockAccessibilityAnnouncements.announceInvoked, 0); + + semantics().semanticsEnabled = false; + }); + + test('does not announce the same label over and over', () async { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + final MockAccessibilityAnnouncements mockAccessibilityAnnouncements = + MockAccessibilityAnnouncements(); + debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); + + ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); + updateNode( + builder, + label: 'This is a snackbar', + flags: 0 | ui.SemanticsFlag.isLiveRegion.index, + transform: Matrix4.identity().toFloat64(), + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + semantics().updateSemantics(builder.build()); - expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, ''); + builder = ui.SemanticsUpdateBuilder(); + updateNode( + builder, + label: 'This is a snackbar', + flags: 0 | ui.SemanticsFlag.isLiveRegion.index, + transform: Matrix4.identity().toFloat64(), + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + semantics().updateSemantics(builder.build()); + expect(mockAccessibilityAnnouncements.announceInvoked, 1); semantics().semanticsEnabled = false; }); From 7b6fe94b10c0dc907805bd0125a3673bda3c8592 Mon Sep 17 00:00:00 2001 From: Negar Date: Tue, 6 Dec 2022 14:46:59 -0800 Subject: [PATCH 4/4] throw an error for unimplemented functions in the mock class --- lib/web_ui/test/engine/semantics/semantics_test.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 2c38207d6df64..3de8054bc9844 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -1753,15 +1753,20 @@ class MockAccessibilityAnnouncements implements AccessibilityAnnouncements { @override DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) { - return createDomHTMLDivElement(); + throw UnsupportedError( + 'ariaLiveElementFor is not supported in MockAccessibilityAnnouncements'); } @override void dispose() { + throw UnsupportedError( + 'dispose is not supported in MockAccessibilityAnnouncements!'); } @override void handleMessage(StandardMessageCodec codec, ByteData? data) { + throw UnsupportedError( + 'handleMessage is not supported in MockAccessibilityAnnouncements!'); } } @@ -1829,6 +1834,7 @@ void _testLiveRegion() { rect: const ui.Rect.fromLTRB(0, 0, 100, 50), ); semantics().updateSemantics(builder.build()); + expect(mockAccessibilityAnnouncements.announceInvoked, 1); builder = ui.SemanticsUpdateBuilder(); updateNode(