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
return Semantics(
checked: widget.value ?? false,
mixed: widget.tristate ? widget.value == null : null,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
......
......@@ -872,6 +872,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.checked != null) {
config.isChecked = properties.checked;
}
if (properties.mixed != null) {
config.isCheckStateMixed = properties.mixed;
}
if (properties.selected != null) {
config.isSelected = properties.selected!;
}
......
......@@ -4355,6 +4355,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (_properties.checked != null) {
config.isChecked = _properties.checked;
}
if (_properties.mixed != null) {
config.isCheckStateMixed = _properties.mixed;
}
if (_properties.toggled != null) {
config.isToggled = _properties.toggled;
}
......
......@@ -776,6 +776,7 @@ class SemanticsProperties extends DiagnosticableTree {
const SemanticsProperties({
this.enabled,
this.checked,
this.mixed,
this.selected,
this.toggled,
this.button,
......@@ -851,14 +852,30 @@ class SemanticsProperties extends DiagnosticableTree {
/// or similar widget with a "checked" state, and what its current
/// 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;
/// 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
/// or similar widget with an "on" state, and what its current
/// state is.
///
/// This is mutually exclusive with [checked].
/// This is mutually exclusive with [checked] and [mixed].
final bool? toggled;
/// If non-null indicates that this subtree represents something that can be
......@@ -1490,6 +1507,7 @@ class SemanticsProperties extends DiagnosticableTree {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
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(StringProperty('label', label, defaultValue: null));
properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null));
......@@ -4189,10 +4207,26 @@ class SemanticsConfiguration {
/// checked/unchecked state.
bool? get isChecked => _hasFlag(SemanticsFlag.hasCheckedState) ? _hasFlag(SemanticsFlag.isChecked) : null;
set isChecked(bool? value) {
assert(value != true || isCheckStateMixed != true);
_setFlag(SemanticsFlag.hasCheckedState, true);
_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
/// that state is on or off, corresponding to true and false, respectively.
///
......
......@@ -6878,6 +6878,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool excludeSemantics = false,
bool? enabled,
bool? checked,
bool? mixed,
bool? selected,
bool? toggled,
bool? button,
......@@ -6943,6 +6944,7 @@ class Semantics extends SingleChildRenderObjectWidget {
properties: SemanticsProperties(
enabled: enabled,
checked: checked,
mixed: mixed,
toggled: toggled,
selected: selected,
button: button,
......
......@@ -139,6 +139,57 @@ void main() {
hasEnabledState: 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();
});
......@@ -239,6 +290,7 @@ void main() {
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isCheckStateMixed,
],
actions: <SemanticsAction>[SemanticsAction.tap],
), hasLength(1));
......
......@@ -451,7 +451,9 @@ void _defineTests() {
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
// therefore it has to be removed.
flags.remove(SemanticsFlag.hasImplicitScrolling);
flags
..remove(SemanticsFlag.hasImplicitScrolling)
..remove(SemanticsFlag.isCheckStateMixed);
TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
......@@ -475,7 +477,8 @@ void _defineTests() {
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
enabled: true,
checked: true,
checked: false,
mixed: true,
toggled: true,
selected: true,
hidden: true,
......@@ -502,7 +505,9 @@ void _defineTests() {
flags = SemanticsFlag.values.values.toList();
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
// therefore it has to be removed.
flags.remove(SemanticsFlag.hasImplicitScrolling);
flags
..remove(SemanticsFlag.hasImplicitScrolling)
..remove(SemanticsFlag.isChecked);
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
......@@ -519,7 +524,7 @@ void _defineTests() {
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
}, skip: true); // [intended] https://github.com/flutter/flutter/issues/110107
});
group('diffing', () {
testWidgets('complains about duplicate keys', (WidgetTester tester) async {
......
......@@ -584,7 +584,8 @@ void main() {
flags
..remove(SemanticsFlag.hasToggledState)
..remove(SemanticsFlag.isToggled)
..remove(SemanticsFlag.hasImplicitScrolling);
..remove(SemanticsFlag.hasImplicitScrolling)
..remove(SemanticsFlag.isCheckStateMixed);
TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
......@@ -631,8 +632,50 @@ void main() {
);
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();
}, 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 {
final SemanticsTester semantics = SemanticsTester(tester);
......
......@@ -542,6 +542,7 @@ Matcher matchesSemantics({
// Flags //
bool hasCheckedState = false,
bool isChecked = false,
bool isCheckStateMixed = false,
bool isSelected = false,
bool isButton = false,
bool isSlider = false,
......@@ -617,6 +618,7 @@ Matcher matchesSemantics({
// Flags
hasCheckedState: hasCheckedState,
isChecked: isChecked,
isCheckStateMixed: isCheckStateMixed,
isSelected: isSelected,
isButton: isButton,
isSlider: isSlider,
......@@ -713,6 +715,7 @@ Matcher containsSemantics({
// Flags
bool? hasCheckedState,
bool? isChecked,
bool? isCheckStateMixed,
bool? isSelected,
bool? isButton,
bool? isSlider,
......@@ -788,6 +791,7 @@ Matcher containsSemantics({
// Flags
hasCheckedState: hasCheckedState,
isChecked: isChecked,
isCheckStateMixed: isCheckStateMixed,
isSelected: isSelected,
isButton: isButton,
isSlider: isSlider,
......@@ -2085,6 +2089,7 @@ class _MatchesSemanticsData extends Matcher {
// Flags
required bool? hasCheckedState,
required bool? isChecked,
required bool? isCheckStateMixed,
required bool? isSelected,
required bool? isButton,
required bool? isSlider,
......@@ -2138,6 +2143,7 @@ class _MatchesSemanticsData extends Matcher {
}) : flags = <SemanticsFlag, bool>{
if (hasCheckedState != null) SemanticsFlag.hasCheckedState: hasCheckedState,
if (isChecked != null) SemanticsFlag.isChecked: isChecked,
if (isCheckStateMixed != null) SemanticsFlag.isCheckStateMixed: isCheckStateMixed,
if (isSelected != null) SemanticsFlag.isSelected: isSelected,
if (isButton != null) SemanticsFlag.isButton: isButton,
if (isSlider != null) SemanticsFlag.isSlider: isSlider,
......
......@@ -656,6 +656,7 @@ void main() {
/* Flags */
hasCheckedState: true,
isChecked: true,
isCheckStateMixed: true,
isSelected: true,
isButton: 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