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

Commit bc6382e

Browse files
authored
[web] move AccessibilityAnnouncements into SemanticsOwner (#52138)
Move `AccessibilityAnnouncements` into `SemanticsOwner`, effectively making it a singleton (because `SemanticsOwner` is). Fixes flutter/flutter#139272
1 parent f8ecbc0 commit bc6382e

File tree

7 files changed

+45
-26
lines changed

7 files changed

+45
-26
lines changed

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,9 +688,10 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
688688
case 'flutter/accessibility':
689689
// In widget tests we want to bypass processing of platform messages.
690690
const StandardMessageCodec codec = StandardMessageCodec();
691-
// TODO(yjbanov): Dispatch the announcement to the correct view?
692-
// https://github.com/flutter/flutter/issues/137445
693-
implicitView?.accessibilityAnnouncements.handleMessage(codec, data);
691+
final EngineSemantics semantics = EngineSemantics.instance;
692+
if (semantics.semanticsEnabled) {
693+
semantics.accessibilityAnnouncements.handleMessage(codec, data);
694+
}
694695
replyToPlatformMessage(callback, codec.encodeMessage(true));
695696
return;
696697

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import 'package:meta/meta.dart';
66

7-
import '../platform_dispatcher.dart';
87
import 'accessibility.dart';
98
import 'label_and_value.dart';
109
import 'semantics.dart';
@@ -32,7 +31,7 @@ class LiveRegion extends RoleManager {
3231

3332
AccessibilityAnnouncements get _accessibilityAnnouncements =>
3433
_accessibilityAnnouncementsOverride ??
35-
EnginePlatformDispatcher.instance.implicitView!.accessibilityAnnouncements;
34+
EngineSemantics.instance.accessibilityAnnouncements;
3635

3736
@override
3837
void update() {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import '../platform_dispatcher.dart';
1818
import '../util.dart';
1919
import '../vector_math.dart';
2020
import '../window.dart';
21+
import 'accessibility.dart';
2122
import 'checkable.dart';
2223
import 'dialog.dart';
2324
import 'focusable.dart';
@@ -1938,6 +1939,19 @@ class EngineSemantics {
19381939

19391940
static EngineSemantics? _instance;
19401941

1942+
/// The tag name for the accessibility announcements host.
1943+
static const String announcementsHostTagName = 'flt-announcement-host';
1944+
1945+
/// Implements verbal accessibility announcements.
1946+
final AccessibilityAnnouncements accessibilityAnnouncements =
1947+
AccessibilityAnnouncements(hostElement: _initializeAccessibilityAnnouncementHost());
1948+
1949+
static DomElement _initializeAccessibilityAnnouncementHost() {
1950+
final DomElement host = createDomElement(announcementsHostTagName);
1951+
domDocument.body!.append(host);
1952+
return host;
1953+
}
1954+
19411955
/// Disables semantics and uninitializes the singleton [instance].
19421956
///
19431957
/// Instances of [EngineSemanticsOwner] are no longer valid after calling this

lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import 'style_manager.dart';
2626
/// | | | |
2727
/// | | | +- <flt-scene>
2828
/// | | |
29-
/// | | +- [announcementsHost] <flt-announcement-host>
30-
/// | | |
3129
/// | | +- <style>
3230
/// | |
3331
/// | +- ...platform views
@@ -50,7 +48,6 @@ class DomManager {
5048
final DomElement sceneHost = domDocument.createElement(DomManager.sceneHostTagName);
5149
final DomElement textEditingHost = domDocument.createElement(DomManager.textEditingHostTagName);
5250
final DomElement semanticsHost = domDocument.createElement(DomManager.semanticsHostTagName);
53-
final DomElement announcementsHost = createDomElement(DomManager.announcementsHostTagName);
5451

5552
// Root element children.
5653
rootElement.appendChild(platformViewsHost);
@@ -71,7 +68,6 @@ class DomManager {
7168
// Rendering host (shadow root) children.
7269

7370
renderingHost.append(sceneHost);
74-
renderingHost.append(announcementsHost);
7571

7672
// Styling.
7773

@@ -106,7 +102,6 @@ class DomManager {
106102
sceneHost: sceneHost,
107103
textEditingHost: textEditingHost,
108104
semanticsHost: semanticsHost,
109-
announcementsHost: announcementsHost,
110105
);
111106
}
112107

@@ -117,7 +112,6 @@ class DomManager {
117112
required this.sceneHost,
118113
required this.textEditingHost,
119114
required this.semanticsHost,
120-
required this.announcementsHost,
121115
});
122116

123117
/// The tag name for the Flutter View root element.
@@ -135,9 +129,6 @@ class DomManager {
135129
/// The tag name for the semantics host.
136130
static const String semanticsHostTagName = 'flt-semantics-host';
137131

138-
/// The tag name for the accessibility announcements host.
139-
static const String announcementsHostTagName = 'flt-announcement-host';
140-
141132
/// The root DOM element for the entire Flutter View.
142133
///
143134
/// This is where input events are captured, such as pointer events.
@@ -168,9 +159,6 @@ class DomManager {
168159
/// around the UI.
169160
final DomElement semanticsHost;
170161

171-
/// This is where accessibility announcements are inserted.
172-
final DomElement announcementsHost;
173-
174162
DomElement? _lastSceneElement;
175163

176164
/// Inserts the [sceneElement] into the DOM and removes the existing scene (if

lib/web_ui/lib/src/engine/window.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,6 @@ base class EngineFlutterView implements ui.FlutterView {
135135
semantics.updateSemantics(update);
136136
}
137137

138-
// TODO(yjbanov): How should this look like for multi-view?
139-
// https://github.com/flutter/flutter/issues/137445
140-
late final AccessibilityAnnouncements accessibilityAnnouncements =
141-
AccessibilityAnnouncements(hostElement: dom.announcementsHost);
142-
143138
late final GlobalHtmlAttributes _globalHtmlAttributes = GlobalHtmlAttributes(
144139
rootElement: dom.rootElement,
145140
hostElement: embeddingStrategy.hostElement,

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,30 @@ void _testEngineSemanticsOwner() {
229229
expect(semantics().mode, AccessibilityMode.unknown);
230230
});
231231

232+
// Expecting the following DOM structure by default:
233+
//
234+
// <body>
235+
// <flt-announcement-host>
236+
// <flt-announcement-polite></flt-announcement-polite>
237+
// <flt-announcement-assertive></flt-announcement-assertive>
238+
// </flt-announcement-host>
239+
// </body>
240+
test('places accessibility announcements in the <body> tag', () {
241+
final AccessibilityAnnouncements accessibilityAnnouncements = semantics().accessibilityAnnouncements;
242+
final DomElement politeElement = accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite);
243+
final DomElement assertiveElement = accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive);
244+
final DomElement announcementHost = politeElement.parent!;
245+
246+
// Polite and assertive elements share the same host.
247+
expect(
248+
assertiveElement.parent,
249+
announcementHost,
250+
);
251+
252+
// The host is a direct child of <body>
253+
expect(announcementHost.parent, domDocument.body);
254+
});
255+
232256
test('accessibilityFeatures copyWith function works', () {
233257
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
234258
EngineAccessibilityFeatures copy =

lib/web_ui/test/engine/view_embedder/dom_manager_test.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ void doTests() {
2727
expect(domManager.platformViewsHost.tagName, equalsIgnoringCase(DomManager.glassPaneTagName));
2828
expect(domManager.textEditingHost.tagName, equalsIgnoringCase(DomManager.textEditingHostTagName));
2929
expect(domManager.semanticsHost.tagName, equalsIgnoringCase(DomManager.semanticsHostTagName));
30-
expect(domManager.announcementsHost.tagName, equalsIgnoringCase(DomManager.announcementsHostTagName));
3130

3231
// Check parent-child relationships.
3332

@@ -39,10 +38,9 @@ void doTests() {
3938
expect(rootChildren[3].tagName, equalsIgnoringCase('style'));
4039

4140
final List<DomElement> shadowChildren = domManager.renderingHost.childNodes.cast<DomElement>().toList();
42-
expect(shadowChildren.length, 3);
41+
expect(shadowChildren.length, 2);
4342
expect(shadowChildren[0], domManager.sceneHost);
44-
expect(shadowChildren[1], domManager.announcementsHost);
45-
expect(shadowChildren[2].tagName, equalsIgnoringCase('style'));
43+
expect(shadowChildren[1].tagName, equalsIgnoringCase('style'));
4644
});
4745

4846
test('hide placeholder text for textfield', () {

0 commit comments

Comments
 (0)