Skip to content

Commit 0e477e6

Browse files
flutter-zlIvoneDjaja
authored andcommitted
Use aria-hidden attribute for platform view accessibility on web (flutter#177969)
Use aria-hidden attribute for platform view accessibility on web Before change: https://map-1023-before.web.app/ After change: https://map-1023-after.web.app/ Fixes flutter#171948. Note: When a descendant element receives focus (for example, a marker), the browser automatically overrides aria-hidden. This behavior is correct and expected for accessibility compliance.
1 parent b0aea8b commit 0e477e6

File tree

4 files changed

+142
-3
lines changed

4 files changed

+142
-3
lines changed

engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class PlatformViewManager {
3737
/// rendering of PlatformViews into the web app.
3838
static PlatformViewManager instance = PlatformViewManager();
3939

40+
/// The attribute name used to hide platform views from accessibility.
41+
static const String _ariaHiddenAttribute = 'aria-hidden';
42+
4043
// The factory functions, indexed by the viewType
4144
final Map<String, Function> _factories = <String, Function>{};
4245

@@ -119,7 +122,7 @@ class PlatformViewManager {
119122
/// The resulting DOM for the `contents` of a Platform View looks like this:
120123
///
121124
/// ```html
122-
/// <flt-platform-view id="flt-pv-VIEW_ID" slot="...">
125+
/// <flt-platform-view id="flt-pv-VIEW_ID" slot="..." aria-hidden="true">
123126
/// <arbitrary-html-elements />
124127
/// </flt-platform-view-slot>
125128
/// ```
@@ -131,6 +134,9 @@ class PlatformViewManager {
131134
/// a place where to attach the `slot` property, that will tell the browser
132135
/// what `slot` tag will reveal this `contents`, **without modifying the returned
133136
/// html from the `factory` function**.
137+
///
138+
/// By default, platform views are hidden from accessibility using aria-hidden.
139+
/// The semantics layer will remove this when a semantic node is created.
134140
DomElement renderContent(String viewType, int viewId, Object? params) {
135141
assert(
136142
knowsViewType(viewType),
@@ -158,6 +164,8 @@ class PlatformViewManager {
158164
_ensureContentCorrectlySized(content, viewType);
159165
wrapper.append(content);
160166

167+
wrapper.setAttribute(_ariaHiddenAttribute, 'true');
168+
161169
return wrapper;
162170
});
163171
}
@@ -209,6 +217,23 @@ class PlatformViewManager {
209217
/// component.
210218
bool isVisible(int viewId) => !isInvisible(viewId);
211219

220+
/// Updates the accessibility attributes of a platform view.
221+
///
222+
/// This is called by the semantics layer to hide or show platform views
223+
/// from screen readers based on semantic properties like ExcludeSemantics.
224+
void updatePlatformViewAccessibility(int viewId, bool isHidden) {
225+
final DomElement? wrapper = getSlottedContent(viewId);
226+
if (wrapper == null) {
227+
return;
228+
}
229+
230+
if (isHidden) {
231+
wrapper.setAttribute(_ariaHiddenAttribute, 'true');
232+
} else {
233+
wrapper.removeAttribute(_ariaHiddenAttribute);
234+
}
235+
}
236+
212237
/// Clears the state. Used in tests.
213238
void debugClear() {
214239
_contents.keys.toList().forEach(clearPlatformView);

engine/src/flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import '../platform_views/content_manager.dart';
56
import '../platform_views/slots.dart';
67
import 'label_and_value.dart';
78
import 'semantics.dart';
@@ -40,9 +41,18 @@ class SemanticPlatformView extends SemanticRole {
4041
super.update();
4142

4243
if (semanticsObject.isPlatformView) {
43-
if (semanticsObject.isPlatformViewIdDirty) {
44-
setAttribute('aria-owns', getPlatformViewDomId(semanticsObject.platformViewId));
44+
final int platformViewId = semanticsObject.platformViewId;
45+
final bool isHidden = semanticsObject.flags.isHidden;
46+
47+
if (isHidden) {
48+
// When hidden, remove aria-owns since the platform view is not part
49+
// of the accessibility tree.
50+
removeAttribute('aria-owns');
51+
} else {
52+
setAttribute('aria-owns', getPlatformViewDomId(platformViewId));
4553
}
54+
55+
PlatformViewManager.instance.updatePlatformViewAccessibility(platformViewId, isHidden);
4656
} else {
4757
removeAttribute('aria-owns');
4858
}

engine/src/flutter/lib/web_ui/test/engine/platform_views/content_manager_test.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,5 +242,72 @@ void testMain() {
242242
expect(contentManager.isVisible(viewId + 2), isTrue);
243243
expect(contentManager.isInvisible(viewId + 2), isFalse);
244244
});
245+
246+
group('updatePlatformViewAccessibility', () {
247+
setUp(() {
248+
contentManager.registerFactory(viewType, (int id) => createDomHTMLDivElement());
249+
});
250+
251+
test('sets aria-hidden attribute by default when rendering', () {
252+
final DomElement wrapper = contentManager.renderContent(viewType, viewId, null);
253+
254+
expect(
255+
wrapper.getAttribute('aria-hidden'),
256+
'true',
257+
reason: 'Platform views should be aria-hidden by default for ExcludeSemantics support',
258+
);
259+
});
260+
261+
test('hides platform view from accessibility when isHidden is true', () {
262+
final DomElement wrapper = contentManager.renderContent(viewType, viewId, null);
263+
264+
wrapper.removeAttribute('aria-hidden');
265+
expect(wrapper.hasAttribute('aria-hidden'), isFalse);
266+
267+
contentManager.updatePlatformViewAccessibility(viewId, true);
268+
269+
expect(
270+
wrapper.getAttribute('aria-hidden'),
271+
'true',
272+
reason: 'aria-hidden should be set to true when isHidden is true',
273+
);
274+
});
275+
276+
test('makes platform view accessible when isHidden is false', () {
277+
final DomElement wrapper = contentManager.renderContent(viewType, viewId, null);
278+
279+
expect(
280+
wrapper.getAttribute('aria-hidden'),
281+
'true',
282+
reason: 'Platform view should start with aria-hidden=true',
283+
);
284+
285+
contentManager.updatePlatformViewAccessibility(viewId, false);
286+
287+
expect(
288+
wrapper.hasAttribute('aria-hidden'),
289+
isFalse,
290+
reason: 'aria-hidden should be removed when isHidden is false',
291+
);
292+
});
293+
294+
test('handles toggle between hidden and accessible states', () {
295+
final DomElement wrapper = contentManager.renderContent(viewType, viewId, null);
296+
297+
contentManager.updatePlatformViewAccessibility(viewId, false);
298+
expect(wrapper.hasAttribute('aria-hidden'), isFalse);
299+
300+
contentManager.updatePlatformViewAccessibility(viewId, true);
301+
expect(wrapper.getAttribute('aria-hidden'), 'true');
302+
303+
contentManager.updatePlatformViewAccessibility(viewId, false);
304+
expect(wrapper.hasAttribute('aria-hidden'), isFalse);
305+
});
306+
307+
test('does nothing when viewId does not exist', () {
308+
expect(() => contentManager.updatePlatformViewAccessibility(999, true), returnsNormally);
309+
expect(() => contentManager.updatePlatformViewAccessibility(999, false), returnsNormally);
310+
});
311+
});
245312
});
246313
}

engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3960,6 +3960,43 @@ void _testPlatformView() {
39603960

39613961
semantics().semanticsEnabled = false;
39623962
});
3963+
3964+
test('removes aria-owns when platform view is hidden', () async {
3965+
semantics()
3966+
..debugOverrideTimestampFunction(() => _testTime)
3967+
..semanticsEnabled = true;
3968+
3969+
// Create a platform view that is visible (has aria-owns).
3970+
{
3971+
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
3972+
updateNode(builder, platformViewId: 5, rect: const ui.Rect.fromLTRB(0, 0, 100, 50));
3973+
owner().updateSemantics(builder.build());
3974+
expectSemanticsTree(owner(), '<sem aria-owns="flt-pv-5"></sem>');
3975+
}
3976+
3977+
// Hide the platform view (should remove aria-owns).
3978+
{
3979+
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
3980+
updateNode(
3981+
builder,
3982+
platformViewId: 5,
3983+
flags: const ui.SemanticsFlags(isHidden: true),
3984+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
3985+
);
3986+
owner().updateSemantics(builder.build());
3987+
expectSemanticsTree(owner(), '<sem></sem>');
3988+
}
3989+
3990+
// Show the platform view again (should restore aria-owns).
3991+
{
3992+
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
3993+
updateNode(builder, platformViewId: 5, rect: const ui.Rect.fromLTRB(0, 0, 100, 50));
3994+
owner().updateSemantics(builder.build());
3995+
expectSemanticsTree(owner(), '<sem aria-owns="flt-pv-5"></sem>');
3996+
}
3997+
3998+
semantics().semanticsEnabled = false;
3999+
});
39634000
}
39644001

39654002
void _testGroup() {

0 commit comments

Comments
 (0)