Skip to content

Commit

Permalink
Wires up expanded state in web engine (flutter#164048)
Browse files Browse the repository at this point in the history
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

fixes flutter#162141

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Mouad Debbar <[email protected]>
  • Loading branch information
chunhtai and mdebbar authored Feb 27, 2025
1 parent b8f274d commit 37ff5a3
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 0 deletions.
2 changes: 2 additions & 0 deletions engine/src/flutter/ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions engine/src/flutter/lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -461,6 +462,7 @@ abstract class SemanticRole {
addRouteName();
addLabelAndValue(preferredRepresentation: preferredLabelRepresentation);
addSelectableBehavior();
addExpandableBehavior();
}

/// Initializes a blank role for a [semanticsObject].
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ void runSemanticsTests() {
group('selectables', () {
_testSelectables();
});
group('expandables', () {
_testExpandables();
});
group('tappable', () {
_testTappable();
});
Expand Down Expand Up @@ -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: <SemanticsNodeUpdate>[
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(), '''
<sem>
<sem-c>
<sem></sem>
<sem aria-expanded="false"></sem>
<sem aria-expanded="true"></sem>
</sem-c>
</sem>
''');

// 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(), '''
<sem>
<sem-c>
<sem></sem>
<sem aria-expanded="true"></sem>
<sem aria-expanded="false"></sem>
</sem-c>
</sem>
''');

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

void _testTappable() {
test('renders an enabled button', () async {
semantics()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class SemanticsTester {
bool? isChecked,
bool? isSelectable,
bool? isSelected,
bool? isExpandable,
bool? isExpanded,
bool? isButton,
bool? isLink,
bool? isTextField,
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 37ff5a3

Please sign in to comment.