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 09c1ec4a583..a2e41fb441d 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 @@ -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(), diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart index 073dd48960e..1c4ac91bd9b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart @@ -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(); diff --git a/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web.dart b/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web.dart index b790ae7b671..c5e4907e5c6 100644 --- a/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web.dart +++ b/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web.dart @@ -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'; diff --git a/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/semantics.dart b/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/semantics.dart new file mode 100644 index 00000000000..91ff6f90f64 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/ui_web/src/ui_web/semantics.dart @@ -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); +} diff --git a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart index 5392039ef1f..a8cfb3a1d22 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart @@ -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 drawFrameCalled = Completer(); diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_helper_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_helper_test.dart index edf61ad1930..11b05edd97b 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_helper_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_helper_test.dart @@ -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(); } 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 704da949962..5309e59db3b 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 @@ -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(); }