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> {
);
}
return child;
return MergeSemantics(child: child);
}
void _handleFocusChange() {
......@@ -1904,19 +1904,23 @@ class _SubmenuButtonState extends State<SubmenuButton> {
controller._anchor!._focusButton();
}
}
child = TextButton(
style: mergedStyle,
focusNode: _buttonFocusNode,
onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null,
onPressed: _enabled ? () => toggleShowMenu(context) : null,
isSemanticButton: null,
child: _MenuItemLabel(
leadingIcon: widget.leadingIcon,
trailingIcon: widget.trailingIcon,
hasSubmenu: true,
showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical,
child: child ?? const SizedBox(),
child = MergeSemantics(
child: Semantics(
expanded: controller.isOpen,
child: TextButton(
style: mergedStyle,
focusNode: _buttonFocusNode,
onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null,
onPressed: _enabled ? () => toggleShowMenu(context) : null,
isSemanticButton: null,
child: _MenuItemLabel(
leadingIcon: widget.leadingIcon,
trailingIcon: widget.trailingIcon,
hasSubmenu: true,
showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical,
child: child ?? const SizedBox(),
),
),
),
);
......
......@@ -900,6 +900,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.button != null) {
config.isButton = properties.button!;
}
if (properties.expanded != null) {
config.isExpanded = properties.expanded;
}
if (properties.link != null) {
config.isLink = properties.link!;
}
......
......@@ -4319,6 +4319,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (_properties.button != null) {
config.isButton = _properties.button!;
}
if (_properties.expanded != null) {
config.isExpanded = _properties.expanded;
}
if (_properties.link != null) {
config.isLink = _properties.link!;
}
......
......@@ -870,6 +870,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.enabled,
this.checked,
this.mixed,
this.expanded,
this.selected,
this.toggled,
this.button,
......@@ -964,6 +965,14 @@ class SemanticsProperties extends DiagnosticableTree {
/// This is mutually exclusive with [checked] and [toggled].
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
/// or similar widget with an "on" state, and what its current
/// state is.
......@@ -1612,6 +1621,7 @@ class SemanticsProperties extends DiagnosticableTree {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('checked', checked, 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(StringProperty('label', label, defaultValue: null));
properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null));
......@@ -4398,6 +4408,20 @@ class SemanticsConfiguration {
_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.
///
/// A disabled object does not respond to user interactions. Only objects that
......
......@@ -7138,6 +7138,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool? hidden,
bool? image,
bool? liveRegion,
bool? expanded,
int? maxValueLength,
int? currentValueLength,
String? label,
......@@ -7186,6 +7187,7 @@ class Semantics extends SingleChildRenderObjectWidget {
enabled: enabled,
checked: checked,
mixed: mixed,
expanded: expanded,
toggled: toggled,
selected: selected,
button: button,
......
......@@ -3181,7 +3181,8 @@ void main() {
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState],
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState,
SemanticsFlag.hasExpandedState],
label: 'ABC',
textDirection: TextDirection.ltr,
),
......@@ -3194,6 +3195,131 @@ void main() {
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() {
image: true,
liveRegion: true,
toggled: true,
expanded: true,
),
),
),
......@@ -494,6 +495,7 @@ void _defineTests() {
namesRoute: true,
image: true,
liveRegion: true,
expanded: true,
),
),
),
......@@ -520,7 +522,7 @@ void _defineTests() {
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
}, skip: true); // https://github.com/flutter/flutter/issues/127617
});
group('diffing', () {
testWidgets('complains about duplicate keys', (WidgetTester tester) async {
......
......@@ -609,6 +609,7 @@ void main() {
namesRoute: true,
image: true,
liveRegion: true,
expanded: true,
),
);
final List<SemanticsFlag> flags = SemanticsFlag.values.toList();
......@@ -691,6 +692,7 @@ void main() {
namesRoute: true,
image: true,
liveRegion: true,
expanded: true,
),
);
flags
......@@ -706,7 +708,7 @@ void main() {
],
);
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 {
final SemanticsTester semantics = SemanticsTester(tester);
......
......@@ -564,6 +564,8 @@ Matcher matchesSemantics({
bool hasToggledState = false,
bool isToggled = false,
bool hasImplicitScrolling = false,
bool hasExpandedState = false,
bool isExpanded = false,
// Actions //
bool hasTapAction = false,
bool hasLongPressAction = false,
......@@ -640,6 +642,8 @@ Matcher matchesSemantics({
hasToggledState: hasToggledState,
isToggled: isToggled,
hasImplicitScrolling: hasImplicitScrolling,
hasExpandedState: hasExpandedState,
isExpanded: isExpanded,
// Actions
hasTapAction: hasTapAction,
hasLongPressAction: hasLongPressAction,
......@@ -737,6 +741,8 @@ Matcher containsSemantics({
bool? hasToggledState,
bool? isToggled,
bool? hasImplicitScrolling,
bool? hasExpandedState,
bool? isExpanded,
// Actions
bool? hasTapAction,
bool? hasLongPressAction,
......@@ -813,6 +819,8 @@ Matcher containsSemantics({
hasToggledState: hasToggledState,
isToggled: isToggled,
hasImplicitScrolling: hasImplicitScrolling,
hasExpandedState: hasExpandedState,
isExpanded: isExpanded,
// Actions
hasTapAction: hasTapAction,
hasLongPressAction: hasLongPressAction,
......@@ -2111,6 +2119,8 @@ class _MatchesSemanticsData extends Matcher {
required bool? hasToggledState,
required bool? isToggled,
required bool? hasImplicitScrolling,
required bool? hasExpandedState,
required bool? isExpanded,
// Actions
required bool? hasTapAction,
required bool? hasLongPressAction,
......@@ -2166,6 +2176,8 @@ class _MatchesSemanticsData extends Matcher {
if (isToggled != null) SemanticsFlag.isToggled: isToggled,
if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling,
if (isSlider != null) SemanticsFlag.isSlider: isSlider,
if (hasExpandedState != null) SemanticsFlag.hasExpandedState: hasExpandedState,
if (isExpanded != null) SemanticsFlag.isExpanded: isExpanded,
},
actions = <SemanticsAction, bool>{
if (hasTapAction != null) SemanticsAction.tap: hasTapAction,
......
......@@ -683,6 +683,8 @@ void main() {
hasToggledState: true,
isToggled: true,
hasImplicitScrolling: true,
hasExpandedState: true,
isExpanded: true,
/* Actions */
hasTapAction: true,
hasLongPressAction: true,
......@@ -966,6 +968,8 @@ void main() {
hasToggledState: true,
isToggled: true,
hasImplicitScrolling: true,
hasExpandedState: true,
isExpanded: true,
/* Actions */
hasTapAction: true,
hasLongPressAction: true,
......@@ -1055,6 +1059,8 @@ void main() {
hasToggledState: false,
isToggled: false,
hasImplicitScrolling: false,
hasExpandedState: false,
isExpanded: false,
/* Actions */
hasTapAction: 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