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

Commit a1d5bdf

Browse files
committed
empty labeled header is a heading
1 parent 1cf381c commit a1d5bdf

3 files changed

Lines changed: 68 additions & 38 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class SemanticHeading extends SemanticRole {
2020

2121
@override
2222
DomElement createElement() {
23-
final element = createDomElement('h${semanticsObject.headingLevel}');
23+
final element = createDomElement('h${semanticsObject.effectiveHeadingLevel}');
2424
element.style
2525
// Browser adds default non-zero margins/paddings to <h*> tags, which
2626
// affects the size of the element. As the element size is fully defined

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

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,11 +1122,23 @@ class SemanticsObject {
11221122
_dirtyFields |= _platformViewIdIndex;
11231123
}
11241124

1125-
/// See [ui.SemanticsUpdateBuilder.updateNode].
1126-
int get headingLevel => _headingLevel;
1125+
// This field is not exposed publicly because code that applies heading levels
1126+
// should use [effectiveHeadingLevel] instead.
11271127
int _headingLevel = 0;
11281128

1129-
int get effectiveHeadingLevel => isHeader ? 2 : headingLevel;
1129+
/// The effective heading level value to be used when rendering this node as
1130+
/// a heading.
1131+
///
1132+
/// If a heading is rendered from a header, uses heading level 2.
1133+
int get effectiveHeadingLevel {
1134+
if (_headingLevel != 0) {
1135+
return _headingLevel;
1136+
} else {
1137+
// This branch may be taken when a heading is rendered from a header,
1138+
// where the heading level is not provided.
1139+
return 2;
1140+
}
1141+
}
11301142

11311143
static const int _headingLevelIndex = 1 << 24;
11321144

@@ -1137,6 +1149,36 @@ class SemanticsObject {
11371149
_dirtyFields |= _headingLevelIndex;
11381150
}
11391151

1152+
/// Whether this object represents a heading.
1153+
///
1154+
/// Typically, a heading is a prominent piece of text that provides a title
1155+
/// for a section in the UI.
1156+
///
1157+
/// Labeled empty headers are treated as headings too.
1158+
///
1159+
/// See also:
1160+
///
1161+
/// * [isHeader], which also describes the rest of the screen, and is
1162+
/// sometimes presented to the user as a heading.
1163+
bool get isHeading => _headingLevel != 0 || isHeader && hasLabel && !hasChildren;
1164+
1165+
/// Whether this object represents a header.
1166+
///
1167+
/// A header is used for one of two purposes:
1168+
///
1169+
/// * Introduce the content of the main screen or a page. In this case, the
1170+
/// header is a, possibly labeled, container of widgets that together
1171+
/// provide the description of the screen.
1172+
/// * Provide a heading (like [isHeading]). Native mobile apps do not have a
1173+
/// notion of "heading". It is common to mark headings as headers instead
1174+
/// and the screen readers will announce "heading". Labeled empty headers
1175+
/// are treated as heading by the web engine.
1176+
///
1177+
/// See also:
1178+
///
1179+
/// * [isHeading], which determines whether this node represents a heading.
1180+
bool get isHeader => hasFlag(ui.SemanticsFlag.isHeader);
1181+
11401182
/// See [ui.SemanticsUpdateBuilder.updateNode].
11411183
String? get identifier => _identifier;
11421184
String? _identifier;
@@ -1272,39 +1314,9 @@ class SemanticsObject {
12721314
/// Whether this object represents an editable text field.
12731315
bool get isTextField => hasFlag(ui.SemanticsFlag.isTextField);
12741316

1275-
/// Whether this object represents a heading.
1276-
///
1277-
/// Typically, a heading is a prominent piece of text that provides a title
1278-
/// for a section in the UI.
1279-
///
1280-
/// Labeled empty headers are treated as headings too.
1281-
///
1282-
/// See also:
1283-
///
1284-
/// * [isHeader], which also describes the rest of the screen, and is
1285-
/// sometimes presented to the user as a heading.
1286-
bool get isHeading => headingLevel != 0 || isHeader && hasLabel && !hasChildren;
1287-
12881317
/// Whether this object represents an interactive link.
12891318
bool get isLink => hasFlag(ui.SemanticsFlag.isLink);
12901319

1291-
/// Whether this object represents a header.
1292-
///
1293-
/// A header is used for one of two purposes:
1294-
///
1295-
/// * Introduce the content of the main screen or a page. In this case, the
1296-
/// header is a, possibly labeled, container of widgets that together
1297-
/// provide the description of the screen.
1298-
/// * Provide a heading (like [isHeading]). Native mobile apps do not have a
1299-
/// notion of "heading". It is common to mark headings as headers instead
1300-
/// and the screen readers will announce "heading". Labeled empty headers
1301-
/// are treated as heading by the web engine.
1302-
///
1303-
/// See also:
1304-
///
1305-
/// * [isHeading], which determines whether this node represents a heading.
1306-
bool get isHeader => hasFlag(ui.SemanticsFlag.isHeader);
1307-
13081320
/// Whether this object needs screen readers attention right away.
13091321
bool get isLiveRegion =>
13101322
hasFlag(ui.SemanticsFlag.isLiveRegion) &&

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ class MockSemanticsEnabler implements SemanticsEnabler {
742742
}
743743

744744
void _testHeader() {
745-
test('renders a header with a label and uses a sized span for label', () {
745+
test('renders an empty labeled header as a heading with a label and uses a sized span for label', () {
746746
semantics()
747747
..debugOverrideTimestampFunction(() => _testTime)
748748
..semanticsEnabled = true;
@@ -757,9 +757,27 @@ void _testHeader() {
757757
);
758758

759759
owner().updateSemantics(builder.build());
760-
expectSemanticsTree(owner(), '''
761-
<header><span>Header of the page</span></header>
762-
''');
760+
expectSemanticsTree(owner(), '<h2>Header of the page</span></h2>');
761+
762+
semantics().semanticsEnabled = false;
763+
});
764+
765+
// This is a useless case, but we should at least not crash if it happens.
766+
test('renders an empty unlabeled header', () {
767+
semantics()
768+
..debugOverrideTimestampFunction(() => _testTime)
769+
..semanticsEnabled = true;
770+
771+
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
772+
updateNode(
773+
builder,
774+
flags: 0 | ui.SemanticsFlag.isHeader.index,
775+
transform: Matrix4.identity().toFloat64(),
776+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
777+
);
778+
779+
owner().updateSemantics(builder.build());
780+
expectSemanticsTree(owner(), '<header></header>');
763781

764782
semantics().semanticsEnabled = false;
765783
});

0 commit comments

Comments
 (0)