Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 11a32d4

Browse files
authored
Add support for setting the heading level for web semantics (#97894) (#41435)
This change adds a new property in Semantics widget that would take an integer value corresponding to the heading levels defined by the ARIA heading role. This is necessary in order to get proper accessibility and usability in a website for users who rely on screen readers and other assistive technologies. Issue fixed by this PR: flutter/flutter#97894 Framework part: flutter/flutter#125771
1 parent 9e0630f commit 11a32d4

File tree

14 files changed

+139
-5
lines changed

14 files changed

+139
-5
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43347,6 +43347,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
4334743347
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
4334843348
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart + ../../../flutter/LICENSE
4334943349
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ../../../flutter/LICENSE
43350+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart + ../../../flutter/LICENSE
4335043351
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../../flutter/LICENSE
4335143352
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart + ../../../flutter/LICENSE
4335243353
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart + ../../../flutter/LICENSE
@@ -46218,6 +46219,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
4621846219
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
4621946220
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart
4622046221
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
46222+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart
4622146223
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
4622246224
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart
4622346225
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart

lib/ui/fixtures/ui_test.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ void sendSemanticsUpdate() {
231231
transform: transform,
232232
childrenInTraversalOrder: childrenInTraversalOrder,
233233
childrenInHitTestOrder: childrenInHitTestOrder,
234-
additionalActions: additionalActions);
234+
additionalActions: additionalActions,
235+
headingLevel: 0,
236+
);
235237
_semanticsUpdate(builder.build());
236238
}
237239

lib/ui/semantics.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,18 @@ abstract class SemanticsUpdateBuilder {
841841
/// z-direction starting at `elevation`. Basically, in the z-direction the
842842
/// node starts at `elevation` above the parent and ends at `elevation` +
843843
/// `thickness` above the parent.
844+
///
845+
/// The `headingLevel` describes that this node is a heading and the hierarchy
846+
/// level this node represents as a heading. A value of 0 indicates that this
847+
/// node is not a heading. A value of 1 or greater indicates that this node is
848+
/// a heading at the specified level. The valid value range is from 1 to 6,
849+
/// inclusive. This attribute is only used for Web platform, and it will have
850+
/// no effect on other platforms.
851+
///
852+
/// See also:
853+
///
854+
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/heading_role
855+
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-level
844856
void updateNode({
845857
required int id,
846858
required int flags,
@@ -875,6 +887,7 @@ abstract class SemanticsUpdateBuilder {
875887
required Int32List childrenInTraversalOrder,
876888
required Int32List childrenInHitTestOrder,
877889
required Int32List additionalActions,
890+
int headingLevel = 0,
878891
});
879892

880893
/// Update the custom semantics action associated with the given `id`.
@@ -945,8 +958,13 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
945958
required Int32List childrenInTraversalOrder,
946959
required Int32List childrenInHitTestOrder,
947960
required Int32List additionalActions,
961+
int headingLevel = 0,
948962
}) {
949963
assert(_matrix4IsValid(transform));
964+
assert (
965+
headingLevel >= 0 && headingLevel <= 6,
966+
'Heading level must be between 1 and 6, or 0 to indicate that this node is not a heading.'
967+
);
950968
_updateNode(
951969
id,
952970
flags,
@@ -984,6 +1002,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
9841002
childrenInTraversalOrder,
9851003
childrenInHitTestOrder,
9861004
additionalActions,
1005+
headingLevel,
9871006
);
9881007
}
9891008
@Native<
@@ -1024,7 +1043,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
10241043
Handle,
10251044
Handle,
10261045
Handle,
1027-
Handle)>(symbol: 'SemanticsUpdateBuilder::updateNode')
1046+
Handle,
1047+
Int32)>(symbol: 'SemanticsUpdateBuilder::updateNode')
10281048
external void _updateNode(
10291049
int id,
10301050
int flags,
@@ -1061,7 +1081,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
10611081
Float64List transform,
10621082
Int32List childrenInTraversalOrder,
10631083
Int32List childrenInHitTestOrder,
1064-
Int32List additionalActions);
1084+
Int32List additionalActions,
1085+
int headingLevel);
10651086

10661087
@override
10671088
void updateCustomAction({required int id, String? label, String? hint, int overrideId = -1}) {

lib/ui/semantics/semantics_node.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ struct SemanticsNode {
143143
std::vector<int32_t> childrenInTraversalOrder;
144144
std::vector<int32_t> childrenInHitTestOrder;
145145
std::vector<int32_t> customAccessibilityActions;
146+
int32_t headingLevel = 0;
146147
};
147148

148149
// Contains semantic nodes that need to be updated.

lib/ui/semantics/semantics_update_builder.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ void SemanticsUpdateBuilder::updateNode(
6666
const tonic::Float64List& transform,
6767
const tonic::Int32List& childrenInTraversalOrder,
6868
const tonic::Int32List& childrenInHitTestOrder,
69-
const tonic::Int32List& localContextActions) {
69+
const tonic::Int32List& localContextActions,
70+
int headingLevel) {
7071
FML_CHECK(scrollChildren == 0 ||
7172
(scrollChildren > 0 && childrenInHitTestOrder.data()))
7273
<< "Semantics update contained scrollChildren but did not have "
@@ -118,6 +119,8 @@ void SemanticsUpdateBuilder::updateNode(
118119
localContextActions.data(),
119120
localContextActions.data() + localContextActions.num_elements());
120121
nodes_[id] = node;
122+
123+
node.headingLevel = headingLevel;
121124
}
122125

123126
void SemanticsUpdateBuilder::updateCustomAction(int id,

lib/ui/semantics/semantics_update_builder.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ class SemanticsUpdateBuilder
6565
const tonic::Float64List& transform,
6666
const tonic::Int32List& childrenInTraversalOrder,
6767
const tonic::Int32List& childrenInHitTestOrder,
68-
const tonic::Int32List& customAccessibilityActions);
68+
const tonic::Int32List& customAccessibilityActions,
69+
int headingLevel);
6970

7071
void updateCustomAction(int id,
7172
std::string label,

lib/web_ui/lib/semantics.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ class SemanticsUpdateBuilder {
286286
required Int32List childrenInTraversalOrder,
287287
required Int32List childrenInHitTestOrder,
288288
required Int32List additionalActions,
289+
int headingLevel = 0,
289290
}) {
290291
if (transform.length != 16) {
291292
throw ArgumentError('transform argument must have 16 entries.');
@@ -324,6 +325,7 @@ class SemanticsUpdateBuilder {
324325
childrenInHitTestOrder: childrenInHitTestOrder,
325326
additionalActions: additionalActions,
326327
platformViewId: platformViewId,
328+
headingLevel: headingLevel,
327329
));
328330
}
329331

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export 'engine/semantics/accessibility.dart';
146146
export 'engine/semantics/checkable.dart';
147147
export 'engine/semantics/dialog.dart';
148148
export 'engine/semantics/focusable.dart';
149+
export 'engine/semantics/heading.dart';
149150
export 'engine/semantics/image.dart';
150151
export 'engine/semantics/incrementable.dart';
151152
export 'engine/semantics/label_and_value.dart';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
export 'semantics/accessibility.dart';
66
export 'semantics/checkable.dart';
77
export 'semantics/focusable.dart';
8+
export 'semantics/heading.dart';
89
export 'semantics/image.dart';
910
export 'semantics/incrementable.dart';
1011
export 'semantics/label_and_value.dart';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import '../dom.dart';
6+
import 'semantics.dart';
7+
8+
/// Renders semantics objects as headings with the corresponding
9+
/// level (h1 ... h6).
10+
class Heading extends PrimaryRoleManager {
11+
Heading(SemanticsObject semanticsObject)
12+
: super.blank(PrimaryRole.heading, semanticsObject) {
13+
addHeadingRole();
14+
}
15+
16+
@override
17+
void update() {
18+
super.update();
19+
20+
if (!semanticsObject.isHeadingLevelDirty) {
21+
return;
22+
}
23+
24+
addHeadingLevel(semanticsObject.headingLevel);
25+
}
26+
27+
@override
28+
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
29+
30+
void addHeadingRole() {
31+
setAriaRole('heading');
32+
}
33+
34+
void addHeadingLevel(int headingLevel) {
35+
semanticsObject.element.setAttribute('aria-level', headingLevel);
36+
}
37+
}

0 commit comments

Comments
 (0)