Unverified Commit 4d3c1224 authored by yaakovschectman's avatar yaakovschectman Committed by GitHub

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: 's avatarChris 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: 's avatarLoïc Sharma <737941+loic-sharma@users.noreply.github.com>
Co-authored-by: 's avatarChris Bracken <chris@bracken.jp>
Co-authored-by: 's avatarLoïc Sharma <737941+loic-sharma@users.noreply.github.com>
parent 70f6bed9
...@@ -474,6 +474,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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({ ...@@ -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({ ...@@ -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({ ...@@ -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({ ...@@ -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 { ...@@ -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 { ...@@ -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() { ...@@ -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,
......
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