[web] move AccessibilityAnnouncements into SemanticsOwner (flutter/engine#52138)

Move `AccessibilityAnnouncements` into `SemanticsOwner`, effectively making it a singleton (because `SemanticsOwner` is).

Fixes https://github.com/flutter/flutter/issues/139272
This commit is contained in:
Yegor 2024-04-17 09:49:07 -07:00 committed by GitHub
parent e1b38345d3
commit 2affc5230f
7 changed files with 45 additions and 26 deletions

View File

@ -688,9 +688,10 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
case 'flutter/accessibility':
// In widget tests we want to bypass processing of platform messages.
const StandardMessageCodec codec = StandardMessageCodec();
// TODO(yjbanov): Dispatch the announcement to the correct view?
// https://github.com/flutter/flutter/issues/137445
implicitView?.accessibilityAnnouncements.handleMessage(codec, data);
final EngineSemantics semantics = EngineSemantics.instance;
if (semantics.semanticsEnabled) {
semantics.accessibilityAnnouncements.handleMessage(codec, data);
}
replyToPlatformMessage(callback, codec.encodeMessage(true));
return;

View File

@ -4,7 +4,6 @@
import 'package:meta/meta.dart';
import '../platform_dispatcher.dart';
import 'accessibility.dart';
import 'label_and_value.dart';
import 'semantics.dart';
@ -32,7 +31,7 @@ class LiveRegion extends RoleManager {
AccessibilityAnnouncements get _accessibilityAnnouncements =>
_accessibilityAnnouncementsOverride ??
EnginePlatformDispatcher.instance.implicitView!.accessibilityAnnouncements;
EngineSemantics.instance.accessibilityAnnouncements;
@override
void update() {

View File

@ -18,6 +18,7 @@ import '../platform_dispatcher.dart';
import '../util.dart';
import '../vector_math.dart';
import '../window.dart';
import 'accessibility.dart';
import 'checkable.dart';
import 'dialog.dart';
import 'focusable.dart';
@ -1938,6 +1939,19 @@ class EngineSemantics {
static EngineSemantics? _instance;
/// The tag name for the accessibility announcements host.
static const String announcementsHostTagName = 'flt-announcement-host';
/// Implements verbal accessibility announcements.
final AccessibilityAnnouncements accessibilityAnnouncements =
AccessibilityAnnouncements(hostElement: _initializeAccessibilityAnnouncementHost());
static DomElement _initializeAccessibilityAnnouncementHost() {
final DomElement host = createDomElement(announcementsHostTagName);
domDocument.body!.append(host);
return host;
}
/// Disables semantics and uninitializes the singleton [instance].
///
/// Instances of [EngineSemanticsOwner] are no longer valid after calling this

View File

@ -26,8 +26,6 @@ import 'style_manager.dart';
/// | | | |
/// | | | +- <flt-scene>
/// | | |
/// | | +- [announcementsHost] <flt-announcement-host>
/// | | |
/// | | +- <style>
/// | |
/// | +- ...platform views
@ -50,7 +48,6 @@ class DomManager {
final DomElement sceneHost = domDocument.createElement(DomManager.sceneHostTagName);
final DomElement textEditingHost = domDocument.createElement(DomManager.textEditingHostTagName);
final DomElement semanticsHost = domDocument.createElement(DomManager.semanticsHostTagName);
final DomElement announcementsHost = createDomElement(DomManager.announcementsHostTagName);
// Root element children.
rootElement.appendChild(platformViewsHost);
@ -71,7 +68,6 @@ class DomManager {
// Rendering host (shadow root) children.
renderingHost.append(sceneHost);
renderingHost.append(announcementsHost);
// Styling.
@ -106,7 +102,6 @@ class DomManager {
sceneHost: sceneHost,
textEditingHost: textEditingHost,
semanticsHost: semanticsHost,
announcementsHost: announcementsHost,
);
}
@ -117,7 +112,6 @@ class DomManager {
required this.sceneHost,
required this.textEditingHost,
required this.semanticsHost,
required this.announcementsHost,
});
/// The tag name for the Flutter View root element.
@ -135,9 +129,6 @@ class DomManager {
/// The tag name for the semantics host.
static const String semanticsHostTagName = 'flt-semantics-host';
/// The tag name for the accessibility announcements host.
static const String announcementsHostTagName = 'flt-announcement-host';
/// The root DOM element for the entire Flutter View.
///
/// This is where input events are captured, such as pointer events.
@ -168,9 +159,6 @@ class DomManager {
/// around the UI.
final DomElement semanticsHost;
/// This is where accessibility announcements are inserted.
final DomElement announcementsHost;
DomElement? _lastSceneElement;
/// Inserts the [sceneElement] into the DOM and removes the existing scene (if

View File

@ -135,11 +135,6 @@ base class EngineFlutterView implements ui.FlutterView {
semantics.updateSemantics(update);
}
// TODO(yjbanov): How should this look like for multi-view?
// https://github.com/flutter/flutter/issues/137445
late final AccessibilityAnnouncements accessibilityAnnouncements =
AccessibilityAnnouncements(hostElement: dom.announcementsHost);
late final GlobalHtmlAttributes _globalHtmlAttributes = GlobalHtmlAttributes(
rootElement: dom.rootElement,
hostElement: embeddingStrategy.hostElement,

View File

@ -229,6 +229,30 @@ void _testEngineSemanticsOwner() {
expect(semantics().mode, AccessibilityMode.unknown);
});
// Expecting the following DOM structure by default:
//
// <body>
// <flt-announcement-host>
// <flt-announcement-polite></flt-announcement-polite>
// <flt-announcement-assertive></flt-announcement-assertive>
// </flt-announcement-host>
// </body>
test('places accessibility announcements in the <body> tag', () {
final AccessibilityAnnouncements accessibilityAnnouncements = semantics().accessibilityAnnouncements;
final DomElement politeElement = accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite);
final DomElement assertiveElement = accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive);
final DomElement announcementHost = politeElement.parent!;
// Polite and assertive elements share the same host.
expect(
assertiveElement.parent,
announcementHost,
);
// The host is a direct child of <body>
expect(announcementHost.parent, domDocument.body);
});
test('accessibilityFeatures copyWith function works', () {
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
EngineAccessibilityFeatures copy =

View File

@ -27,7 +27,6 @@ void doTests() {
expect(domManager.platformViewsHost.tagName, equalsIgnoringCase(DomManager.glassPaneTagName));
expect(domManager.textEditingHost.tagName, equalsIgnoringCase(DomManager.textEditingHostTagName));
expect(domManager.semanticsHost.tagName, equalsIgnoringCase(DomManager.semanticsHostTagName));
expect(domManager.announcementsHost.tagName, equalsIgnoringCase(DomManager.announcementsHostTagName));
// Check parent-child relationships.
@ -39,10 +38,9 @@ void doTests() {
expect(rootChildren[3].tagName, equalsIgnoringCase('style'));
final List<DomElement> shadowChildren = domManager.renderingHost.childNodes.cast<DomElement>().toList();
expect(shadowChildren.length, 3);
expect(shadowChildren.length, 2);
expect(shadowChildren[0], domManager.sceneHost);
expect(shadowChildren[1], domManager.announcementsHost);
expect(shadowChildren[2].tagName, equalsIgnoringCase('style'));
expect(shadowChildren[1].tagName, equalsIgnoringCase('style'));
});
test('hide placeholder text for textfield', () {