[web] API to customize semantics placeholder message (#178309)

To customize the accessibility placeholder message:

```dart
import 'dart:ui_web' as ui_web;

void main() {
  ui_web.accessibilityPlaceholderMessage = 'My Custom Accessibility Message!';
  // ....
}
```

Fixes https://github.com/flutter/flutter/issues/178172
This commit is contained in:
Mouad Debbar 2025-11-12 13:08:38 -05:00 committed by GitHub
parent d8a99e9a9a
commit e4d2b8743f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 90 additions and 26 deletions

View File

@ -60,8 +60,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher();
@visibleForTesting
final DomElement accessibilityPlaceholder = EngineSemantics.instance.semanticsHelper
.prepareAccessibilityPlaceholder();
DomElement get accessibilityPlaceholder =>
EngineSemantics.instance.semanticsHelper.accessibilityPlaceholder;
PlatformConfiguration configuration = PlatformConfiguration(
locales: parseBrowserLanguages(),

View File

@ -24,13 +24,6 @@ const int kMaxSemanticsActivationAttempts = 20;
/// Otherwise these events can cause unintended gestures on the framework side.
const Duration _periodToConsumeEvents = Duration(milliseconds: 300);
/// The message in the label for the placeholder element used to enable
/// accessibility.
///
/// This uses US English as the default message. Set this value prior to
/// calling `runApp` to translate to another language.
String placeholderMessage = 'Enable accessibility';
/// A helper for [EngineSemanticsOwner].
///
/// [SemanticsHelper] prepares and placeholder to enable semantics.
@ -56,8 +49,10 @@ class SemanticsHelper {
return _semanticsEnabler.shouldEnableSemantics(event);
}
DomElement prepareAccessibilityPlaceholder() {
return _semanticsEnabler.prepareAccessibilityPlaceholder();
DomElement get accessibilityPlaceholder => _semanticsEnabler.accessibilityPlaceholder;
void updatePlaceholderLabel(String message) {
_semanticsEnabler.updatePlaceholderLabel(message);
}
/// Stops waiting for the user to enable semantics and removes the
@ -103,13 +98,16 @@ abstract class SemanticsEnabler {
/// should be forwarded to the framework.
bool tryEnableSemantics(DomEvent event);
/// Creates the placeholder for accessibility.
///
/// Puts it inside the glasspane.
/// The placeholder element for enabling accessibility.
///
/// On focus the element announces that accessibility can be enabled by
/// tapping/clicking. (Announcement depends on the assistive technology)
DomElement prepareAccessibilityPlaceholder();
late final DomElement accessibilityPlaceholder = _prepareAccessibilityPlaceholder();
DomElement _prepareAccessibilityPlaceholder();
/// Updates the placeholder's label to the given [message].
void updatePlaceholderLabel(String message);
/// Whether platform is still considering enabling semantics.
///
@ -184,7 +182,7 @@ class DesktopSemanticsEnabler extends SemanticsEnabler {
}
@override
DomElement prepareAccessibilityPlaceholder() {
DomElement _prepareAccessibilityPlaceholder() {
final DomElement placeholder = _semanticsPlaceholder = createDomElement(
'flt-semantics-placeholder',
);
@ -207,8 +205,9 @@ class DesktopSemanticsEnabler extends SemanticsEnabler {
placeholder
..setAttribute('role', 'button')
..setAttribute('aria-live', 'polite')
..setAttribute('tabindex', '0')
..setAttribute('aria-label', placeholderMessage);
..setAttribute('tabindex', '0');
updatePlaceholderLabel(ui_web.accessibilityPlaceholderMessage);
// The placeholder sits just outside the window so only AT can reach it.
placeholder.style
@ -220,6 +219,11 @@ class DesktopSemanticsEnabler extends SemanticsEnabler {
return placeholder;
}
@override
void updatePlaceholderLabel(String message) {
_semanticsPlaceholder?.setAttribute('aria-label', message);
}
@override
void dispose() {
_semanticsPlaceholder?.remove();
@ -383,7 +387,7 @@ class MobileSemanticsEnabler extends SemanticsEnabler {
}
@override
DomElement prepareAccessibilityPlaceholder() {
DomElement _prepareAccessibilityPlaceholder() {
final DomElement placeholder = _semanticsPlaceholder = createDomElement(
'flt-semantics-placeholder',
);
@ -398,9 +402,8 @@ class MobileSemanticsEnabler extends SemanticsEnabler {
true.toJS,
);
placeholder
..setAttribute('role', 'button')
..setAttribute('aria-label', placeholderMessage);
placeholder.setAttribute('role', 'button');
updatePlaceholderLabel(ui_web.accessibilityPlaceholderMessage);
placeholder.style
..position = 'absolute'
..left = '0'
@ -411,6 +414,11 @@ class MobileSemanticsEnabler extends SemanticsEnabler {
return placeholder;
}
@override
void updatePlaceholderLabel(String message) {
_semanticsPlaceholder?.setAttribute('aria-label', message);
}
@override
void dispose() {
_semanticsPlaceholder?.remove();

View File

@ -18,4 +18,5 @@ export 'ui_web/navigation/platform_location.dart';
export 'ui_web/navigation/url_strategy.dart';
export 'ui_web/platform_view_registry.dart';
export 'ui_web/plugins.dart';
export 'ui_web/semantics.dart';
export 'ui_web/testing.dart';

View File

@ -0,0 +1,21 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ui/src/engine.dart';
String _accessibilityPlaceholderMessage = 'Enable accessibility';
/// The message in the label for the placeholder element used to enable accessibility.
///
/// This uses US English as the default message. Set this value at any time to update the label
/// in the placeholder element.
String get accessibilityPlaceholderMessage => _accessibilityPlaceholderMessage;
set accessibilityPlaceholderMessage(String message) {
if (message == _accessibilityPlaceholderMessage) {
return;
}
_accessibilityPlaceholderMessage = message;
EngineSemantics.instance.semanticsHelper.updatePlaceholderLabel(message);
}

View File

@ -481,6 +481,17 @@ void testMain() {
expect(dispatcher.accessibilityPlaceholder.isConnected, isFalse);
});
test('accessibility placeholder label can be updated', () {
final placeholder = domDocument.querySelector('flt-semantics-placeholder')!;
const String testLabel = 'Test accessibility label';
ui_web.accessibilityPlaceholderMessage = testLabel;
expect(placeholder.getAttribute('aria-label'), testLabel);
ui_web.accessibilityPlaceholderMessage = 'Enable accessibility';
expect(placeholder.getAttribute('aria-label'), 'Enable accessibility');
});
test('scheduleWarmupFrame should call both callbacks', () async {
bool beginFrameCalled = false;
final Completer<void> drawFrameCalled = Completer<void>();

View File

@ -21,7 +21,7 @@ void testMain() {
setUp(() {
EngineSemantics.instance.semanticsEnabled = false;
desktopSemanticsEnabler = DesktopSemanticsEnabler();
placeholder = desktopSemanticsEnabler.prepareAccessibilityPlaceholder();
placeholder = desktopSemanticsEnabler.accessibilityPlaceholder;
domDocument.body!.append(placeholder!);
});
@ -94,6 +94,16 @@ void testMain() {
},
);
test('Can update placeholder label', () {
const String testLabel = 'Test label for placeholder';
desktopSemanticsEnabler.updatePlaceholderLabel(testLabel);
expect(placeholder!.getAttribute('aria-label'), testLabel);
const String anotherLabel = 'Another label for placeholder';
desktopSemanticsEnabler.dispose();
expect(() => desktopSemanticsEnabler.updatePlaceholderLabel(anotherLabel), returnsNormally);
});
test('disposes of the placeholder', () {
domDocument.body!.append(placeholder!);
@ -112,7 +122,7 @@ void testMain() {
setUp(() {
EngineSemantics.instance.semanticsEnabled = false;
mobileSemanticsEnabler = MobileSemanticsEnabler();
placeholder = mobileSemanticsEnabler.prepareAccessibilityPlaceholder();
placeholder = mobileSemanticsEnabler.accessibilityPlaceholder;
domDocument.body!.append(placeholder!);
});
@ -140,6 +150,16 @@ void testMain() {
expect(shouldForwardToFramework, isTrue);
});
test('Can update placeholder label', () {
const String testLabel = 'Test label for placeholder';
mobileSemanticsEnabler.updatePlaceholderLabel(testLabel);
expect(placeholder!.getAttribute('aria-label'), testLabel);
const String anotherLabel = 'Another label for placeholder';
mobileSemanticsEnabler.dispose();
expect(() => mobileSemanticsEnabler.updatePlaceholderLabel(anotherLabel), returnsNormally);
});
test('Enables semantics when receiving a relevant event', () {
expect(mobileSemanticsEnabler.semanticsActivationTimer, isNull);
@ -183,7 +203,7 @@ class FakeSemanticsEnabler extends SemanticsEnabler {
bool get isWaitingToEnableSemantics => true;
@override
DomElement prepareAccessibilityPlaceholder() {
void updatePlaceholderLabel(String message) {
throw UnimplementedError();
}

View File

@ -850,7 +850,10 @@ class MockSemanticsEnabler implements SemanticsEnabler {
bool get isWaitingToEnableSemantics => throw UnimplementedError();
@override
DomElement prepareAccessibilityPlaceholder() {
DomElement get accessibilityPlaceholder => throw UnimplementedError();
@override
void updatePlaceholderLabel(String message) {
throw UnimplementedError();
}