Skip to content

Commit

Permalink
[web] Refactor a11y announcements out of FlutterViewEmbedder (flutter…
Browse files Browse the repository at this point in the history
…#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 flutter/flutter#134443
  • Loading branch information
mdebbar authored Nov 9, 2023
1 parent 9f4af5d commit 0fae75f
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 31 deletions.
14 changes: 4 additions & 10 deletions lib/web_ui/lib/src/engine/embedder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
23 changes: 19 additions & 4 deletions lib/web_ui/lib/src/engine/semantics/live_region.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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,
);
}
}
Expand Down
5 changes: 4 additions & 1 deletion lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import '../embedder.dart';
/// | | | |
/// | | | +- <flt-scene>
/// | | |
/// | | +- <flt-announcement-host>
/// | | +- [announcementsHost] <flt-announcement-host>
/// | |
/// | +- ...platform views
/// |
Expand Down Expand Up @@ -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;
}
6 changes: 6 additions & 0 deletions lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 5 additions & 12 deletions lib/web_ui/test/engine/semantics/accessibility_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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<void>.delayed(liveMessageDuration * 2);
domManager.rootElement.remove();
});

group('$AccessibilityAnnouncements', () {
Expand Down
10 changes: 7 additions & 3 deletions lib/web_ui/test/engine/semantics/semantics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2331,14 +2331,18 @@ class MockAccessibilityAnnouncements implements AccessibilityAnnouncements {
}

void _testLiveRegion() {
tearDown(() {
LiveRegion.debugOverrideAccessibilityAnnouncements(null);
});

test('announces the label after an update', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;

final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
MockAccessibilityAnnouncements();
flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
LiveRegion.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);

final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
Expand All @@ -2361,7 +2365,7 @@ void _testLiveRegion() {

final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
MockAccessibilityAnnouncements();
flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
LiveRegion.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);

final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
Expand All @@ -2383,7 +2387,7 @@ void _testLiveRegion() {

final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
MockAccessibilityAnnouncements();
flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
LiveRegion.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);

ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/test/engine/view_embedder/dom_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}

0 comments on commit 0fae75f

Please sign in to comment.