Skip to content

Commit 7d525ea

Browse files
authored
Add support for setting the heading level for web semantics (#97894) (#125771)
This change adds a new property in Semantics widget that would take an integer 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: fixes flutter/flutter#97894 Engine part: flutter/engine#41435
1 parent 6f2d0be commit 7d525ea

File tree

5 files changed

+73
-5
lines changed

5 files changed

+73
-5
lines changed

packages/flutter/lib/src/rendering/custom_paint.dart

+3
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,9 @@ class RenderCustomPaint extends RenderProxyBox {
940940
if (properties.header != null) {
941941
config.isHeader = properties.header!;
942942
}
943+
if (properties.headingLevel != null) {
944+
config.headingLevel = properties.headingLevel!;
945+
}
943946
if (properties.scopesRoute != null) {
944947
config.scopesRoute = properties.scopesRoute!;
945948
}

packages/flutter/lib/src/rendering/proxy_box.dart

+3
Original file line numberDiff line numberDiff line change
@@ -4329,6 +4329,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
43294329
if (_properties.header != null) {
43304330
config.isHeader = _properties.header!;
43314331
}
4332+
if (_properties.headingLevel != null) {
4333+
config.headingLevel = _properties.headingLevel!;
4334+
}
43324335
if (_properties.textField != null) {
43334336
config.isTextField = _properties.textField!;
43344337
}

packages/flutter/lib/src/semantics/semantics.dart

+59-5
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ class SemanticsData with Diagnosticable {
446446
required this.platformViewId,
447447
required this.maxValueLength,
448448
required this.currentValueLength,
449+
required this.headingLevel,
449450
this.tags,
450451
this.transform,
451452
this.customSemanticsActionIds,
@@ -454,7 +455,8 @@ class SemanticsData with Diagnosticable {
454455
assert(attributedValue.string == '' || textDirection != null, 'A SemanticsData object with value "${attributedValue.string}" had a null textDirection.'),
455456
assert(attributedDecreasedValue.string == '' || textDirection != null, 'A SemanticsData object with decreasedValue "${attributedDecreasedValue.string}" had a null textDirection.'),
456457
assert(attributedIncreasedValue.string == '' || textDirection != null, 'A SemanticsData object with increasedValue "${attributedIncreasedValue.string}" had a null textDirection.'),
457-
assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.');
458+
assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.'),
459+
assert(headingLevel >= 0 && headingLevel <= 6, 'Heading level must be between 0 and 6');
458460

459461
/// A bit field of [SemanticsFlag]s that apply to this node.
460462
final int flags;
@@ -547,6 +549,12 @@ class SemanticsData with Diagnosticable {
547549
/// The reading direction is given by [textDirection].
548550
final String tooltip;
549551

552+
/// Indicates that this subtree represents a heading.
553+
///
554+
/// A value of 0 indicates that it is not a heading. The value should be a
555+
/// number between 1 and 6, indicating the hierarchical level as a heading.
556+
final int headingLevel;
557+
550558
/// The reading direction for the text in [label], [value],
551559
/// [increasedValue], [decreasedValue], and [hint].
552560
final TextDirection? textDirection;
@@ -719,6 +727,7 @@ class SemanticsData with Diagnosticable {
719727
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
720728
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
721729
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
730+
properties.add(IntProperty('headingLevel', headingLevel, defaultValue: 0));
722731
}
723732

724733
@override
@@ -748,6 +757,7 @@ class SemanticsData with Diagnosticable {
748757
&& other.transform == transform
749758
&& other.elevation == elevation
750759
&& other.thickness == thickness
760+
&& other.headingLevel == headingLevel
751761
&& _sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds);
752762
}
753763

@@ -778,6 +788,7 @@ class SemanticsData with Diagnosticable {
778788
transform,
779789
elevation,
780790
thickness,
791+
headingLevel,
781792
customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!),
782793
),
783794
);
@@ -892,6 +903,7 @@ class SemanticsProperties extends DiagnosticableTree {
892903
this.button,
893904
this.link,
894905
this.header,
906+
this.headingLevel,
895907
this.textField,
896908
this.slider,
897909
this.keyboardKey,
@@ -950,7 +962,8 @@ class SemanticsProperties extends DiagnosticableTree {
950962
assert(value == null || attributedValue == null, 'Only one of value or attributedValue should be provided'),
951963
assert(increasedValue == null || attributedIncreasedValue == null, 'Only one of increasedValue or attributedIncreasedValue should be provided'),
952964
assert(decreasedValue == null || attributedDecreasedValue == null, 'Only one of decreasedValue or attributedDecreasedValue should be provided'),
953-
assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided');
965+
assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided'),
966+
assert(headingLevel == null || (headingLevel > 0 && headingLevel <= 6), 'Heading level must be between 1 and 6');
954967

955968
/// If non-null, indicates that this subtree represents something that can be
956969
/// in an enabled or disabled state.
@@ -1362,6 +1375,17 @@ class SemanticsProperties extends DiagnosticableTree {
13621375
/// [Directionality] or an explicit [textDirection] should be provided.
13631376
final String? tooltip;
13641377

1378+
/// The heading level in the DOM document structure.
1379+
///
1380+
/// This is only applied to web semantics and is ignored on other platforms.
1381+
///
1382+
/// Screen readers will use this value to determine which part of the page
1383+
/// structure this heading represents. A level 1 heading, indicated
1384+
/// with aria-level="1", usually indicates the main heading of a page,
1385+
/// a level 2 heading, defined with aria-level="2" the first subsection,
1386+
/// a level 3 is a subsection of that, and so on.
1387+
final int? headingLevel;
1388+
13651389
/// Provides hint values which override the default hints on supported
13661390
/// platforms.
13671391
///
@@ -2236,7 +2260,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
22362260
|| _maxValueLength != config._maxValueLength
22372261
|| _currentValueLength != config._currentValueLength
22382262
|| _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants
2239-
|| _areUserActionsBlocked != config.isBlockingUserActions;
2263+
|| _areUserActionsBlocked != config.isBlockingUserActions
2264+
|| _headingLevel != config._headingLevel;
22402265
}
22412266

22422267
// TAGS, LABELS, ACTIONS
@@ -2540,7 +2565,14 @@ class SemanticsNode with DiagnosticableTreeMixin {
25402565
int? get currentValueLength => _currentValueLength;
25412566
int? _currentValueLength;
25422567

2543-
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
2568+
/// The level of the widget as a heading within the structural hierarchy
2569+
/// of the screen. A value of 1 indicates the highest level of structural
2570+
/// hierarchy. A value of 2 indicates the next level, and so on.
2571+
int get headingLevel => _headingLevel;
2572+
int _headingLevel = _kEmptyConfig._headingLevel;
2573+
2574+
bool _canPerformAction(SemanticsAction action) =>
2575+
_actions.containsKey(action);
25442576

25452577
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
25462578

@@ -2598,6 +2630,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
25982630
_maxValueLength = config._maxValueLength;
25992631
_currentValueLength = config._currentValueLength;
26002632
_areUserActionsBlocked = config.isBlockingUserActions;
2633+
_headingLevel = config._headingLevel;
26012634
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
26022635

26032636
if (mergeAllDescendantsIntoThisNodeValueChanged) {
@@ -2643,6 +2676,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
26432676
int? platformViewId = _platformViewId;
26442677
int? maxValueLength = _maxValueLength;
26452678
int? currentValueLength = _currentValueLength;
2679+
int headingLevel = _headingLevel;
26462680
final double elevation = _elevation;
26472681
double thickness = _thickness;
26482682
final Set<int> customSemanticsActionIds = <int>{};
@@ -2682,6 +2716,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
26822716
platformViewId ??= node._platformViewId;
26832717
maxValueLength ??= node._maxValueLength;
26842718
currentValueLength ??= node._currentValueLength;
2719+
headingLevel = node._headingLevel;
2720+
26852721
if (identifier == '') {
26862722
identifier = node._identifier;
26872723
}
@@ -2765,6 +2801,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
27652801
maxValueLength: maxValueLength,
27662802
currentValueLength: currentValueLength,
27672803
customSemanticsActionIds: customSemanticsActionIds.toList()..sort(),
2804+
headingLevel: headingLevel,
27682805
);
27692806
}
27702807

@@ -2840,6 +2877,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
28402877
childrenInTraversalOrder: childrenInTraversalOrder,
28412878
childrenInHitTestOrder: childrenInHitTestOrder,
28422879
additionalActions: customSemanticsActionIds ?? _kEmptyCustomSemanticsActionsList,
2880+
headingLevel: data.headingLevel,
28432881
);
28442882
_dirty = false;
28452883
}
@@ -4711,6 +4749,21 @@ class SemanticsConfiguration {
47114749
_setFlag(SemanticsFlag.isHeader, value);
47124750
}
47134751

4752+
/// Indicates the heading level in the document structure.
4753+
///
4754+
/// This is only used for web semantics, and is ignored on other platforms.
4755+
int get headingLevel => _headingLevel;
4756+
int _headingLevel = 0;
4757+
4758+
set headingLevel(int value) {
4759+
assert(value >= 0 && value <= 6);
4760+
if (value == headingLevel) {
4761+
return;
4762+
}
4763+
_headingLevel = value;
4764+
_hasBeenAnnotated = true;
4765+
}
4766+
47144767
/// Whether the owning [RenderObject] is a slider (true) or not (false).
47154768
bool get isSlider => _hasFlag(SemanticsFlag.isSlider);
47164769
set isSlider(bool value) {
@@ -5044,7 +5097,8 @@ class SemanticsConfiguration {
50445097
.._currentValueLength = _currentValueLength
50455098
.._actions.addAll(_actions)
50465099
.._customSemanticsActions.addAll(_customSemanticsActions)
5047-
..isBlockingUserActions = isBlockingUserActions;
5100+
..isBlockingUserActions = isBlockingUserActions
5101+
.._headingLevel = _headingLevel;
50485102
}
50495103
}
50505104

packages/flutter/lib/src/widgets/basic.dart

+2
Original file line numberDiff line numberDiff line change
@@ -7101,6 +7101,7 @@ class Semantics extends SingleChildRenderObjectWidget {
71017101
bool? keyboardKey,
71027102
bool? link,
71037103
bool? header,
7104+
int? headingLevel,
71047105
bool? textField,
71057106
bool? readOnly,
71067107
bool? focusable,
@@ -7172,6 +7173,7 @@ class Semantics extends SingleChildRenderObjectWidget {
71727173
keyboardKey: keyboardKey,
71737174
link: link,
71747175
header: header,
7176+
headingLevel: headingLevel,
71757177
textField: textField,
71767178
readOnly: readOnly,
71777179
focusable: focusable,

packages/flutter_test/test/matchers_test.dart

+6
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@ void main() {
684684
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
685685
currentValueLength: 10,
686686
maxValueLength: 15,
687+
headingLevel: 0,
687688
);
688689
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
689690

@@ -970,6 +971,7 @@ void main() {
970971
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
971972
currentValueLength: 10,
972973
maxValueLength: 15,
974+
headingLevel: 0,
973975
);
974976
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
975977

@@ -1062,6 +1064,7 @@ void main() {
10621064
platformViewId: 105,
10631065
currentValueLength: 10,
10641066
maxValueLength: 15,
1067+
headingLevel: 0,
10651068
);
10661069
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
10671070

@@ -1161,6 +1164,7 @@ void main() {
11611164
platformViewId: 105,
11621165
currentValueLength: 10,
11631166
maxValueLength: 15,
1167+
headingLevel: 0,
11641168
);
11651169
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);
11661170

@@ -1189,6 +1193,7 @@ void main() {
11891193
currentValueLength: 10,
11901194
maxValueLength: 15,
11911195
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
1196+
headingLevel: 0,
11921197
);
11931198
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);
11941199

@@ -1279,6 +1284,7 @@ void main() {
12791284
currentValueLength: 10,
12801285
maxValueLength: 15,
12811286
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
1287+
headingLevel: 0,
12821288
);
12831289
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
12841290

0 commit comments

Comments
 (0)