From 6d1d4eb9843e80a03cd219c38865a7017bfdb0fb Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 9 Nov 2023 14:28:05 -0500 Subject: [PATCH] [web] Refactor a11y announcements out of FlutterViewEmbedder (flutter/engine#47487) - Remove a11y announcements from `FlutterViewEmbedder`. - Simplify a11y announcements tests so they don't need `FlutterViewEmbedder` nor `DomManager`. - Left a few a11y-multi-view TODOs (cc @yjbanov). Part of https://github.com/flutter/flutter/issues/134443 --- .../lib/web_ui/lib/src/engine/embedder.dart | 14 ++++------- .../lib/src/engine/platform_dispatcher.dart | 4 +++- .../lib/src/engine/semantics/live_region.dart | 23 +++++++++++++++---- .../src/engine/view_embedder/dom_manager.dart | 5 +++- .../lib/web_ui/lib/src/engine/window.dart | 6 +++++ .../engine/semantics/accessibility_test.dart | 17 ++++---------- .../test/engine/semantics/semantics_test.dart | 10 +++++--- .../view_embedder/dom_manager_test.dart | 1 + 8 files changed, 49 insertions(+), 31 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart index 57904dc838..ba576d7164 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart @@ -133,8 +133,8 @@ class FlutterViewEmbedder { DomElement get textEditingHostNodeDEPRECATED => _textEditingHostNode; late DomElement _textEditingHostNode; - AccessibilityAnnouncements get accessibilityAnnouncements => _accessibilityAnnouncements; - late AccessibilityAnnouncements _accessibilityAnnouncements; + DomElement get announcementsHostDEPRECATED => _announcementsHost; + late DomElement _announcementsHost; static const String defaultFontStyle = 'normal'; static const String defaultFontWeight = 'normal'; @@ -214,12 +214,11 @@ class FlutterViewEmbedder { .instance.semanticsHelper .prepareAccessibilityPlaceholder(); - final DomElement announcementsElement = createDomElement('flt-announcement-host'); - _accessibilityAnnouncements = AccessibilityAnnouncements(hostElement: announcementsElement); + _announcementsHost = createDomElement('flt-announcement-host'); shadowRoot.append(accessibilityPlaceholder); shadowRoot.append(_sceneHostElement); - shadowRoot.append(announcementsElement); + shadowRoot.append(_announcementsHost); // The semantic host goes last because hit-test order-wise it must be // first. If semantics goes under the scene host, platform views will @@ -248,11 +247,6 @@ class FlutterViewEmbedder { window.onResize.listen(_metricsDidChange); } - /// For tests only. - void debugOverrideAccessibilityAnnouncements(AccessibilityAnnouncements override) { - _accessibilityAnnouncements = override; - } - /// The framework specifies semantics in physical pixels, but CSS uses /// logical pixels. To compensate, an inverse scale is injected at the root /// level. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 0628301b41..d5ce7fd284 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -601,7 +601,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { case 'flutter/accessibility': // In widget tests we want to bypass processing of platform messages. const StandardMessageCodec codec = StandardMessageCodec(); - flutterViewEmbedder.accessibilityAnnouncements.handleMessage(codec, data); + // TODO(yjbanov): Dispatch the announcement to the correct view? + // https://github.com/flutter/flutter/issues/137445 + implicitView!.accessibilityAnnouncements.handleMessage(codec, data); replyToPlatformMessage(callback, codec.encodeMessage(true)); return; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart index cea9c997e1..cd0002f49b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart @@ -2,14 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import '../embedder.dart' show flutterViewEmbedder; +import 'package:meta/meta.dart'; + +import '../platform_dispatcher.dart'; import 'accessibility.dart'; +import 'label_and_value.dart'; import 'semantics.dart'; /// Manages semantics configurations that represent live regions. /// /// Assistive technologies treat "aria-live" attribute differently. To keep -/// the behavior consistent, [accessibilityAnnouncements.announce] is used. +/// the behavior consistent, [AccessibilityAnnouncements.announce] is used. /// /// When there is an update to [LiveRegion], assistive technologies read the /// label of the element. See [LabelAndValue]. If there is no label provided @@ -20,6 +23,17 @@ class LiveRegion extends RoleManager { String? _lastAnnouncement; + static AccessibilityAnnouncements? _accessibilityAnnouncementsOverride; + + @visibleForTesting + static void debugOverrideAccessibilityAnnouncements(AccessibilityAnnouncements? value) { + _accessibilityAnnouncementsOverride = value; + } + + AccessibilityAnnouncements get _accessibilityAnnouncements => + _accessibilityAnnouncementsOverride ?? + EnginePlatformDispatcher.instance.implicitView!.accessibilityAnnouncements; + @override void update() { if (!semanticsObject.isLiveRegion) { @@ -30,8 +44,9 @@ class LiveRegion extends RoleManager { if (_lastAnnouncement != semanticsObject.label) { _lastAnnouncement = semanticsObject.label; if (semanticsObject.hasLabel) { - flutterViewEmbedder.accessibilityAnnouncements.announce( - _lastAnnouncement! , Assertiveness.polite + _accessibilityAnnouncements.announce( + _lastAnnouncement!, + Assertiveness.polite, ); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart index 44f3468611..8d629c12f4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart @@ -23,7 +23,7 @@ import '../embedder.dart'; /// | | | | /// | | | +- /// | | | -/// | | +- +/// | | +- [announcementsHost] /// | | /// | +- ...platform views /// | @@ -63,4 +63,7 @@ class DomManager { /// Otherwise, the phone will disable focusing by touch, only by tabbing /// around the UI. DomElement get semanticsHost => _embedder.semanticsHostElementDEPRECATED; + + /// This is where accessibility announcements are inserted. + DomElement get announcementsHost => _embedder.announcementsHostDEPRECATED; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart index 3bc1fed157..48e12ec5d1 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart @@ -22,6 +22,7 @@ import 'mouse/cursor.dart'; import 'navigation/history.dart'; import 'platform_dispatcher.dart'; import 'platform_views/message_handler.dart'; +import 'semantics/accessibility.dart'; import 'services.dart'; import 'util.dart'; import 'view_embedder/dom_manager.dart'; @@ -63,6 +64,11 @@ base class EngineFlutterView implements ui.FlutterView { @override void updateSemantics(ui.SemanticsUpdate update) => platformDispatcher.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 MouseCursor mouseCursor = MouseCursor(dom.rootElement); late final ContextMenu contextMenu = ContextMenu(dom.rootElement); diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/accessibility_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/accessibility_test.dart index 36385dd066..0ac51c51c5 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -7,11 +7,7 @@ import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/embedder.dart'; -import 'package:ui/src/engine/semantics.dart'; -import 'package:ui/src/engine/services.dart'; -import 'package:ui/src/engine/view_embedder/dom_manager.dart'; +import 'package:ui/src/engine.dart'; const StandardMessageCodec codec = StandardMessageCodec(); @@ -20,27 +16,24 @@ void main() { } void testMain() { - late DomManager domManager; late AccessibilityAnnouncements accessibilityAnnouncements; setUp(() { - final FlutterViewEmbedder embedder = FlutterViewEmbedder(); - domManager = DomManager.fromFlutterViewEmbedderDEPRECATED(embedder); - accessibilityAnnouncements = embedder.accessibilityAnnouncements; + final DomElement announcementsHost = createDomElement('flt-announcement-host'); + accessibilityAnnouncements = AccessibilityAnnouncements(hostElement: announcementsHost); setLiveMessageDurationForTest(const Duration(milliseconds: 10)); expect( - domManager.renderingHost.querySelector('flt-announcement-polite'), + announcementsHost.querySelector('flt-announcement-polite'), accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite), ); expect( - domManager.renderingHost.querySelector('flt-announcement-assertive'), + announcementsHost.querySelector('flt-announcement-assertive'), accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive), ); }); tearDown(() async { await Future.delayed(liveMessageDuration * 2); - domManager.rootElement.remove(); }); group('$AccessibilityAnnouncements', () { diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 8534fe009b..c335f16a93 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -2331,6 +2331,10 @@ class MockAccessibilityAnnouncements implements AccessibilityAnnouncements { } void _testLiveRegion() { + tearDown(() { + LiveRegion.debugOverrideAccessibilityAnnouncements(null); + }); + test('announces the label after an update', () async { semantics() ..debugOverrideTimestampFunction(() => _testTime) @@ -2338,7 +2342,7 @@ void _testLiveRegion() { final MockAccessibilityAnnouncements mockAccessibilityAnnouncements = MockAccessibilityAnnouncements(); - flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); + LiveRegion.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); updateNode( @@ -2361,7 +2365,7 @@ void _testLiveRegion() { final MockAccessibilityAnnouncements mockAccessibilityAnnouncements = MockAccessibilityAnnouncements(); - flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); + LiveRegion.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); updateNode( @@ -2383,7 +2387,7 @@ void _testLiveRegion() { final MockAccessibilityAnnouncements mockAccessibilityAnnouncements = MockAccessibilityAnnouncements(); - flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); + LiveRegion.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements); ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); updateNode( diff --git a/engine/src/flutter/lib/web_ui/test/engine/view_embedder/dom_manager_test.dart b/engine/src/flutter/lib/web_ui/test/engine/view_embedder/dom_manager_test.dart index 915a4eb07f..c593353d3a 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/view_embedder/dom_manager_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/view_embedder/dom_manager_test.dart @@ -22,6 +22,7 @@ void doTests() { expect(domManager.platformViewsHost, embedder.glassPaneElementDEPRECATED); expect(domManager.textEditingHost, embedder.textEditingHostNodeDEPRECATED); expect(domManager.semanticsHost, embedder.semanticsHostElementDEPRECATED); + expect(domManager.announcementsHost, embedder.announcementsHostDEPRECATED); }); }); }