diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index b3650f2d5f..371f392e64 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -42685,6 +42685,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../fl ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE 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/expandable.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 @@ -45617,6 +45618,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart 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/expandable.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 diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index 9fd76751e4..23794f5236 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -105,6 +105,7 @@ export 'engine/scene_painting.dart'; export 'engine/scene_view.dart'; export 'engine/semantics/accessibility.dart'; export 'engine/semantics/checkable.dart'; +export 'engine/semantics/expandable.dart'; export 'engine/semantics/focusable.dart'; export 'engine/semantics/header.dart'; export 'engine/semantics/heading.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart index ff2726bae5..f6b9cff1ed 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart @@ -4,6 +4,7 @@ export 'semantics/accessibility.dart'; export 'semantics/checkable.dart'; +export 'semantics/expandable.dart'; export 'semantics/focusable.dart'; export 'semantics/header.dart'; export 'semantics/heading.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart new file mode 100644 index 0000000000..cdd841839e --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart @@ -0,0 +1,27 @@ +// 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 'semantics.dart'; + +/// Adds expandability behavior to a semantic node. +/// +/// An expandable node would have the `aria-expanded` attribute set to "true" if the node +/// is currently expanded (i.e. [SemanticsObject.isExpanded] is true), and set +/// to "false" if it's not expanded (i.e. [SemanticsObject.isExpanded] is +/// false). If the node is not expandable (i.e. [SemanticsObject.isExpandable] +/// is false), then `aria-expanded` is unset. +class Expandable extends SemanticBehavior { + Expandable(super.semanticsObject, super.owner); + + @override + void update() { + if (semanticsObject.isFlagsDirty) { + if (semanticsObject.isExpandable) { + owner.setAttribute('aria-expanded', semanticsObject.isExpanded); + } else { + owner.removeAttribute('aria-expanded'); + } + } + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 46d732f91d..f4dc7c64c9 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -20,6 +20,7 @@ import '../vector_math.dart'; import '../window.dart'; import 'accessibility.dart'; import 'checkable.dart'; +import 'expandable.dart'; import 'focusable.dart'; import 'header.dart'; import 'heading.dart'; @@ -461,6 +462,7 @@ abstract class SemanticRole { addRouteName(); addLabelAndValue(preferredRepresentation: preferredLabelRepresentation); addSelectableBehavior(); + addExpandableBehavior(); } /// Initializes a blank role for a [semanticsObject]. @@ -627,6 +629,10 @@ abstract class SemanticRole { } } + void addExpandableBehavior() { + addSemanticBehavior(Expandable(semanticsObject, this)); + } + /// Adds a semantic behavior to this role. /// /// This method should be called by concrete implementations of @@ -1947,6 +1953,19 @@ class SemanticsObject { /// selected. bool get isSelected => hasFlag(ui.SemanticsFlag.isSelected); + /// If true, this node represents something that can be annotated as + /// "expanded", such as a expansion tile or drop down menu + /// + /// Expandability is managed by `aria-expanded`. + /// + /// See also: + /// + /// * [isExpanded], which indicates whether the node is currently selected. + bool get isExpandable => hasFlag(ui.SemanticsFlag.hasExpandedState); + + /// Indicates whether the node is currently expanded. + bool get isExpanded => hasFlag(ui.SemanticsFlag.isExpanded); + /// Role-specific adjustment of the vertical position of the child container. /// /// This is used, for example, by the [SemanticScrollable] to compensate for the diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 9a35c301d7..13d60d1c0e 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -86,6 +86,9 @@ void runSemanticsTests() { group('selectables', () { _testSelectables(); }); + group('expandables', () { + _testExpandables(); + }); group('tappable', () { _testTappable(); }); @@ -2328,6 +2331,77 @@ void _testSelectables() { }); } +void _testExpandables() { + test('renders and updates non-expandable, expanded, and unexpanded nodes', () async { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + final tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + rect: const ui.Rect.fromLTRB(0, 0, 100, 60), + children: [ + tester.updateNode(id: 1, isSelectable: false, rect: const ui.Rect.fromLTRB(0, 0, 100, 20)), + tester.updateNode( + id: 2, + isExpandable: true, + isExpanded: false, + rect: const ui.Rect.fromLTRB(0, 20, 100, 40), + ), + tester.updateNode( + id: 3, + isExpandable: true, + isExpanded: true, + rect: const ui.Rect.fromLTRB(0, 40, 100, 60), + ), + ], + ); + tester.apply(); + + expectSemanticsTree(owner(), ''' + + + + + + + +'''); + + // Missing attributes cannot be expressed using HTML patterns, so check directly. + final nonExpandable = owner().debugSemanticsTree![1]!.element; + expect(nonExpandable.getAttribute('aria-expanded'), isNull); + + // Flip the values and check that that ARIA attribute is updated. + tester.updateNode( + id: 2, + isExpandable: true, + isExpanded: true, + rect: const ui.Rect.fromLTRB(0, 20, 100, 40), + ); + tester.updateNode( + id: 3, + isExpandable: true, + isExpanded: false, + rect: const ui.Rect.fromLTRB(0, 40, 100, 60), + ); + tester.apply(); + + expectSemanticsTree(owner(), ''' + + + + + + + +'''); + + semantics().semanticsEnabled = false; + }); +} + void _testTappable() { test('renders an enabled button', () async { semantics() diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart index c4644605db..5932dbd4bb 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -34,6 +34,8 @@ class SemanticsTester { bool? isChecked, bool? isSelectable, bool? isSelected, + bool? isExpandable, + bool? isExpanded, bool? isButton, bool? isLink, bool? isTextField, @@ -130,6 +132,12 @@ class SemanticsTester { if (isSelected ?? false) { flags |= ui.SemanticsFlag.isSelected.index; } + if (isExpandable ?? false) { + flags |= ui.SemanticsFlag.hasExpandedState.index; + } + if (isExpanded ?? false) { + flags |= ui.SemanticsFlag.isExpanded.index; + } if (isButton ?? false) { flags |= ui.SemanticsFlag.isButton.index; }