Skip to content

Commit

Permalink
Add support for setting the heading level for web semantics (#97894) …
Browse files Browse the repository at this point in the history
…(#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
  • Loading branch information
victorgalo authored Jun 6, 2024
1 parent 6f2d0be commit 7d525ea
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 5 deletions.
3 changes: 3 additions & 0 deletions packages/flutter/lib/src/rendering/custom_paint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.header != null) {
config.isHeader = properties.header!;
}
if (properties.headingLevel != null) {
config.headingLevel = properties.headingLevel!;
}
if (properties.scopesRoute != null) {
config.scopesRoute = properties.scopesRoute!;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/flutter/lib/src/rendering/proxy_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4329,6 +4329,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (_properties.header != null) {
config.isHeader = _properties.header!;
}
if (_properties.headingLevel != null) {
config.headingLevel = _properties.headingLevel!;
}
if (_properties.textField != null) {
config.isTextField = _properties.textField!;
}
Expand Down
64 changes: 59 additions & 5 deletions packages/flutter/lib/src/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ class SemanticsData with Diagnosticable {
required this.platformViewId,
required this.maxValueLength,
required this.currentValueLength,
required this.headingLevel,
this.tags,
this.transform,
this.customSemanticsActionIds,
Expand All @@ -454,7 +455,8 @@ class SemanticsData with Diagnosticable {
assert(attributedValue.string == '' || textDirection != null, 'A SemanticsData object with value "${attributedValue.string}" had a null textDirection.'),
assert(attributedDecreasedValue.string == '' || textDirection != null, 'A SemanticsData object with decreasedValue "${attributedDecreasedValue.string}" had a null textDirection.'),
assert(attributedIncreasedValue.string == '' || textDirection != null, 'A SemanticsData object with increasedValue "${attributedIncreasedValue.string}" had a null textDirection.'),
assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.');
assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.'),
assert(headingLevel >= 0 && headingLevel <= 6, 'Heading level must be between 0 and 6');

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

/// Indicates that this subtree represents a heading.
///
/// A value of 0 indicates that it is not a heading. The value should be a
/// number between 1 and 6, indicating the hierarchical level as a heading.
final int headingLevel;

/// The reading direction for the text in [label], [value],
/// [increasedValue], [decreasedValue], and [hint].
final TextDirection? textDirection;
Expand Down Expand Up @@ -719,6 +727,7 @@ class SemanticsData with Diagnosticable {
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
properties.add(IntProperty('headingLevel', headingLevel, defaultValue: 0));
}

@override
Expand Down Expand Up @@ -748,6 +757,7 @@ class SemanticsData with Diagnosticable {
&& other.transform == transform
&& other.elevation == elevation
&& other.thickness == thickness
&& other.headingLevel == headingLevel
&& _sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds);
}

Expand Down Expand Up @@ -778,6 +788,7 @@ class SemanticsData with Diagnosticable {
transform,
elevation,
thickness,
headingLevel,
customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!),
),
);
Expand Down Expand Up @@ -892,6 +903,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.button,
this.link,
this.header,
this.headingLevel,
this.textField,
this.slider,
this.keyboardKey,
Expand Down Expand Up @@ -950,7 +962,8 @@ class SemanticsProperties extends DiagnosticableTree {
assert(value == null || attributedValue == null, 'Only one of value or attributedValue should be provided'),
assert(increasedValue == null || attributedIncreasedValue == null, 'Only one of increasedValue or attributedIncreasedValue should be provided'),
assert(decreasedValue == null || attributedDecreasedValue == null, 'Only one of decreasedValue or attributedDecreasedValue should be provided'),
assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided');
assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided'),
assert(headingLevel == null || (headingLevel > 0 && headingLevel <= 6), 'Heading level must be between 1 and 6');

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

/// The heading level in the DOM document structure.
///
/// This is only applied to web semantics and is ignored on other platforms.
///
/// Screen readers will use this value to determine which part of the page
/// structure this heading represents. A level 1 heading, indicated
/// with aria-level="1", usually indicates the main heading of a page,
/// a level 2 heading, defined with aria-level="2" the first subsection,
/// a level 3 is a subsection of that, and so on.
final int? headingLevel;

/// Provides hint values which override the default hints on supported
/// platforms.
///
Expand Down Expand Up @@ -2236,7 +2260,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
|| _maxValueLength != config._maxValueLength
|| _currentValueLength != config._currentValueLength
|| _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants
|| _areUserActionsBlocked != config.isBlockingUserActions;
|| _areUserActionsBlocked != config.isBlockingUserActions
|| _headingLevel != config._headingLevel;
}

// TAGS, LABELS, ACTIONS
Expand Down Expand Up @@ -2540,7 +2565,14 @@ class SemanticsNode with DiagnosticableTreeMixin {
int? get currentValueLength => _currentValueLength;
int? _currentValueLength;

bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
/// The level of the widget as a heading within the structural hierarchy
/// of the screen. A value of 1 indicates the highest level of structural
/// hierarchy. A value of 2 indicates the next level, and so on.
int get headingLevel => _headingLevel;
int _headingLevel = _kEmptyConfig._headingLevel;

bool _canPerformAction(SemanticsAction action) =>
_actions.containsKey(action);

static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();

Expand Down Expand Up @@ -2598,6 +2630,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
_maxValueLength = config._maxValueLength;
_currentValueLength = config._currentValueLength;
_areUserActionsBlocked = config.isBlockingUserActions;
_headingLevel = config._headingLevel;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);

if (mergeAllDescendantsIntoThisNodeValueChanged) {
Expand Down Expand Up @@ -2643,6 +2676,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
int? platformViewId = _platformViewId;
int? maxValueLength = _maxValueLength;
int? currentValueLength = _currentValueLength;
int headingLevel = _headingLevel;
final double elevation = _elevation;
double thickness = _thickness;
final Set<int> customSemanticsActionIds = <int>{};
Expand Down Expand Up @@ -2682,6 +2716,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
platformViewId ??= node._platformViewId;
maxValueLength ??= node._maxValueLength;
currentValueLength ??= node._currentValueLength;
headingLevel = node._headingLevel;

if (identifier == '') {
identifier = node._identifier;
}
Expand Down Expand Up @@ -2765,6 +2801,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
customSemanticsActionIds: customSemanticsActionIds.toList()..sort(),
headingLevel: headingLevel,
);
}

Expand Down Expand Up @@ -2840,6 +2877,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: customSemanticsActionIds ?? _kEmptyCustomSemanticsActionsList,
headingLevel: data.headingLevel,
);
_dirty = false;
}
Expand Down Expand Up @@ -4711,6 +4749,21 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isHeader, value);
}

/// Indicates the heading level in the document structure.
///
/// This is only used for web semantics, and is ignored on other platforms.
int get headingLevel => _headingLevel;
int _headingLevel = 0;

set headingLevel(int value) {
assert(value >= 0 && value <= 6);
if (value == headingLevel) {
return;
}
_headingLevel = value;
_hasBeenAnnotated = true;
}

/// Whether the owning [RenderObject] is a slider (true) or not (false).
bool get isSlider => _hasFlag(SemanticsFlag.isSlider);
set isSlider(bool value) {
Expand Down Expand Up @@ -5044,7 +5097,8 @@ class SemanticsConfiguration {
.._currentValueLength = _currentValueLength
.._actions.addAll(_actions)
.._customSemanticsActions.addAll(_customSemanticsActions)
..isBlockingUserActions = isBlockingUserActions;
..isBlockingUserActions = isBlockingUserActions
.._headingLevel = _headingLevel;
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/lib/src/widgets/basic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7101,6 +7101,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool? keyboardKey,
bool? link,
bool? header,
int? headingLevel,
bool? textField,
bool? readOnly,
bool? focusable,
Expand Down Expand Up @@ -7172,6 +7173,7 @@ class Semantics extends SingleChildRenderObjectWidget {
keyboardKey: keyboardKey,
link: link,
header: header,
headingLevel: headingLevel,
textField: textField,
readOnly: readOnly,
focusable: focusable,
Expand Down
6 changes: 6 additions & 0 deletions packages/flutter_test/test/matchers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ void main() {
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
currentValueLength: 10,
maxValueLength: 15,
headingLevel: 0,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);

Expand Down Expand Up @@ -970,6 +971,7 @@ void main() {
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
currentValueLength: 10,
maxValueLength: 15,
headingLevel: 0,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);

Expand Down Expand Up @@ -1062,6 +1064,7 @@ void main() {
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
headingLevel: 0,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);

Expand Down Expand Up @@ -1161,6 +1164,7 @@ void main() {
platformViewId: 105,
currentValueLength: 10,
maxValueLength: 15,
headingLevel: 0,
);
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);

Expand Down Expand Up @@ -1189,6 +1193,7 @@ void main() {
currentValueLength: 10,
maxValueLength: 15,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
headingLevel: 0,
);
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);

Expand Down Expand Up @@ -1279,6 +1284,7 @@ void main() {
currentValueLength: 10,
maxValueLength: 15,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
headingLevel: 0,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);

Expand Down

0 comments on commit 7d525ea

Please sign in to comment.