Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 67254d6

Browse files
authored
Use announce function in live region (#38084)
* use announce function in live region * unit tests * createtouch * throw an error for unimplemented functions in the mock class
1 parent 5429243 commit 67254d6

File tree

3 files changed

+91
-28
lines changed

3 files changed

+91
-28
lines changed

lib/web_ui/lib/src/engine/semantics/accessibility.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ AccessibilityAnnouncements get accessibilityAnnouncements {
2929
}
3030
AccessibilityAnnouncements? _accessibilityAnnouncements;
3131

32+
void debugOverrideAccessibilityAnnouncements(AccessibilityAnnouncements override) {
33+
_accessibilityAnnouncements = override;
34+
}
35+
3236
/// Initializes the [accessibilityAnnouncements] singleton.
3337
///
3438
/// It is an error to attempt to initialize the singleton more than once. Call

lib/web_ui/lib/src/engine/semantics/live_region.dart

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,37 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import '../dom.dart';
5+
import 'accessibility.dart';
66
import 'semantics.dart';
77

88
/// Manages semantics configurations that represent live regions.
99
///
10-
/// "aria-live" attribute is added to communicate the live region to the
11-
/// assistive technology.
10+
/// Assistive technologies treat "aria-live" attribute differently. To keep
11+
/// the behavior consistent, [accessibilityAnnouncements.announce] is used.
1212
///
13-
/// The usage of "aria-live" is browser-dependent.
14-
///
15-
/// VoiceOver only supports "aria-live" with "polite" politeness setting. When
16-
/// the inner html content is changed. It doesn't read the "aria-label".
17-
///
18-
/// When there is an aria-live attribute added, assistive technologies read the
13+
/// When there is an update to [LiveRegion], assistive technologies read the
1914
/// label of the element. See [LabelAndValue]. If there is no label provided
20-
/// no content will be read, therefore DOM is cleaned.
15+
/// no content will be read.
2116
class LiveRegion extends RoleManager {
2217
LiveRegion(SemanticsObject semanticsObject)
2318
: super(Role.labelAndValue, semanticsObject);
2419

20+
String? _lastAnnouncement;
21+
2522
@override
2623
void update() {
27-
if (semanticsObject.hasLabel) {
28-
semanticsObject.element.setAttribute('aria-live', 'polite');
29-
} else {
30-
_cleanupDom();
24+
// Avoid announcing the same message over and over.
25+
if (_lastAnnouncement != semanticsObject.label) {
26+
_lastAnnouncement = semanticsObject.label;
27+
if (semanticsObject.hasLabel) {
28+
accessibilityAnnouncements.announce(
29+
_lastAnnouncement! , Assertiveness.polite
30+
);
31+
}
3132
}
3233
}
3334

34-
void _cleanupDom() {
35-
semanticsObject.element.removeAttribute('aria-live');
36-
}
37-
3835
@override
3936
void dispose() {
40-
_cleanupDom();
4137
}
4238
}

lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,12 +1743,43 @@ void _testImage() {
17431743
});
17441744
}
17451745

1746+
class MockAccessibilityAnnouncements implements AccessibilityAnnouncements {
1747+
int announceInvoked = 0;
1748+
1749+
@override
1750+
void announce(String message, Assertiveness assertiveness) {
1751+
announceInvoked += 1;
1752+
}
1753+
1754+
@override
1755+
DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) {
1756+
throw UnsupportedError(
1757+
'ariaLiveElementFor is not supported in MockAccessibilityAnnouncements');
1758+
}
1759+
1760+
@override
1761+
void dispose() {
1762+
throw UnsupportedError(
1763+
'dispose is not supported in MockAccessibilityAnnouncements!');
1764+
}
1765+
1766+
@override
1767+
void handleMessage(StandardMessageCodec codec, ByteData? data) {
1768+
throw UnsupportedError(
1769+
'handleMessage is not supported in MockAccessibilityAnnouncements!');
1770+
}
1771+
}
1772+
17461773
void _testLiveRegion() {
1747-
test('renders a live region if there is a label', () async {
1774+
test('announces the label after an update', () async {
17481775
semantics()
17491776
..debugOverrideTimestampFunction(() => _testTime)
17501777
..semanticsEnabled = true;
17511778

1779+
final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
1780+
MockAccessibilityAnnouncements();
1781+
debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
1782+
17521783
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
17531784
updateNode(
17541785
builder,
@@ -1758,19 +1789,20 @@ void _testLiveRegion() {
17581789
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
17591790
);
17601791
semantics().updateSemantics(builder.build());
1761-
1762-
expectSemanticsTree('''
1763-
<sem aria-label="This is a snackbar" aria-live="polite" style="$rootSemanticStyle"></sem>
1764-
''');
1792+
expect(mockAccessibilityAnnouncements.announceInvoked, 1);
17651793

17661794
semantics().semanticsEnabled = false;
17671795
});
17681796

1769-
test('does not render a live region if there is no label', () async {
1797+
test('does not announce anything if there is no label', () async {
17701798
semantics()
17711799
..debugOverrideTimestampFunction(() => _testTime)
17721800
..semanticsEnabled = true;
17731801

1802+
final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
1803+
MockAccessibilityAnnouncements();
1804+
debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
1805+
17741806
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
17751807
updateNode(
17761808
builder,
@@ -1779,10 +1811,41 @@ void _testLiveRegion() {
17791811
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
17801812
);
17811813
semantics().updateSemantics(builder.build());
1814+
expect(mockAccessibilityAnnouncements.announceInvoked, 0);
17821815

1783-
expectSemanticsTree('''
1784-
<sem style="$rootSemanticStyle"></sem>
1785-
''');
1816+
semantics().semanticsEnabled = false;
1817+
});
1818+
1819+
test('does not announce the same label over and over', () async {
1820+
semantics()
1821+
..debugOverrideTimestampFunction(() => _testTime)
1822+
..semanticsEnabled = true;
1823+
1824+
final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
1825+
MockAccessibilityAnnouncements();
1826+
debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
1827+
1828+
ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
1829+
updateNode(
1830+
builder,
1831+
label: 'This is a snackbar',
1832+
flags: 0 | ui.SemanticsFlag.isLiveRegion.index,
1833+
transform: Matrix4.identity().toFloat64(),
1834+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
1835+
);
1836+
semantics().updateSemantics(builder.build());
1837+
expect(mockAccessibilityAnnouncements.announceInvoked, 1);
1838+
1839+
builder = ui.SemanticsUpdateBuilder();
1840+
updateNode(
1841+
builder,
1842+
label: 'This is a snackbar',
1843+
flags: 0 | ui.SemanticsFlag.isLiveRegion.index,
1844+
transform: Matrix4.identity().toFloat64(),
1845+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
1846+
);
1847+
semantics().updateSemantics(builder.build());
1848+
expect(mockAccessibilityAnnouncements.announceInvoked, 1);
17861849

17871850
semantics().semanticsEnabled = false;
17881851
});

0 commit comments

Comments
 (0)