diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index cc483755af75f..362641dc9f466 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -43886,7 +43886,6 @@ 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 @@ -46765,7 +46764,6 @@ 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 diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index d2491003efc86..f50b7cf78c73e 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -147,7 +147,6 @@ 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'; diff --git a/lib/web_ui/lib/src/engine/semantics.dart b/lib/web_ui/lib/src/engine/semantics.dart index 036faceca171b..6bfca5a58571a 100644 --- a/lib/web_ui/lib/src/engine/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics.dart @@ -5,7 +5,6 @@ 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'; diff --git a/lib/web_ui/lib/src/engine/semantics/header.dart b/lib/web_ui/lib/src/engine/semantics/header.dart deleted file mode 100644 index 9de9e3d4d271f..0000000000000 --- a/lib/web_ui/lib/src/engine/semantics/header.dart +++ /dev/null @@ -1,44 +0,0 @@ -// 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 `
` 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
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
DOM text
causes the browser to - // generate two a11y nodes, one for the
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
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; -} diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index 710a068b83d1c..ea31b6356e3b1 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -21,7 +21,6 @@ import '../window.dart'; import 'accessibility.dart'; import 'checkable.dart'; import 'focusable.dart'; -import 'header.dart'; import 'heading.dart'; import 'image.dart'; import 'incrementable.dart'; @@ -397,17 +396,14 @@ 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 @@ -692,11 +688,13 @@ final class GenericRole extends SemanticRole { return; } - // Assign one of two roles to the element: group or text. + // Assign one of three roles to the element: group, heading, 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 @@ -704,6 +702,9 @@ final class GenericRole extends SemanticRole { 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'); @@ -1271,24 +1272,11 @@ 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 interactive link. + /// Whether this object represents an editable text field. 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) && @@ -1702,8 +1690,6 @@ class SemanticsObject { return SemanticRoleKind.route; } else if (isLink) { return SemanticRoleKind.link; - } else if (isHeader) { - return SemanticRoleKind.header; } else { return SemanticRoleKind.generic; } @@ -1721,7 +1707,6 @@ class SemanticsObject { SemanticRoleKind.platformView => SemanticPlatformView(this), SemanticRoleKind.link => SemanticLink(this), SemanticRoleKind.heading => SemanticHeading(this), - SemanticRoleKind.header => SemanticHeader(this), SemanticRoleKind.generic => GenericRole(this), }; } diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 36c3483c92cfc..dd9cf23b9dea7 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -739,7 +739,7 @@ class MockSemanticsEnabler implements SemanticsEnabler { } void _testHeader() { - test('renders a header with a label and uses a sized span for label', () { + test('renders heading role for headers', () { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -755,13 +755,19 @@ void _testHeader() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' -
Header of the page
+Header of the page '''); semantics().semanticsEnabled = false; }); - test('renders a header with children and uses aria-label', () { + // 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', () { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -785,7 +791,7 @@ void _testHeader() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' -
+ '''); semantics().semanticsEnabled = false;