Use tristate checkbox engine changes (#111032)
* Introduce tests for tristate checkboxes * Initial * Use tristate changes in engine * Flutter_test matchers test update * Comments, tests * Update packages/flutter/lib/src/semantics/semantics.dart Co-authored-by: Chris Bracken <chris@bracken.jp> * Assert mutual exclusivity * Assert valid state before updating state * Update packages/flutter/lib/src/semantics/semantics.dart Typo fix in comment Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> Co-authored-by: Chris Bracken <chris@bracken.jp> Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
This commit is contained in:
parent
70f6bed9ee
commit
4d3c122434
@ -474,6 +474,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
|
|||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
checked: widget.value ?? false,
|
checked: widget.value ?? false,
|
||||||
|
mixed: widget.tristate ? widget.value == null : null,
|
||||||
child: buildToggleable(
|
child: buildToggleable(
|
||||||
mouseCursor: effectiveMouseCursor,
|
mouseCursor: effectiveMouseCursor,
|
||||||
focusNode: widget.focusNode,
|
focusNode: widget.focusNode,
|
||||||
|
@ -872,6 +872,9 @@ class RenderCustomPaint extends RenderProxyBox {
|
|||||||
if (properties.checked != null) {
|
if (properties.checked != null) {
|
||||||
config.isChecked = properties.checked;
|
config.isChecked = properties.checked;
|
||||||
}
|
}
|
||||||
|
if (properties.mixed != null) {
|
||||||
|
config.isCheckStateMixed = properties.mixed;
|
||||||
|
}
|
||||||
if (properties.selected != null) {
|
if (properties.selected != null) {
|
||||||
config.isSelected = properties.selected!;
|
config.isSelected = properties.selected!;
|
||||||
}
|
}
|
||||||
|
@ -4355,6 +4355,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
|||||||
if (_properties.checked != null) {
|
if (_properties.checked != null) {
|
||||||
config.isChecked = _properties.checked;
|
config.isChecked = _properties.checked;
|
||||||
}
|
}
|
||||||
|
if (_properties.mixed != null) {
|
||||||
|
config.isCheckStateMixed = _properties.mixed;
|
||||||
|
}
|
||||||
if (_properties.toggled != null) {
|
if (_properties.toggled != null) {
|
||||||
config.isToggled = _properties.toggled;
|
config.isToggled = _properties.toggled;
|
||||||
}
|
}
|
||||||
|
@ -776,6 +776,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
|||||||
const SemanticsProperties({
|
const SemanticsProperties({
|
||||||
this.enabled,
|
this.enabled,
|
||||||
this.checked,
|
this.checked,
|
||||||
|
this.mixed,
|
||||||
this.selected,
|
this.selected,
|
||||||
this.toggled,
|
this.toggled,
|
||||||
this.button,
|
this.button,
|
||||||
@ -851,14 +852,30 @@ class SemanticsProperties extends DiagnosticableTree {
|
|||||||
/// or similar widget with a "checked" state, and what its current
|
/// or similar widget with a "checked" state, and what its current
|
||||||
/// state is.
|
/// state is.
|
||||||
///
|
///
|
||||||
/// This is mutually exclusive with [toggled].
|
/// When the [Checkbox.value] of a tristate Checkbox is null,
|
||||||
|
/// indicating a mixed-state, this value shall be false, in which
|
||||||
|
/// case, [mixed] will be true.
|
||||||
|
///
|
||||||
|
/// This is mutually exclusive with [toggled] and [mixed].
|
||||||
final bool? checked;
|
final bool? checked;
|
||||||
|
|
||||||
|
/// If non-null, indicates that this subtree represents a checkbox
|
||||||
|
/// or similar widget with a "half-checked" state or similar, and
|
||||||
|
/// whether it is currently in this half-checked state.
|
||||||
|
///
|
||||||
|
/// This must be null when [Checkbox.tristate] is false, or
|
||||||
|
/// when the widget is not a checkbox. When a tristate
|
||||||
|
/// checkbox is fully unchecked/checked, this value shall
|
||||||
|
/// be false.
|
||||||
|
///
|
||||||
|
/// This is mutually exclusive with [checked] and [toggled].
|
||||||
|
final bool? mixed;
|
||||||
|
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
/// This is mutually exclusive with [checked].
|
/// This is mutually exclusive with [checked] and [mixed].
|
||||||
final bool? toggled;
|
final bool? toggled;
|
||||||
|
|
||||||
/// If non-null indicates that this subtree represents something that can be
|
/// If non-null indicates that this subtree represents something that can be
|
||||||
@ -1490,6 +1507,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
|||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
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>('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));
|
||||||
@ -4189,10 +4207,26 @@ class SemanticsConfiguration {
|
|||||||
/// checked/unchecked state.
|
/// checked/unchecked state.
|
||||||
bool? get isChecked => _hasFlag(SemanticsFlag.hasCheckedState) ? _hasFlag(SemanticsFlag.isChecked) : null;
|
bool? get isChecked => _hasFlag(SemanticsFlag.hasCheckedState) ? _hasFlag(SemanticsFlag.isChecked) : null;
|
||||||
set isChecked(bool? value) {
|
set isChecked(bool? value) {
|
||||||
|
assert(value != true || isCheckStateMixed != true);
|
||||||
_setFlag(SemanticsFlag.hasCheckedState, true);
|
_setFlag(SemanticsFlag.hasCheckedState, true);
|
||||||
_setFlag(SemanticsFlag.isChecked, value!);
|
_setFlag(SemanticsFlag.isChecked, value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If this node has tristate that can be controlled by the user, whether
|
||||||
|
/// that state is in its mixed state.
|
||||||
|
///
|
||||||
|
/// Do not call the setter for this field if the owning [RenderObject] doesn't
|
||||||
|
/// have checked/unchecked state that can be controlled by the user.
|
||||||
|
///
|
||||||
|
/// The getter returns null if the owning [RenderObject] does not have
|
||||||
|
/// mixed checked state.
|
||||||
|
bool? get isCheckStateMixed => _hasFlag(SemanticsFlag.hasCheckedState) ? _hasFlag(SemanticsFlag.isCheckStateMixed) : null;
|
||||||
|
set isCheckStateMixed(bool? value) {
|
||||||
|
assert(value != true || isChecked != true);
|
||||||
|
_setFlag(SemanticsFlag.hasCheckedState, true);
|
||||||
|
_setFlag(SemanticsFlag.isCheckStateMixed, value!);
|
||||||
|
}
|
||||||
|
|
||||||
/// If this node has Boolean state that can be controlled by the user, whether
|
/// If this node has Boolean state that can be controlled by the user, whether
|
||||||
/// that state is on or off, corresponding to true and false, respectively.
|
/// that state is on or off, corresponding to true and false, respectively.
|
||||||
///
|
///
|
||||||
|
@ -6878,6 +6878,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
|||||||
bool excludeSemantics = false,
|
bool excludeSemantics = false,
|
||||||
bool? enabled,
|
bool? enabled,
|
||||||
bool? checked,
|
bool? checked,
|
||||||
|
bool? mixed,
|
||||||
bool? selected,
|
bool? selected,
|
||||||
bool? toggled,
|
bool? toggled,
|
||||||
bool? button,
|
bool? button,
|
||||||
@ -6943,6 +6944,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
|||||||
properties: SemanticsProperties(
|
properties: SemanticsProperties(
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
checked: checked,
|
checked: checked,
|
||||||
|
mixed: mixed,
|
||||||
toggled: toggled,
|
toggled: toggled,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
button: button,
|
button: button,
|
||||||
|
@ -139,6 +139,57 @@ void main() {
|
|||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
await tester.pumpWidget(Theme(
|
||||||
|
data: theme,
|
||||||
|
child: const Material(
|
||||||
|
child: Checkbox(
|
||||||
|
value: null,
|
||||||
|
tristate: true,
|
||||||
|
onChanged: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||||
|
hasCheckedState: true,
|
||||||
|
hasEnabledState: true,
|
||||||
|
isCheckStateMixed: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pumpWidget(Theme(
|
||||||
|
data: theme,
|
||||||
|
child: const Material(
|
||||||
|
child: Checkbox(
|
||||||
|
value: true,
|
||||||
|
tristate: true,
|
||||||
|
onChanged: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||||
|
hasCheckedState: true,
|
||||||
|
hasEnabledState: true,
|
||||||
|
isChecked: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pumpWidget(Theme(
|
||||||
|
data: theme,
|
||||||
|
child: const Material(
|
||||||
|
child: Checkbox(
|
||||||
|
value: false,
|
||||||
|
tristate: true,
|
||||||
|
onChanged: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||||
|
hasCheckedState: true,
|
||||||
|
hasEnabledState: true,
|
||||||
|
));
|
||||||
|
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -239,6 +290,7 @@ void main() {
|
|||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
SemanticsFlag.isFocusable,
|
SemanticsFlag.isFocusable,
|
||||||
|
SemanticsFlag.isCheckStateMixed,
|
||||||
],
|
],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
), hasLength(1));
|
), hasLength(1));
|
||||||
|
@ -451,7 +451,9 @@ void _defineTests() {
|
|||||||
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
|
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
|
||||||
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
|
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
|
||||||
// therefore it has to be removed.
|
// therefore it has to be removed.
|
||||||
flags.remove(SemanticsFlag.hasImplicitScrolling);
|
flags
|
||||||
|
..remove(SemanticsFlag.hasImplicitScrolling)
|
||||||
|
..remove(SemanticsFlag.isCheckStateMixed);
|
||||||
TestSemantics expectedSemantics = TestSemantics.root(
|
TestSemantics expectedSemantics = TestSemantics.root(
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics.rootChild(
|
TestSemantics.rootChild(
|
||||||
@ -475,7 +477,8 @@ void _defineTests() {
|
|||||||
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
|
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
|
||||||
properties: SemanticsProperties(
|
properties: SemanticsProperties(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
checked: true,
|
checked: false,
|
||||||
|
mixed: true,
|
||||||
toggled: true,
|
toggled: true,
|
||||||
selected: true,
|
selected: true,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@ -502,7 +505,9 @@ void _defineTests() {
|
|||||||
flags = SemanticsFlag.values.values.toList();
|
flags = SemanticsFlag.values.values.toList();
|
||||||
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
|
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
|
||||||
// therefore it has to be removed.
|
// therefore it has to be removed.
|
||||||
flags.remove(SemanticsFlag.hasImplicitScrolling);
|
flags
|
||||||
|
..remove(SemanticsFlag.hasImplicitScrolling)
|
||||||
|
..remove(SemanticsFlag.isChecked);
|
||||||
expectedSemantics = TestSemantics.root(
|
expectedSemantics = TestSemantics.root(
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics.rootChild(
|
TestSemantics.rootChild(
|
||||||
@ -519,7 +524,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); // [intended] https://github.com/flutter/flutter/issues/110107
|
});
|
||||||
|
|
||||||
group('diffing', () {
|
group('diffing', () {
|
||||||
testWidgets('complains about duplicate keys', (WidgetTester tester) async {
|
testWidgets('complains about duplicate keys', (WidgetTester tester) async {
|
||||||
|
@ -584,7 +584,8 @@ void main() {
|
|||||||
flags
|
flags
|
||||||
..remove(SemanticsFlag.hasToggledState)
|
..remove(SemanticsFlag.hasToggledState)
|
||||||
..remove(SemanticsFlag.isToggled)
|
..remove(SemanticsFlag.isToggled)
|
||||||
..remove(SemanticsFlag.hasImplicitScrolling);
|
..remove(SemanticsFlag.hasImplicitScrolling)
|
||||||
|
..remove(SemanticsFlag.isCheckStateMixed);
|
||||||
|
|
||||||
TestSemantics expectedSemantics = TestSemantics.root(
|
TestSemantics expectedSemantics = TestSemantics.root(
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
@ -631,8 +632,50 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
|
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Semantics(
|
||||||
|
key: const Key('a'),
|
||||||
|
container: true,
|
||||||
|
explicitChildNodes: true,
|
||||||
|
// flags
|
||||||
|
enabled: true,
|
||||||
|
hidden: true,
|
||||||
|
checked: false,
|
||||||
|
mixed: true,
|
||||||
|
selected: true,
|
||||||
|
button: true,
|
||||||
|
slider: true,
|
||||||
|
keyboardKey: true,
|
||||||
|
link: true,
|
||||||
|
textField: true,
|
||||||
|
readOnly: true,
|
||||||
|
focused: true,
|
||||||
|
focusable: true,
|
||||||
|
inMutuallyExclusiveGroup: true,
|
||||||
|
header: true,
|
||||||
|
obscured: true,
|
||||||
|
multiline: true,
|
||||||
|
scopesRoute: true,
|
||||||
|
namesRoute: true,
|
||||||
|
image: true,
|
||||||
|
liveRegion: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
flags
|
||||||
|
..remove(SemanticsFlag.isChecked)
|
||||||
|
..add(SemanticsFlag.isCheckStateMixed);
|
||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
}, skip: true); // [intended] https://github.com/flutter/flutter/issues/110107
|
expectedSemantics = TestSemantics.root(
|
||||||
|
children: <TestSemantics>[
|
||||||
|
TestSemantics.rootChild(
|
||||||
|
rect: TestSemantics.fullScreen,
|
||||||
|
flags: flags,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
||||||
|
@ -542,6 +542,7 @@ Matcher matchesSemantics({
|
|||||||
// Flags //
|
// Flags //
|
||||||
bool hasCheckedState = false,
|
bool hasCheckedState = false,
|
||||||
bool isChecked = false,
|
bool isChecked = false,
|
||||||
|
bool isCheckStateMixed = false,
|
||||||
bool isSelected = false,
|
bool isSelected = false,
|
||||||
bool isButton = false,
|
bool isButton = false,
|
||||||
bool isSlider = false,
|
bool isSlider = false,
|
||||||
@ -617,6 +618,7 @@ Matcher matchesSemantics({
|
|||||||
// Flags
|
// Flags
|
||||||
hasCheckedState: hasCheckedState,
|
hasCheckedState: hasCheckedState,
|
||||||
isChecked: isChecked,
|
isChecked: isChecked,
|
||||||
|
isCheckStateMixed: isCheckStateMixed,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
isButton: isButton,
|
isButton: isButton,
|
||||||
isSlider: isSlider,
|
isSlider: isSlider,
|
||||||
@ -713,6 +715,7 @@ Matcher containsSemantics({
|
|||||||
// Flags
|
// Flags
|
||||||
bool? hasCheckedState,
|
bool? hasCheckedState,
|
||||||
bool? isChecked,
|
bool? isChecked,
|
||||||
|
bool? isCheckStateMixed,
|
||||||
bool? isSelected,
|
bool? isSelected,
|
||||||
bool? isButton,
|
bool? isButton,
|
||||||
bool? isSlider,
|
bool? isSlider,
|
||||||
@ -788,6 +791,7 @@ Matcher containsSemantics({
|
|||||||
// Flags
|
// Flags
|
||||||
hasCheckedState: hasCheckedState,
|
hasCheckedState: hasCheckedState,
|
||||||
isChecked: isChecked,
|
isChecked: isChecked,
|
||||||
|
isCheckStateMixed: isCheckStateMixed,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
isButton: isButton,
|
isButton: isButton,
|
||||||
isSlider: isSlider,
|
isSlider: isSlider,
|
||||||
@ -2085,6 +2089,7 @@ class _MatchesSemanticsData extends Matcher {
|
|||||||
// Flags
|
// Flags
|
||||||
required bool? hasCheckedState,
|
required bool? hasCheckedState,
|
||||||
required bool? isChecked,
|
required bool? isChecked,
|
||||||
|
required bool? isCheckStateMixed,
|
||||||
required bool? isSelected,
|
required bool? isSelected,
|
||||||
required bool? isButton,
|
required bool? isButton,
|
||||||
required bool? isSlider,
|
required bool? isSlider,
|
||||||
@ -2138,6 +2143,7 @@ class _MatchesSemanticsData extends Matcher {
|
|||||||
}) : flags = <SemanticsFlag, bool>{
|
}) : flags = <SemanticsFlag, bool>{
|
||||||
if (hasCheckedState != null) SemanticsFlag.hasCheckedState: hasCheckedState,
|
if (hasCheckedState != null) SemanticsFlag.hasCheckedState: hasCheckedState,
|
||||||
if (isChecked != null) SemanticsFlag.isChecked: isChecked,
|
if (isChecked != null) SemanticsFlag.isChecked: isChecked,
|
||||||
|
if (isCheckStateMixed != null) SemanticsFlag.isCheckStateMixed: isCheckStateMixed,
|
||||||
if (isSelected != null) SemanticsFlag.isSelected: isSelected,
|
if (isSelected != null) SemanticsFlag.isSelected: isSelected,
|
||||||
if (isButton != null) SemanticsFlag.isButton: isButton,
|
if (isButton != null) SemanticsFlag.isButton: isButton,
|
||||||
if (isSlider != null) SemanticsFlag.isSlider: isSlider,
|
if (isSlider != null) SemanticsFlag.isSlider: isSlider,
|
||||||
|
@ -656,6 +656,7 @@ void main() {
|
|||||||
/* Flags */
|
/* Flags */
|
||||||
hasCheckedState: true,
|
hasCheckedState: true,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
|
isCheckStateMixed: true,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
isButton: true,
|
isButton: true,
|
||||||
isSlider: true,
|
isSlider: true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user