mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Move announcement live elements to the end of the DOM and make them divs instead of labels. (flutter/engine#42432)
- Moving them to the end prevents the screen reader from landing on them before the relevant content. - Making them `div`s instead of `label`s prevents some screen readers (ChromeVox in particular) from landing on the live elements when the live elements are empty. Fixes https://github.com/flutter/flutter/issues/127862.
This commit is contained in:
parent
e61409c668
commit
a612a1dfd1
@ -135,6 +135,9 @@ class FlutterViewEmbedder {
|
||||
DomElement get textEditingHostNode => _textEditingHostNode;
|
||||
late DomElement _textEditingHostNode;
|
||||
|
||||
AccessibilityAnnouncements get accessibilityAnnouncements => _accessibilityAnnouncements;
|
||||
late AccessibilityAnnouncements _accessibilityAnnouncements;
|
||||
|
||||
static const String defaultFontStyle = 'normal';
|
||||
static const String defaultFontWeight = 'normal';
|
||||
static const double defaultFontSize = 14;
|
||||
@ -163,7 +166,6 @@ class FlutterViewEmbedder {
|
||||
_flutterViewElement = domDocument.createElement(flutterViewTagName);
|
||||
_glassPaneElement = domDocument.createElement(glassPaneTagName);
|
||||
|
||||
|
||||
// This must be attached to the DOM now, so the engine can create a host
|
||||
// node (ShadowDOM or a fallback) next.
|
||||
//
|
||||
@ -216,8 +218,12 @@ class FlutterViewEmbedder {
|
||||
.instance.semanticsHelper
|
||||
.prepareAccessibilityPlaceholder();
|
||||
|
||||
final DomElement announcementsElement = createDomElement('flt-announcement-host');
|
||||
_accessibilityAnnouncements = AccessibilityAnnouncements(hostElement: announcementsElement);
|
||||
|
||||
shadowRoot.append(accessibilityPlaceholder);
|
||||
shadowRoot.append(_sceneHostElement!);
|
||||
shadowRoot.append(announcementsElement);
|
||||
|
||||
// The semantic host goes last because hit-test order-wise it must be
|
||||
// first. If semantics goes under the scene host, platform views will
|
||||
@ -246,6 +252,11 @@ 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.
|
||||
|
||||
@ -223,7 +223,6 @@ Future<void> initializeEngineUi() async {
|
||||
}
|
||||
_initializationState = DebugEngineInitializationState.initializingUi;
|
||||
|
||||
initializeAccessibilityAnnouncements();
|
||||
RawKeyboard.initialize(onMacOs: operatingSystem == OperatingSystem.macOs);
|
||||
MouseCursor.initialize();
|
||||
ensureFlutterViewEmbedderInitialized();
|
||||
|
||||
@ -11,10 +11,9 @@ import 'package:ui/src/engine/canvaskit/renderer.dart';
|
||||
import 'package:ui/src/engine/renderer.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../engine.dart' show platformViewManager, registerHotRestartListener;
|
||||
import '../engine.dart' show flutterViewEmbedder, platformViewManager, registerHotRestartListener;
|
||||
import 'clipboard.dart';
|
||||
import 'dom.dart';
|
||||
import 'embedder.dart';
|
||||
import 'mouse_cursor.dart';
|
||||
import 'platform_views/message_handler.dart';
|
||||
import 'plugins.dart';
|
||||
@ -645,7 +644,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
case 'flutter/accessibility':
|
||||
// In widget tests we want to bypass processing of platform messages.
|
||||
const StandardMessageCodec codec = StandardMessageCodec();
|
||||
accessibilityAnnouncements.handleMessage(codec, data);
|
||||
flutterViewEmbedder.accessibilityAnnouncements.handleMessage(codec, data);
|
||||
replyToPlatformMessage(callback, codec.encodeMessage(true));
|
||||
return;
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../../engine.dart' show registerHotRestartListener;
|
||||
import '../dom.dart';
|
||||
import '../services.dart';
|
||||
import '../util.dart';
|
||||
@ -20,37 +19,6 @@ enum Assertiveness {
|
||||
assertive,
|
||||
}
|
||||
|
||||
/// Singleton for accessing accessibility announcements from the platform.
|
||||
AccessibilityAnnouncements get accessibilityAnnouncements {
|
||||
assert(
|
||||
_accessibilityAnnouncements != null,
|
||||
'AccessibilityAnnouncements not initialized. Call initializeAccessibilityAnnouncements() to initialize it.',
|
||||
);
|
||||
return _accessibilityAnnouncements!;
|
||||
}
|
||||
AccessibilityAnnouncements? _accessibilityAnnouncements;
|
||||
|
||||
void debugOverrideAccessibilityAnnouncements(AccessibilityAnnouncements override) {
|
||||
_accessibilityAnnouncements = override;
|
||||
}
|
||||
|
||||
/// Initializes the [accessibilityAnnouncements] singleton.
|
||||
///
|
||||
/// It is an error to attempt to initialize the singleton more than once. Call
|
||||
/// [AccessibilityAnnouncements.dispose] prior to calling this function again.
|
||||
void initializeAccessibilityAnnouncements() {
|
||||
assert(
|
||||
_accessibilityAnnouncements == null,
|
||||
'AccessibilityAnnouncements is already initialized. This is likely a bug in '
|
||||
'Flutter Web engine initialization. Please file an issue at '
|
||||
'https://github.com/flutter/flutter/issues/new/choose',
|
||||
);
|
||||
_accessibilityAnnouncements = AccessibilityAnnouncements();
|
||||
registerHotRestartListener(() {
|
||||
accessibilityAnnouncements.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/// Duration for which a live message will be present in the DOM for the screen
|
||||
/// reader to announce it.
|
||||
///
|
||||
@ -65,11 +33,11 @@ void setLiveMessageDurationForTest(Duration duration) {
|
||||
/// Makes accessibility announcements using `aria-live` DOM elements.
|
||||
class AccessibilityAnnouncements {
|
||||
/// Creates a new instance with its own DOM elements used for announcements.
|
||||
factory AccessibilityAnnouncements() {
|
||||
factory AccessibilityAnnouncements({required DomElement hostElement}) {
|
||||
final DomHTMLElement politeElement = _createElement(Assertiveness.polite);
|
||||
final DomHTMLElement assertiveElement = _createElement(Assertiveness.assertive);
|
||||
domDocument.body!.append(politeElement);
|
||||
domDocument.body!.append(assertiveElement);
|
||||
hostElement.append(politeElement);
|
||||
hostElement.append(assertiveElement);
|
||||
return AccessibilityAnnouncements._(politeElement, assertiveElement);
|
||||
}
|
||||
|
||||
@ -85,32 +53,17 @@ class AccessibilityAnnouncements {
|
||||
|
||||
/// Looks up the element used to announce messages of the given [assertiveness].
|
||||
DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) {
|
||||
assert(!_isDisposed);
|
||||
switch (assertiveness) {
|
||||
case Assertiveness.polite: return _politeElement;
|
||||
case Assertiveness.assertive: return _assertiveElement;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
/// Disposes of the resources used by this object.
|
||||
///
|
||||
/// This object's methods must not be called after calling this method.
|
||||
void dispose() {
|
||||
assert(!_isDisposed);
|
||||
_isDisposed = true;
|
||||
_politeElement.remove();
|
||||
_assertiveElement.remove();
|
||||
_accessibilityAnnouncements = null;
|
||||
}
|
||||
|
||||
/// Makes an accessibity announcement from a message sent by the framework
|
||||
/// over the 'flutter/accessibility' channel.
|
||||
///
|
||||
/// The encoded message is passed as [data], and will be decoded using [codec].
|
||||
void handleMessage(StandardMessageCodec codec, ByteData? data) {
|
||||
assert(!_isDisposed);
|
||||
final Map<dynamic, dynamic> inputMap = codec.decodeMessage(data) as Map<dynamic, dynamic>;
|
||||
final Map<dynamic, dynamic> dataMap = inputMap.readDynamicJson('data');
|
||||
final String? message = dataMap.tryString('message');
|
||||
@ -128,19 +81,17 @@ class AccessibilityAnnouncements {
|
||||
///
|
||||
/// [assertiveness] controls how interruptive the announcement is.
|
||||
void announce(String message, Assertiveness assertiveness) {
|
||||
assert(!_isDisposed);
|
||||
final DomHTMLElement ariaLiveElement = ariaLiveElementFor(assertiveness);
|
||||
|
||||
final DomElement messageElement = createDomElement('div');
|
||||
final DomHTMLDivElement messageElement = createDomHTMLDivElement();
|
||||
messageElement.text = message;
|
||||
ariaLiveElement.append(messageElement);
|
||||
Timer(liveMessageDuration, () => messageElement.remove());
|
||||
}
|
||||
|
||||
static DomHTMLLabelElement _createElement(Assertiveness assertiveness) {
|
||||
static DomHTMLElement _createElement(Assertiveness assertiveness) {
|
||||
final String ariaLiveValue = (assertiveness == Assertiveness.assertive) ? 'assertive' : 'polite';
|
||||
final DomHTMLLabelElement liveRegion = createDomHTMLLabelElement();
|
||||
liveRegion.setAttribute('id', 'ftl-announcement-$ariaLiveValue');
|
||||
final DomHTMLElement liveRegion = createDomElement('flt-announcement-$ariaLiveValue') as DomHTMLElement;
|
||||
liveRegion.style
|
||||
..position = 'fixed'
|
||||
..overflow = 'hidden'
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// 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 'accessibility.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
@ -25,7 +26,7 @@ class LiveRegion extends RoleManager {
|
||||
if (_lastAnnouncement != semanticsObject.label) {
|
||||
_lastAnnouncement = semanticsObject.label;
|
||||
if (semanticsObject.hasLabel) {
|
||||
accessibilityAnnouncements.announce(
|
||||
flutterViewEmbedder.accessibilityAnnouncements.announce(
|
||||
_lastAnnouncement! , Assertiveness.polite
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,7 +8,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/initialization.dart';
|
||||
import 'package:ui/src/engine/embedder.dart';
|
||||
import 'package:ui/src/engine/semantics.dart';
|
||||
import 'package:ui/src/engine/services.dart';
|
||||
|
||||
@ -19,42 +19,29 @@ void main() {
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
setUpAll(() async {
|
||||
await initializeEngine();
|
||||
late FlutterViewEmbedder embedder;
|
||||
late AccessibilityAnnouncements accessibilityAnnouncements;
|
||||
|
||||
setUp(() {
|
||||
embedder = FlutterViewEmbedder();
|
||||
accessibilityAnnouncements = embedder.accessibilityAnnouncements;
|
||||
setLiveMessageDurationForTest(const Duration(milliseconds: 10));
|
||||
expect(
|
||||
embedder.glassPaneShadow.querySelector('flt-announcement-polite'),
|
||||
accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite),
|
||||
);
|
||||
expect(
|
||||
embedder.glassPaneShadow.querySelector('flt-announcement-assertive'),
|
||||
accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive),
|
||||
);
|
||||
});
|
||||
|
||||
void expectAnnouncementElements({required bool present}) {
|
||||
expect(
|
||||
domDocument.getElementById('ftl-announcement-polite'),
|
||||
present ? isNotNull : isNull,
|
||||
);
|
||||
expect(
|
||||
domDocument.getElementById('ftl-announcement-assertive'),
|
||||
present ? isNotNull : isNull,
|
||||
);
|
||||
}
|
||||
|
||||
tearDown(() async {
|
||||
// Completely reset accessibility announcements for subsequent tests.
|
||||
accessibilityAnnouncements.dispose();
|
||||
await Future<void>.delayed(liveMessageDuration * 2);
|
||||
initializeAccessibilityAnnouncements();
|
||||
expectAnnouncementElements(present: true);
|
||||
embedder.glassPaneElement.remove();
|
||||
});
|
||||
|
||||
group('$AccessibilityAnnouncements', () {
|
||||
test('Initialization and disposal', () {
|
||||
// Elements should be there right after engine initialization.
|
||||
expectAnnouncementElements(present: true);
|
||||
|
||||
accessibilityAnnouncements.dispose();
|
||||
expectAnnouncementElements(present: false);
|
||||
|
||||
initializeAccessibilityAnnouncements();
|
||||
expectAnnouncementElements(present: true);
|
||||
});
|
||||
|
||||
ByteData? encodeMessageOnly({required String message}) {
|
||||
return codec.encodeMessage(<dynamic, dynamic>{
|
||||
'data': <dynamic, dynamic>{'message': message},
|
||||
|
||||
@ -2122,12 +2122,6 @@ class MockAccessibilityAnnouncements implements AccessibilityAnnouncements {
|
||||
'ariaLiveElementFor is not supported in MockAccessibilityAnnouncements');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
throw UnsupportedError(
|
||||
'dispose is not supported in MockAccessibilityAnnouncements!');
|
||||
}
|
||||
|
||||
@override
|
||||
void handleMessage(StandardMessageCodec codec, ByteData? data) {
|
||||
throw UnsupportedError(
|
||||
@ -2143,7 +2137,7 @@ void _testLiveRegion() {
|
||||
|
||||
final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
|
||||
MockAccessibilityAnnouncements();
|
||||
debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
|
||||
flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
|
||||
|
||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(
|
||||
@ -2166,7 +2160,7 @@ void _testLiveRegion() {
|
||||
|
||||
final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
|
||||
MockAccessibilityAnnouncements();
|
||||
debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
|
||||
flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
|
||||
|
||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(
|
||||
@ -2188,7 +2182,7 @@ void _testLiveRegion() {
|
||||
|
||||
final MockAccessibilityAnnouncements mockAccessibilityAnnouncements =
|
||||
MockAccessibilityAnnouncements();
|
||||
debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
|
||||
flutterViewEmbedder.debugOverrideAccessibilityAnnouncements(mockAccessibilityAnnouncements);
|
||||
|
||||
ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user