[web:a11y] add platform view role (flutter/engine#44188)
Add `PlatformViewRoleManager`, the primary role manager for platform views. Currently, all it does is manager the `aria-owns` attribute that determines the screen reader traversal order of the platform view w.r.t. surrounding content. This is a partial fix for https://github.com/flutter/flutter/issues/124765. While it does not address literally using the TAB key as a means for traversing widgets, it does address traversal via screen reader shortcuts.
This commit is contained in:
@@ -2029,6 +2029,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart + ../../../flutter/LICENSE
|
||||
@@ -4726,6 +4727,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart
|
||||
|
||||
@@ -138,6 +138,7 @@ export 'engine/semantics/image.dart';
|
||||
export 'engine/semantics/incrementable.dart';
|
||||
export 'engine/semantics/label_and_value.dart';
|
||||
export 'engine/semantics/live_region.dart';
|
||||
export 'engine/semantics/platform_view.dart';
|
||||
export 'engine/semantics/scrollable.dart';
|
||||
export 'engine/semantics/semantics.dart';
|
||||
export 'engine/semantics/semantics_helper.dart';
|
||||
|
||||
@@ -108,7 +108,7 @@ class PlatformViewManager {
|
||||
/// The resulting DOM for the `contents` of a Platform View looks like this:
|
||||
///
|
||||
/// ```html
|
||||
/// <flt-platform-view slot="...">
|
||||
/// <flt-platform-view id="flt-pv-VIEW_ID" slot="...">
|
||||
/// <arbitrary-html-elements />
|
||||
/// </flt-platform-view-slot>
|
||||
/// ```
|
||||
@@ -134,6 +134,7 @@ class PlatformViewManager {
|
||||
return _contents.putIfAbsent(viewId, () {
|
||||
final DomElement wrapper = domDocument
|
||||
.createElement('flt-platform-view')
|
||||
..id = getPlatformViewDomId(viewId)
|
||||
..setAttribute('slot', slotName);
|
||||
|
||||
final Function factoryFunction = _factories[viewType]!;
|
||||
|
||||
@@ -13,6 +13,12 @@ String getPlatformViewSlotName(int viewId) {
|
||||
return 'flt-pv-slot-$viewId';
|
||||
}
|
||||
|
||||
/// Returns the value of the HTML "id" attribute set on the wrapper element that
|
||||
/// hosts the platform view content.
|
||||
String getPlatformViewDomId(int viewId) {
|
||||
return 'flt-pv-$viewId';
|
||||
}
|
||||
|
||||
/// Creates the HTML markup for the `slot` of a Platform View.
|
||||
///
|
||||
/// The resulting DOM for a `slot` looks like this:
|
||||
|
||||
@@ -9,6 +9,7 @@ export 'semantics/image.dart';
|
||||
export 'semantics/incrementable.dart';
|
||||
export 'semantics/label_and_value.dart';
|
||||
export 'semantics/live_region.dart';
|
||||
export 'semantics/platform_view.dart';
|
||||
export 'semantics/scrollable.dart';
|
||||
export 'semantics/semantics.dart';
|
||||
export 'semantics/semantics_helper.dart';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 '../dom.dart';
|
||||
import '../platform_views/slots.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
/// Manages the semantic element corresponding to a platform view.
|
||||
///
|
||||
/// The element in the semantics tree exists only to supply the ARIA traversal
|
||||
/// order. The actual content of the platform view is managed by
|
||||
/// [PlatformViewManager].
|
||||
///
|
||||
/// The traversal order is established using "aria-owns", by pointing to the
|
||||
/// element that hosts the view contents. As of this writing, Safari on macOS
|
||||
/// and on iOS does not support "aria-owns". All other browsers on all operating
|
||||
/// systems support it.
|
||||
///
|
||||
/// See also:
|
||||
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-owns
|
||||
/// * https://bugs.webkit.org/show_bug.cgi?id=223798
|
||||
class PlatformViewRoleManager extends PrimaryRoleManager {
|
||||
PlatformViewRoleManager(SemanticsObject semanticsObject)
|
||||
: super.withBasics(PrimaryRole.platformView, semanticsObject);
|
||||
|
||||
@override
|
||||
void update() {
|
||||
super.update();
|
||||
|
||||
if (semanticsObject.isPlatformView) {
|
||||
if (semanticsObject.isPlatformViewIdDirty) {
|
||||
semanticsObject.element.setAttribute(
|
||||
'aria-owns',
|
||||
getPlatformViewDomId(semanticsObject.platformViewId),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
semanticsObject.element.removeAttribute('aria-owns');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import 'image.dart';
|
||||
import 'incrementable.dart';
|
||||
import 'label_and_value.dart';
|
||||
import 'live_region.dart';
|
||||
import 'platform_view.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'semantics_helper.dart';
|
||||
import 'tappable.dart';
|
||||
@@ -332,12 +333,6 @@ class SemanticsNodeUpdate {
|
||||
/// Each value corresponds to the most specific role a semantics node plays in
|
||||
/// the semantics tree.
|
||||
enum PrimaryRole {
|
||||
/// A role used when a more specific role cannot be assigend to
|
||||
/// a [SemanticsObject].
|
||||
///
|
||||
/// Provides a label or a value.
|
||||
generic,
|
||||
|
||||
/// Supports incrementing and/or decrementing its value.
|
||||
incrementable,
|
||||
|
||||
@@ -378,6 +373,15 @@ enum PrimaryRole {
|
||||
/// children. For example, a modal barrier has `scopesRoute` set but marking
|
||||
/// it as a dialog would be wrong.
|
||||
dialog,
|
||||
|
||||
/// The node's primary role is to host a platform view.
|
||||
platformView,
|
||||
|
||||
/// A role used when a more specific role cannot be assigend to
|
||||
/// a [SemanticsObject].
|
||||
///
|
||||
/// Provides a label or a value.
|
||||
generic,
|
||||
}
|
||||
|
||||
/// Identifies one of the secondary [RoleManager]s of a [PrimaryRoleManager].
|
||||
@@ -964,6 +968,9 @@ class SemanticsObject {
|
||||
|
||||
static const int _platformViewIdIndex = 1 << 23;
|
||||
|
||||
/// Whether the [platformViewId] field has been updated but has not been
|
||||
/// applied to the DOM yet.
|
||||
bool get isPlatformViewIdDirty => _isDirty(_platformViewIdIndex);
|
||||
void _markPlatformViewIdDirty() {
|
||||
_dirtyFields |= _platformViewIdIndex;
|
||||
}
|
||||
@@ -1462,7 +1469,9 @@ class SemanticsObject {
|
||||
|
||||
PrimaryRole _getPrimaryRoleIdentifier() {
|
||||
// The most specific role should take precedence.
|
||||
if (isTextField) {
|
||||
if (isPlatformView) {
|
||||
return PrimaryRole.platformView;
|
||||
} else if (isTextField) {
|
||||
return PrimaryRole.textField;
|
||||
} else if (isIncrementable) {
|
||||
return PrimaryRole.incrementable;
|
||||
@@ -1490,6 +1499,7 @@ class SemanticsObject {
|
||||
PrimaryRole.checkable => Checkable(this),
|
||||
PrimaryRole.dialog => Dialog(this),
|
||||
PrimaryRole.image => ImageRoleManager(this),
|
||||
PrimaryRole.platformView => PlatformViewRoleManager(this),
|
||||
PrimaryRole.generic => GenericRole(this),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,7 +109,8 @@ void testMain() {
|
||||
test('rendered markup contains required attributes', () async {
|
||||
final DomElement content =
|
||||
contentManager.renderContent(viewType, viewId, null);
|
||||
expect(content.getAttribute('slot'), contains('$viewId'));
|
||||
expect(content.getAttribute('slot'), getPlatformViewSlotName(viewId));
|
||||
expect(content.getAttribute('id'), getPlatformViewDomId(viewId));
|
||||
|
||||
final DomElement userContent = content.querySelector('div')!;
|
||||
expect(userContent.style.height, '100%');
|
||||
|
||||
@@ -35,4 +35,12 @@ void testMain() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('getPlatformViewSlotName', () {
|
||||
expect(getPlatformViewSlotName(42), 'flt-pv-slot-42');
|
||||
});
|
||||
|
||||
test('getPlatformViewDomId', () {
|
||||
expect(getPlatformViewDomId(42), 'flt-pv-42');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2277,6 +2277,38 @@ void _testLiveRegion() {
|
||||
}
|
||||
|
||||
void _testPlatformView() {
|
||||
test('sets and updates aria-owns', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
// Set.
|
||||
{
|
||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(
|
||||
builder,
|
||||
platformViewId: 5,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('<sem aria-owns="flt-pv-5" style="$rootSemanticStyle"></sem>');
|
||||
}
|
||||
|
||||
// Update.
|
||||
{
|
||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(
|
||||
builder,
|
||||
platformViewId: 42,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('<sem aria-owns="flt-pv-42" style="$rootSemanticStyle"></sem>');
|
||||
}
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('is transparent w.r.t. hit testing', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
@@ -2290,7 +2322,7 @@ void _testPlatformView() {
|
||||
);
|
||||
semantics().updateSemantics(builder.build());
|
||||
|
||||
expectSemanticsTree('<sem style="$rootSemanticStyle"></sem>');
|
||||
expectSemanticsTree('<sem aria-owns="flt-pv-5" style="$rootSemanticStyle"></sem>');
|
||||
final DomElement element = appHostNode.querySelector('flt-semantics')!;
|
||||
expect(element.style.pointerEvents, 'none');
|
||||
|
||||
@@ -2375,7 +2407,7 @@ void _testPlatformView() {
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem style="z-index: 3"></sem>
|
||||
<sem style="z-index: 2"></sem>
|
||||
<sem style="z-index: 2" aria-owns="flt-pv-0"></sem>
|
||||
<sem style="z-index: 1"></sem>
|
||||
</sem-c>
|
||||
</sem>''');
|
||||
|
||||
@@ -301,7 +301,7 @@ class SemanticsTester {
|
||||
currentValueLength: currentValueLength ?? 0,
|
||||
textSelectionBase: textSelectionBase ?? 0,
|
||||
textSelectionExtent: textSelectionExtent ?? 0,
|
||||
platformViewId: platformViewId ?? 0,
|
||||
platformViewId: platformViewId ?? -1,
|
||||
scrollChildren: scrollChildren ?? 0,
|
||||
scrollIndex: scrollIndex ?? 0,
|
||||
scrollPosition: scrollPosition ?? 0,
|
||||
|
||||
Reference in New Issue
Block a user