Add Expanded/Collapsed State for Semantics (#131233)
This commit is contained in:
parent
e0b6b6c451
commit
71d96ddf9c
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user