Unverified Commit 71d96ddf authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Add Expanded/Collapsed State for Semantics (#131233)

parent e0b6b6c4
...@@ -1079,7 +1079,7 @@ class _MenuItemButtonState extends State<MenuItemButton> { ...@@ -1079,7 +1079,7 @@ class _MenuItemButtonState extends State<MenuItemButton> {
); );
} }
return child; return MergeSemantics(child: child);
} }
void _handleFocusChange() { void _handleFocusChange() {
...@@ -1904,8 +1904,10 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1904,8 +1904,10 @@ class _SubmenuButtonState extends State<SubmenuButton> {
controller._anchor!._focusButton(); controller._anchor!._focusButton();
} }
} }
child = MergeSemantics(
child = TextButton( child: Semantics(
expanded: controller.isOpen,
child: TextButton(
style: mergedStyle, style: mergedStyle,
focusNode: _buttonFocusNode, focusNode: _buttonFocusNode,
onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null, onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null,
...@@ -1918,6 +1920,8 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1918,6 +1920,8 @@ class _SubmenuButtonState extends State<SubmenuButton> {
showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical, showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical,
child: child ?? const SizedBox(), child: child ?? const SizedBox(),
), ),
),
),
); );
if (_enabled && _platformSupportsAccelerators) { if (_enabled && _platformSupportsAccelerators) {
......
...@@ -900,6 +900,9 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -900,6 +900,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.button != null) { if (properties.button != null) {
config.isButton = properties.button!; config.isButton = properties.button!;
} }
if (properties.expanded != null) {
config.isExpanded = properties.expanded;
}
if (properties.link != null) { if (properties.link != null) {
config.isLink = properties.link!; config.isLink = properties.link!;
} }
......
...@@ -4319,6 +4319,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -4319,6 +4319,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (_properties.button != null) { if (_properties.button != null) {
config.isButton = _properties.button!; config.isButton = _properties.button!;
} }
if (_properties.expanded != null) {
config.isExpanded = _properties.expanded;
}
if (_properties.link != null) { if (_properties.link != null) {
config.isLink = _properties.link!; config.isLink = _properties.link!;
} }
......
...@@ -870,6 +870,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -870,6 +870,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.enabled, this.enabled,
this.checked, this.checked,
this.mixed, this.mixed,
this.expanded,
this.selected, this.selected,
this.toggled, this.toggled,
this.button, this.button,
...@@ -964,6 +965,14 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -964,6 +965,14 @@ class SemanticsProperties extends DiagnosticableTree {
/// This is mutually exclusive with [checked] and [toggled]. /// This is mutually exclusive with [checked] and [toggled].
final bool? mixed; final bool? mixed;
/// If non-null, indicates that this subtree represents something
/// that can be in an "expanded" or "collapsed" state.
///
/// For example, if a [SubmenuButton] is opened, this property
/// should be set to true; otherwise, this property should be
/// false.
final bool? expanded;
/// If non-null, indicates that this subtree represents a toggle switch /// If non-null, indicates that this subtree represents a toggle switch
/// or similar widget with an "on" state, and what its current /// or similar widget with an "on" state, and what its current
/// state is. /// state is.
...@@ -1612,6 +1621,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -1612,6 +1621,7 @@ class SemanticsProperties extends DiagnosticableTree {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('mixed', mixed, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('mixed', mixed, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('expanded', expanded, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
properties.add(StringProperty('label', label, defaultValue: null)); properties.add(StringProperty('label', label, defaultValue: null));
properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null)); properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null));
...@@ -4398,6 +4408,20 @@ class SemanticsConfiguration { ...@@ -4398,6 +4408,20 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isSelected, value); _setFlag(SemanticsFlag.isSelected, value);
} }
/// If this node has Boolean state that can be controlled by the user, whether
/// that state is expanded or collapsed, corresponding to true and false, respectively.
///
/// Do not call the setter for this field if the owning [RenderObject] doesn't
/// have expanded/collapsed state that can be controlled by the user.
///
/// The getter returns null if the owning [RenderObject] does not have
/// expanded/collapsed state.
bool? get isExpanded => _hasFlag(SemanticsFlag.hasExpandedState) ? _hasFlag(SemanticsFlag.isExpanded) : null;
set isExpanded(bool? value) {
_setFlag(SemanticsFlag.hasExpandedState, true);
_setFlag(SemanticsFlag.isExpanded, value!);
}
/// Whether the owning [RenderObject] is currently enabled. /// Whether the owning [RenderObject] is currently enabled.
/// ///
/// A disabled object does not respond to user interactions. Only objects that /// A disabled object does not respond to user interactions. Only objects that
......
...@@ -7138,6 +7138,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7138,6 +7138,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool? hidden, bool? hidden,
bool? image, bool? image,
bool? liveRegion, bool? liveRegion,
bool? expanded,
int? maxValueLength, int? maxValueLength,
int? currentValueLength, int? currentValueLength,
String? label, String? label,
...@@ -7186,6 +7187,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7186,6 +7187,7 @@ class Semantics extends SingleChildRenderObjectWidget {
enabled: enabled, enabled: enabled,
checked: checked, checked: checked,
mixed: mixed, mixed: mixed,
expanded: expanded,
toggled: toggled, toggled: toggled,
selected: selected, selected: selected,
button: button, button: button,
......
...@@ -3181,7 +3181,8 @@ void main() { ...@@ -3181,7 +3181,8 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState], flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState,
SemanticsFlag.hasExpandedState],
label: 'ABC', label: 'ABC',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
...@@ -3194,6 +3195,131 @@ void main() { ...@@ -3194,6 +3195,131 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('SubmenuButton expanded/collapsed state', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SubmenuButton(
onHover: (bool value) {},
style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
menuChildren: <Widget>[
MenuItemButton(
child: const Text('Item 0'),
onPressed: () {},
),
],
child: const Text('ABC'),
),
),
),
);
// Test expanded state.
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [
TestSemantics(
id: 2,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [
TestSemantics(
id: 3,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag> [SemanticsFlag.scopesRoute],
children: <TestSemantics> [
TestSemantics(
id: 4,
flags: <SemanticsFlag>[SemanticsFlag.hasExpandedState, SemanticsFlag.isExpanded, SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
)
]
)
]
),
TestSemantics(
id: 6,
rect: const Rect.fromLTRB(0.0, 0.0, 123.0, 64.0),
children: <TestSemantics> [
TestSemantics(
id: 7,
rect: const Rect.fromLTRB(0.0, 0.0, 123.0, 48.0),
flags: <SemanticsFlag> [SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics> [
TestSemantics(
id: 8,
label: 'Item 0',
rect: const Rect.fromLTRB(0.0, 0.0, 123.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
),
],
),
],
),
],
),
],
),
ignoreTransform: true,
),
);
// Test collapsed state.
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [
TestSemantics(
id: 2,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [
TestSemantics(
id: 3,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag> [SemanticsFlag.scopesRoute],
children: <TestSemantics> [
TestSemantics(
id: 4,
flags: <SemanticsFlag>[SemanticsFlag.hasExpandedState, SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
)
]
)
]
),
],
),
],
),
ignoreTransform: true,
),
);
semantics.dispose();
});
}); });
} }
......
...@@ -440,6 +440,7 @@ void _defineTests() { ...@@ -440,6 +440,7 @@ void _defineTests() {
image: true, image: true,
liveRegion: true, liveRegion: true,
toggled: true, toggled: true,
expanded: true,
), ),
), ),
), ),
...@@ -494,6 +495,7 @@ void _defineTests() { ...@@ -494,6 +495,7 @@ void _defineTests() {
namesRoute: true, namesRoute: true,
image: true, image: true,
liveRegion: true, liveRegion: true,
expanded: true,
), ),
), ),
), ),
...@@ -520,7 +522,7 @@ void _defineTests() { ...@@ -520,7 +522,7 @@ void _defineTests() {
); );
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
semantics.dispose(); semantics.dispose();
}, skip: true); // https://github.com/flutter/flutter/issues/127617 });
group('diffing', () { group('diffing', () {
testWidgets('complains about duplicate keys', (WidgetTester tester) async { testWidgets('complains about duplicate keys', (WidgetTester tester) async {
......
...@@ -609,6 +609,7 @@ void main() { ...@@ -609,6 +609,7 @@ void main() {
namesRoute: true, namesRoute: true,
image: true, image: true,
liveRegion: true, liveRegion: true,
expanded: true,
), ),
); );
final List<SemanticsFlag> flags = SemanticsFlag.values.toList(); final List<SemanticsFlag> flags = SemanticsFlag.values.toList();
...@@ -691,6 +692,7 @@ void main() { ...@@ -691,6 +692,7 @@ void main() {
namesRoute: true, namesRoute: true,
image: true, image: true,
liveRegion: true, liveRegion: true,
expanded: true,
), ),
); );
flags flags
...@@ -706,7 +708,7 @@ void main() { ...@@ -706,7 +708,7 @@ void main() {
], ],
); );
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
}, skip: true); // https://github.com/flutter/flutter/issues/127617 });
testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async { testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
......
...@@ -564,6 +564,8 @@ Matcher matchesSemantics({ ...@@ -564,6 +564,8 @@ Matcher matchesSemantics({
bool hasToggledState = false, bool hasToggledState = false,
bool isToggled = false, bool isToggled = false,
bool hasImplicitScrolling = false, bool hasImplicitScrolling = false,
bool hasExpandedState = false,
bool isExpanded = false,
// Actions // // Actions //
bool hasTapAction = false, bool hasTapAction = false,
bool hasLongPressAction = false, bool hasLongPressAction = false,
...@@ -640,6 +642,8 @@ Matcher matchesSemantics({ ...@@ -640,6 +642,8 @@ Matcher matchesSemantics({
hasToggledState: hasToggledState, hasToggledState: hasToggledState,
isToggled: isToggled, isToggled: isToggled,
hasImplicitScrolling: hasImplicitScrolling, hasImplicitScrolling: hasImplicitScrolling,
hasExpandedState: hasExpandedState,
isExpanded: isExpanded,
// Actions // Actions
hasTapAction: hasTapAction, hasTapAction: hasTapAction,
hasLongPressAction: hasLongPressAction, hasLongPressAction: hasLongPressAction,
...@@ -737,6 +741,8 @@ Matcher containsSemantics({ ...@@ -737,6 +741,8 @@ Matcher containsSemantics({
bool? hasToggledState, bool? hasToggledState,
bool? isToggled, bool? isToggled,
bool? hasImplicitScrolling, bool? hasImplicitScrolling,
bool? hasExpandedState,
bool? isExpanded,
// Actions // Actions
bool? hasTapAction, bool? hasTapAction,
bool? hasLongPressAction, bool? hasLongPressAction,
...@@ -813,6 +819,8 @@ Matcher containsSemantics({ ...@@ -813,6 +819,8 @@ Matcher containsSemantics({
hasToggledState: hasToggledState, hasToggledState: hasToggledState,
isToggled: isToggled, isToggled: isToggled,
hasImplicitScrolling: hasImplicitScrolling, hasImplicitScrolling: hasImplicitScrolling,
hasExpandedState: hasExpandedState,
isExpanded: isExpanded,
// Actions // Actions
hasTapAction: hasTapAction, hasTapAction: hasTapAction,
hasLongPressAction: hasLongPressAction, hasLongPressAction: hasLongPressAction,
...@@ -2111,6 +2119,8 @@ class _MatchesSemanticsData extends Matcher { ...@@ -2111,6 +2119,8 @@ class _MatchesSemanticsData extends Matcher {
required bool? hasToggledState, required bool? hasToggledState,
required bool? isToggled, required bool? isToggled,
required bool? hasImplicitScrolling, required bool? hasImplicitScrolling,
required bool? hasExpandedState,
required bool? isExpanded,
// Actions // Actions
required bool? hasTapAction, required bool? hasTapAction,
required bool? hasLongPressAction, required bool? hasLongPressAction,
...@@ -2166,6 +2176,8 @@ class _MatchesSemanticsData extends Matcher { ...@@ -2166,6 +2176,8 @@ class _MatchesSemanticsData extends Matcher {
if (isToggled != null) SemanticsFlag.isToggled: isToggled, if (isToggled != null) SemanticsFlag.isToggled: isToggled,
if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling, if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling,
if (isSlider != null) SemanticsFlag.isSlider: isSlider, if (isSlider != null) SemanticsFlag.isSlider: isSlider,
if (hasExpandedState != null) SemanticsFlag.hasExpandedState: hasExpandedState,
if (isExpanded != null) SemanticsFlag.isExpanded: isExpanded,
}, },
actions = <SemanticsAction, bool>{ actions = <SemanticsAction, bool>{
if (hasTapAction != null) SemanticsAction.tap: hasTapAction, if (hasTapAction != null) SemanticsAction.tap: hasTapAction,
......
...@@ -683,6 +683,8 @@ void main() { ...@@ -683,6 +683,8 @@ void main() {
hasToggledState: true, hasToggledState: true,
isToggled: true, isToggled: true,
hasImplicitScrolling: true, hasImplicitScrolling: true,
hasExpandedState: true,
isExpanded: true,
/* Actions */ /* Actions */
hasTapAction: true, hasTapAction: true,
hasLongPressAction: true, hasLongPressAction: true,
...@@ -966,6 +968,8 @@ void main() { ...@@ -966,6 +968,8 @@ void main() {
hasToggledState: true, hasToggledState: true,
isToggled: true, isToggled: true,
hasImplicitScrolling: true, hasImplicitScrolling: true,
hasExpandedState: true,
isExpanded: true,
/* Actions */ /* Actions */
hasTapAction: true, hasTapAction: true,
hasLongPressAction: true, hasLongPressAction: true,
...@@ -1055,6 +1059,8 @@ void main() { ...@@ -1055,6 +1059,8 @@ void main() {
hasToggledState: false, hasToggledState: false,
isToggled: false, isToggled: false,
hasImplicitScrolling: false, hasImplicitScrolling: false,
hasExpandedState: false,
isExpanded: false,
/* Actions */ /* Actions */
hasTapAction: false, hasTapAction: false,
hasLongPressAction: false, hasLongPressAction: false,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment