mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Update a11y announcements to append divs instead of setting content. (flutter/engine#42258)
This also removes the appended divs after a short time so that screen readers don't navigate to it, especially when users are entering the DOM to enable accessiblity. Fixes https://github.com/flutter/flutter/issues/127335.
This commit is contained in:
parent
5fbc30881d
commit
a96b23fa7e
@ -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 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../../engine.dart' show registerHotRestartListener;
|
||||
@ -23,7 +24,7 @@ enum Assertiveness {
|
||||
AccessibilityAnnouncements get accessibilityAnnouncements {
|
||||
assert(
|
||||
_accessibilityAnnouncements != null,
|
||||
'AccessibilityAnnouncements not initialized. Call initializeAccessibilityAnnouncements() to innitialize it.',
|
||||
'AccessibilityAnnouncements not initialized. Call initializeAccessibilityAnnouncements() to initialize it.',
|
||||
);
|
||||
return _accessibilityAnnouncements!;
|
||||
}
|
||||
@ -50,6 +51,17 @@ void initializeAccessibilityAnnouncements() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Duration for which a live message will be present in the DOM for the screen
|
||||
/// reader to announce it.
|
||||
///
|
||||
/// This was determined by trial and error with some extra buffer added.
|
||||
Duration liveMessageDuration = const Duration(milliseconds: 300);
|
||||
|
||||
/// Sets [liveMessageDuration] to reduce the delay in tests.
|
||||
void setLiveMessageDurationForTest(Duration duration) {
|
||||
liveMessageDuration = duration;
|
||||
}
|
||||
|
||||
/// Makes accessibility announcements using `aria-live` DOM elements.
|
||||
class AccessibilityAnnouncements {
|
||||
/// Creates a new instance with its own DOM elements used for announcements.
|
||||
@ -119,12 +131,10 @@ class AccessibilityAnnouncements {
|
||||
assert(!_isDisposed);
|
||||
final DomHTMLElement ariaLiveElement = ariaLiveElementFor(assertiveness);
|
||||
|
||||
// If the last announced message is the same as the new message, some
|
||||
// screen readers, such as Narrator, will not read the same message
|
||||
// again. In this case, add an artifical "." at the end of the message
|
||||
// string to force the text of the message to look different.
|
||||
final String suffix = ariaLiveElement.innerText == message ? '.' : '';
|
||||
ariaLiveElement.text = '$message$suffix';
|
||||
final DomElement messageElement = createDomElement('div');
|
||||
messageElement.text = message;
|
||||
ariaLiveElement.append(messageElement);
|
||||
Timer(liveMessageDuration, () => messageElement.remove());
|
||||
}
|
||||
|
||||
static DomHTMLLabelElement _createElement(Assertiveness assertiveness) {
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
@ -18,20 +21,29 @@ void main() {
|
||||
void testMain() {
|
||||
setUpAll(() async {
|
||||
await initializeEngine();
|
||||
setLiveMessageDurationForTest(const Duration(milliseconds: 10));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
group('$AccessibilityAnnouncements', () {
|
||||
void expectAnnouncementElements({required bool present}) {
|
||||
expect(
|
||||
domDocument.getElementById('ftl-announcement-polite'),
|
||||
present ? isNotNull : isNull,
|
||||
);
|
||||
expect(
|
||||
domDocument.getElementById('ftl-announcement-assertive'),
|
||||
present ? isNotNull : isNull,
|
||||
);
|
||||
}
|
||||
|
||||
test('Initialization and disposal', () {
|
||||
// Elements should be there right after engine initialization.
|
||||
expectAnnouncementElements(present: true);
|
||||
@ -43,76 +55,89 @@ void testMain() {
|
||||
expectAnnouncementElements(present: true);
|
||||
});
|
||||
|
||||
void resetAccessibilityAnnouncements() {
|
||||
accessibilityAnnouncements.dispose();
|
||||
initializeAccessibilityAnnouncements();
|
||||
expectAnnouncementElements(present: true);
|
||||
ByteData? encodeMessageOnly({required String message}) {
|
||||
return codec.encodeMessage(<dynamic, dynamic>{
|
||||
'data': <dynamic, dynamic>{'message': message},
|
||||
});
|
||||
}
|
||||
|
||||
test('Default value of aria-live is polite when assertiveness is not specified', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
const Map<dynamic, dynamic> testInput = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'polite message'}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
void sendAnnouncementMessage({required String message, int? assertiveness}) {
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(<dynamic, dynamic>{
|
||||
'data': <dynamic, dynamic>{
|
||||
'message': message,
|
||||
'assertiveness': assertiveness,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
void expectMessages({String polite = '', String assertive = ''}) {
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, polite);
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, assertive);
|
||||
}
|
||||
|
||||
void expectNoMessages() => expectMessages();
|
||||
|
||||
test('Default value of aria-live is polite when assertiveness is not specified', () async {
|
||||
accessibilityAnnouncements.handleMessage(codec, encodeMessageOnly(message: 'polite message'));
|
||||
expectMessages(polite: 'polite message');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration);
|
||||
expectNoMessages();
|
||||
});
|
||||
|
||||
test('aria-live is assertive when assertiveness is set to 1', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
const Map<dynamic, dynamic> testInput = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'assertive message', 'assertiveness': 1}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, '');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, 'assertive message');
|
||||
test('aria-live is assertive when assertiveness is set to 1', () async {
|
||||
sendAnnouncementMessage(message: 'assertive message', assertiveness: 1);
|
||||
expectMessages(assertive: 'assertive message');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration);
|
||||
expectNoMessages();
|
||||
});
|
||||
|
||||
test('aria-live is polite when assertiveness is null', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
const Map<dynamic, dynamic> testInput = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'polite message', 'assertiveness': null}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
test('aria-live is polite when assertiveness is null', () async {
|
||||
sendAnnouncementMessage(message: 'polite message');
|
||||
expectMessages(polite: 'polite message');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration);
|
||||
expectNoMessages();
|
||||
});
|
||||
|
||||
test('aria-live is polite when assertiveness is set to 0', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
const Map<dynamic, dynamic> testInput = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'polite message', 'assertiveness': 0}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
test('aria-live is polite when assertiveness is set to 0', () async {
|
||||
sendAnnouncementMessage(message: 'polite message', assertiveness: 0);
|
||||
expectMessages(polite: 'polite message');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration);
|
||||
expectNoMessages();
|
||||
});
|
||||
|
||||
test('The same message announced twice is altered to convince the screen reader to read it again.', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
const Map<dynamic, dynamic> testInput = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'Hello'}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
test('Rapid-fire messages are each announced.', () async {
|
||||
sendAnnouncementMessage(message: 'Hello');
|
||||
expectMessages(polite: 'Hello');
|
||||
|
||||
// The DOM value gains a "." to make the message look updated.
|
||||
const Map<dynamic, dynamic> testInput2 = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'Hello'}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput2));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello.');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
await Future<void>.delayed(liveMessageDuration * 0.5);
|
||||
sendAnnouncementMessage(message: 'There');
|
||||
expectMessages(polite: 'HelloThere');
|
||||
|
||||
// Now the "." is removed because the message without it will also look updated.
|
||||
const Map<dynamic, dynamic> testInput3 = <dynamic, dynamic>{'data': <dynamic, dynamic>{'message': 'Hello'}};
|
||||
accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput3));
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
await Future<void>.delayed(liveMessageDuration * 0.6);
|
||||
expectMessages(polite: 'There');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration * 0.5);
|
||||
expectNoMessages();
|
||||
});
|
||||
|
||||
test('announce() polite', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
test('announce() polite', () async {
|
||||
accessibilityAnnouncements.announce('polite message', Assertiveness.polite);
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, '');
|
||||
expectMessages(polite: 'polite message');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration);
|
||||
expectNoMessages();
|
||||
});
|
||||
|
||||
test('announce() assertive', () {
|
||||
resetAccessibilityAnnouncements();
|
||||
test('announce() assertive', () async {
|
||||
accessibilityAnnouncements.announce('assertive message', Assertiveness.assertive);
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, '');
|
||||
expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, 'assertive message');
|
||||
expectMessages(assertive: 'assertive message');
|
||||
|
||||
await Future<void>.delayed(liveMessageDuration);
|
||||
expectNoMessages();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user