Skip to content

Commit

Permalink
Reapply "[web:a11y] make header a proper <header> (flutter#55747)" (f…
Browse files Browse the repository at this point in the history
…lutter#55993)

This reverts commit d302cc9.
  • Loading branch information
yjbanov committed Nov 6, 2024
1 parent 78f9dcf commit fdcf682
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 20 deletions.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -43885,6 +43885,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flu
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/header.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -46752,6 +46753,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/header.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export 'engine/scene_view.dart';
export 'engine/semantics/accessibility.dart';
export 'engine/semantics/checkable.dart';
export 'engine/semantics/focusable.dart';
export 'engine/semantics/header.dart';
export 'engine/semantics/heading.dart';
export 'engine/semantics/image.dart';
export 'engine/semantics/incrementable.dart';
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export 'semantics/accessibility.dart';
export 'semantics/checkable.dart';
export 'semantics/focusable.dart';
export 'semantics/header.dart';
export 'semantics/heading.dart';
export 'semantics/image.dart';
export 'semantics/incrementable.dart';
Expand Down
44 changes: 44 additions & 0 deletions lib/web_ui/lib/src/engine/semantics/header.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../dom.dart';
import 'label_and_value.dart';
import 'semantics.dart';

/// Renders a semantic header.
///
/// A header is a group of nodes that together introduce the content of the
/// current screen or page.
///
/// Uses the `<header>` element, which implies ARIA role "banner".
///
/// See also:
/// * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role
class SemanticHeader extends SemanticRole {
SemanticHeader(SemanticsObject semanticsObject) : super.withBasics(
SemanticRoleKind.header,
semanticsObject,

// Why use sizedSpan?
//
// On an empty <header> aria-label alone will read the label but also add
// "empty banner". Additionally, if the label contains information that's
// meant to be crawlable, it will be lost by moving into aria-label, because
// most crawlers ignore ARIA labels.
//
// Using DOM text, such as <header>DOM text</header> causes the browser to
// generate two a11y nodes, one for the <header> element, and one for the
// "DOM text" text node. The text node is sized according to the text size,
// and does not match the size of the <header> element, which is the same
// issue as https://github.com/flutter/flutter/issues/146774.
preferredLabelRepresentation: LabelRepresentation.sizedSpan,
);

@override
DomElement createElement() => createDomElement('header');

@override
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
}
35 changes: 25 additions & 10 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import '../window.dart';
import 'accessibility.dart';
import 'checkable.dart';
import 'focusable.dart';
import 'header.dart';
import 'heading.dart';
import 'image.dart';
import 'incrementable.dart';
Expand Down Expand Up @@ -396,14 +397,17 @@ enum SemanticRoleKind {
/// The node's role is to host a platform view.
platformView,

/// Contains a link.
link,

/// Denotes a header.
header,

/// A role used when a more specific role cannot be assigend to
/// a [SemanticsObject].
///
/// Provides a label or a value.
generic,

/// Contains a link.
link,
}

/// Responsible for setting the `role` ARIA attribute, for attaching
Expand Down Expand Up @@ -688,23 +692,18 @@ final class GenericRole extends SemanticRole {
return;
}

// Assign one of three roles to the element: group, heading, text.
// Assign one of two roles to the element: group or text.
//
// - "group" is used when the node has children, irrespective of whether the
// node is marked as a header or not. This is because marking a group
// as a "heading" will prevent the AT from reaching its children.
// - "heading" is used when the framework explicitly marks the node as a
// heading and the node does not have children.
// - If a node has a label and no children, assume is a paragraph of text.
// In HTML text has no ARIA role. It's just a DOM node with text inside
// it. Previously, role="text" was used, but it was only supported by
// Safari, and it was removed starting Safari 17.
if (semanticsObject.hasChildren) {
labelAndValue!.preferredRepresentation = LabelRepresentation.ariaLabel;
setAriaRole('group');
} else if (semanticsObject.hasFlag(ui.SemanticsFlag.isHeader)) {
labelAndValue!.preferredRepresentation = LabelRepresentation.domText;
setAriaRole('heading');
} else {
labelAndValue!.preferredRepresentation = LabelRepresentation.sizedSpan;
removeAttribute('role');
Expand Down Expand Up @@ -1272,11 +1271,24 @@ class SemanticsObject {
bool get isTextField => hasFlag(ui.SemanticsFlag.isTextField);

/// Whether this object represents a heading element.
///
/// Typically, a heading is a prominent piece of text that describes what the
/// rest of the screen or page is about.
///
/// Not to be confused with [isHeader].
bool get isHeading => headingLevel != 0;

/// Whether this object represents an editable text field.
/// Whether this object represents an interactive link.
bool get isLink => hasFlag(ui.SemanticsFlag.isLink);

/// Whether this object represents a header.
///
/// A header is a group of widgets that introduce the content of the screen
/// or a page.
///
/// Not to be confused with [isHeading].
bool get isHeader => hasFlag(ui.SemanticsFlag.isHeader);

/// Whether this object needs screen readers attention right away.
bool get isLiveRegion =>
hasFlag(ui.SemanticsFlag.isLiveRegion) &&
Expand Down Expand Up @@ -1690,6 +1702,8 @@ class SemanticsObject {
return SemanticRoleKind.route;
} else if (isLink) {
return SemanticRoleKind.link;
} else if (isHeader) {
return SemanticRoleKind.header;
} else {
return SemanticRoleKind.generic;
}
Expand All @@ -1707,6 +1721,7 @@ class SemanticsObject {
SemanticRoleKind.platformView => SemanticPlatformView(this),
SemanticRoleKind.link => SemanticLink(this),
SemanticRoleKind.heading => SemanticHeading(this),
SemanticRoleKind.header => SemanticHeader(this),
SemanticRoleKind.generic => GenericRole(this),
};
}
Expand Down
14 changes: 4 additions & 10 deletions lib/web_ui/test/engine/semantics/semantics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ class MockSemanticsEnabler implements SemanticsEnabler {
}

void _testHeader() {
test('renders heading role for headers', () {
test('renders a header with a label and uses a sized span for label', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
Expand All @@ -758,19 +758,13 @@ void _testHeader() {

owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
<sem role="heading">Header of the page</sem>
<header><span>Header of the page</span></header>
''');

semantics().semanticsEnabled = false;
});

// When a header has child elements, role="heading" prevents AT from reaching
// child elements. To fix that role="group" is used, even though that causes
// the heading to not be announced as a heading. If the app really needs the
// heading to be announced as a heading, the developer can restructure the UI
// such that the heading is not a parent node, but a side-note, e.g. preceding
// the child list.
test('uses group role for headers when children are present', () {
test('renders a header with children and uses aria-label', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
Expand All @@ -794,7 +788,7 @@ void _testHeader() {

owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
<sem role="group" aria-label="Header of the page"><sem-c><sem></sem></sem-c></sem>
<header aria-label="Header of the page"><sem-c><sem></sem></sem-c></header>
''');

semantics().semanticsEnabled = false;
Expand Down

0 comments on commit fdcf682

Please sign in to comment.