diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index c10cb419123..580e4fdfe87 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -9,7 +9,7 @@ import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -export 'dart:ui' show SemanticsAction, SemanticsFlag; +export 'dart:ui' show SemanticsAction, SemanticsFlag, SemanticsFlags; export 'package:flutter/rendering.dart' show SemanticsData; const String _matcherHelp = @@ -62,7 +62,7 @@ class TestSemantics { this.currentValueLength, this.identifier = '', this.hintOverrides, - }) : assert(flags is int || flags is List), + }) : assert(flags is int || flags is List || flags is SemanticsFlags), assert(actions is int || actions is List), tags = tags?.toSet() ?? {}; @@ -95,7 +95,7 @@ class TestSemantics { this.identifier = '', this.hintOverrides, }) : id = 0, - assert(flags is int || flags is List), + assert(flags is int || flags is List || flags is SemanticsFlags), assert(actions is int || actions is List), rect = TestSemantics.rootRect, tags = tags?.toSet() ?? {}; @@ -138,7 +138,7 @@ class TestSemantics { this.currentValueLength, this.identifier = '', this.hintOverrides, - }) : assert(flags is int || flags is List), + }) : assert(flags is int || flags is List || flags is SemanticsFlags), assert(actions is int || actions is List), transform = _applyRootChildScale(transform), tags = tags?.toSet() ?? {}; @@ -149,12 +149,15 @@ class TestSemantics { /// they are created. final int? id; - /// The [SemanticsFlag]s set on this node. + /// The SemanticsFlags on this node. /// - /// There are two ways to specify this property: as an `int` that encodes the - /// flags as a bit field, or as a `List` that are _on_. + /// There are three ways to specify this property: as an `int` that encodes the + /// flags as a bit field, or as a `List` that are _on_, or as a `SemanticsFlags`. /// - /// Using `List` is recommended due to better readability. + /// Using `SemanticsFlags` is recommended. + /// + /// The `int` and `List` types are considered deprecated as they + /// have limited bits and only support the first 31 flags. final dynamic flags; /// The [SemanticsAction]s set on this node. @@ -323,14 +326,25 @@ class TestSemantics { final SemanticsData nodeData = node.getSemanticsData(); - final int flagsBitmask = flags is int - ? flags as int - : (flags as List).fold( - 0, - (int bitmask, SemanticsFlag flag) => bitmask | flag.index, - ); - if (flagsBitmask != nodeData.flags) { - return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.'); + if (flags is SemanticsFlags) { + if (flags != nodeData.flagsCollection) { + return fail( + 'expected node id $id to have flags $flags but found flags ${nodeData.flagsCollection}.', + ); + } + } + // the bitmask flags only support first 31 flags. + else { + final int flagsBitmask = flags is int + ? flags as int + : (flags as List).fold( + 0, + (int bitmask, SemanticsFlag flag) => bitmask | flag.index, + ); + + if (flagsBitmask != nodeData.flags) { + return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.'); + } } final int actionsBitmask = actions is int @@ -505,8 +519,9 @@ class TestSemantics { if (id != null) { buf.writeln('$indent id: $id,'); } - if (flags is int && flags != 0 || - flags is List && (flags as List).isNotEmpty) { + if ((flags is int && flags != 0) || + (flags is List && (flags as List).isNotEmpty) || + (flags is SemanticsFlags && (flags as SemanticsFlags) != SemanticsFlags.none)) { buf.writeln('$indent flags: ${SemanticsTester._flagsToSemanticsFlagExpression(flags)},'); } if (actions is int && actions != 0 || @@ -659,6 +674,7 @@ class SemanticsTester { TextDirection? textDirection, List? actions, List? flags, + SemanticsFlags? flagsCollection, Set? tags, double? scrollPosition, double? scrollExtentMax, @@ -719,7 +735,16 @@ class SemanticsTester { return false; } } - if (flags != null) { + + if (flagsCollection != null) { + final SemanticsFlags expectedFlags = flagsCollection; + final SemanticsFlags actualFlags = node.getSemanticsData().flagsCollection; + if (expectedFlags != actualFlags) { + return false; + } + } + // `flags` are deprecated and only support the first 31 flags. + else if (flags != null) { final int expectedFlags = flags.fold( 0, (int value, SemanticsFlag flag) => value | flag.index, @@ -827,7 +852,9 @@ class SemanticsTester { static String _flagsToSemanticsFlagExpression(dynamic flags) { Iterable list; - if (flags is int) { + if (flags is SemanticsFlags) { + return '[${flags.toStrings().join(', ')}]'; + } else if (flags is int) { list = SemanticsFlag.values.where((SemanticsFlag flag) => (flag.index & flags) != 0); } else { list = flags as List; @@ -1048,6 +1075,7 @@ class _IncludesNodeWith extends Matcher { this.textDirection, this.actions, this.flags, + this.flagsCollection, this.tags, this.scrollPosition, this.scrollExtentMax, @@ -1060,6 +1088,7 @@ class _IncludesNodeWith extends Matcher { value != null || actions != null || flags != null || + flagsCollection != null || tags != null || increasedValue != null || decreasedValue != null || @@ -1081,6 +1110,7 @@ class _IncludesNodeWith extends Matcher { final TextDirection? textDirection; final List? actions; final List? flags; + final SemanticsFlags? flagsCollection; final Set? tags; final double? scrollPosition; final double? scrollExtentMax; @@ -1104,6 +1134,7 @@ class _IncludesNodeWith extends Matcher { textDirection: textDirection, actions: actions, flags: flags, + flagsCollection: flagsCollection, tags: tags, scrollPosition: scrollPosition, scrollExtentMax: scrollExtentMax, @@ -1168,6 +1199,7 @@ Matcher includesNodeWith({ TextDirection? textDirection, List? actions, List? flags, + SemanticsFlags? flagsCollection, Set? tags, double? scrollPosition, double? scrollExtentMax, @@ -1188,6 +1220,7 @@ Matcher includesNodeWith({ decreasedValue: decreasedValue, actions: actions, flags: flags, + flagsCollection: flagsCollection, tags: tags, scrollPosition: scrollPosition, scrollExtentMax: scrollExtentMax, diff --git a/packages/flutter/test/widgets/semantics_tester_test.dart b/packages/flutter/test/widgets/semantics_tester_test.dart index 908daaa0a61..f25ca1c6bcd 100644 --- a/packages/flutter/test/widgets/semantics_tester_test.dart +++ b/packages/flutter/test/widgets/semantics_tester_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -46,4 +48,105 @@ void main() { ); semantics.dispose(); }); + + testWidgets('Semantics tester support flags as an int', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + await tester.pumpWidget( + Semantics( + container: true, + child: Semantics( + label: 'test1', + textDirection: TextDirection.ltr, + selected: true, + child: Container(), + ), + ), + ); + + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics.rootChild( + id: 1, + label: 'test1', + rect: TestSemantics.fullScreen, + flags: SemanticsFlag.hasSelectedState.index | SemanticsFlag.isSelected.index, + ), + ], + ), + ), + ); + semantics.dispose(); + }); + + testWidgets('Semantics tester support flags as a list of SemanticsFlag', ( + WidgetTester tester, + ) async { + final SemanticsTester semantics = SemanticsTester(tester); + + await tester.pumpWidget( + Semantics( + container: true, + child: Semantics( + label: 'test1', + textDirection: TextDirection.ltr, + selected: true, + child: Container(), + ), + ), + ); + + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics.rootChild( + id: 1, + label: 'test1', + rect: TestSemantics.fullScreen, + flags: [SemanticsFlag.hasSelectedState, SemanticsFlag.isSelected], + ), + ], + ), + ), + ); + semantics.dispose(); + }); + + testWidgets('Semantics tester support flags as a SemanticsFlags', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + await tester.pumpWidget( + Semantics( + container: true, + child: Semantics( + label: 'test1', + textDirection: TextDirection.ltr, + selected: true, + child: Container(), + ), + ), + ); + + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: [ + TestSemantics.rootChild( + id: 1, + label: 'test1', + rect: TestSemantics.fullScreen, + flags: SemanticsFlags(isSelected: Tristate.isTrue), + ), + ], + ), + ), + ); + semantics.dispose(); + }); } diff --git a/packages/flutter_test/lib/src/accessibility.dart b/packages/flutter_test/lib/src/accessibility.dart index 285279d3804..1a5552752e9 100644 --- a/packages/flutter_test/lib/src/accessibility.dart +++ b/packages/flutter_test/lib/src/accessibility.dart @@ -163,7 +163,7 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { } // skip node if it is touching the edge scrollable, since it might // be partially scrolled offscreen. - if (current.hasFlag(SemanticsFlag.hasImplicitScrolling) && + if (current.flagsCollection.hasImplicitScrolling && _isAtBoundary(paintBounds, current.rect)) { return result; } @@ -207,11 +207,11 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { // Skip node if it has no actions, or is marked as hidden. if ((!data.hasAction(ui.SemanticsAction.longPress) && !data.hasAction(ui.SemanticsAction.tap)) || - data.hasFlag(ui.SemanticsFlag.isHidden)) { + data.flagsCollection.isHidden) { return true; } // Skip links https://www.w3.org/WAI/WCAG21/Understanding/target-size.html - if (data.hasFlag(ui.SemanticsFlag.isLink)) { + if (data.flagsCollection.isLink) { return true; } return false; @@ -253,8 +253,8 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline { }); if (node.isMergedIntoParent || node.isInvisible || - node.hasFlag(ui.SemanticsFlag.isHidden) || - node.hasFlag(ui.SemanticsFlag.isTextField)) { + node.flagsCollection.isHidden || + node.flagsCollection.isTextField) { return result; } final SemanticsData data = node.getSemanticsData(); @@ -346,12 +346,11 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { Evaluation result = const Evaluation.pass(); // Skip disabled nodes, as they not required to pass contrast check. - final bool isDisabled = - node.hasFlag(ui.SemanticsFlag.hasEnabledState) && !node.hasFlag(ui.SemanticsFlag.isEnabled); + final bool isDisabled = node.flagsCollection.isEnabled == ui.Tristate.isFalse; if (node.isInvisible || node.isMergedIntoParent || - node.hasFlag(ui.SemanticsFlag.isHidden) || + node.flagsCollection.isHidden || isDisabled) { return result; } @@ -481,8 +480,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { /// /// Skip routes which might have labels, and nodes without any text. bool shouldSkipNode(SemanticsData data) => - data.hasFlag(ui.SemanticsFlag.scopesRoute) || - (data.label.trim().isEmpty && data.value.trim().isEmpty); + data.flagsCollection.scopesRoute || (data.label.trim().isEmpty && data.value.trim().isEmpty); /// Returns if a rectangle of node is off the screen. /// diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 9d54f05a71a..413db05458d 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -13,6 +13,7 @@ /// @docImport 'widget_tester.dart'; library; +import 'dart:ui' as ui; import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -74,17 +75,6 @@ class SemanticsController { SemanticsAction.scrollRight.index | SemanticsAction.scrollToOffset.index; - /// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/flutter/blob/main/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java). - static final int _importantFlagsForAccessibility = - SemanticsFlag.hasCheckedState.index | - SemanticsFlag.hasToggledState.index | - SemanticsFlag.hasEnabledState.index | - SemanticsFlag.isButton.index | - SemanticsFlag.isTextField.index | - SemanticsFlag.isFocusable.index | - SemanticsFlag.isSlider.index | - SemanticsFlag.isInMutuallyExclusiveGroup.index; - final WidgetController _controller; /// Attempts to find the [SemanticsNode] of first result from `finder`. @@ -353,7 +343,7 @@ class SemanticsController { final SemanticsData data = node.getSemanticsData(); // If the node scopes a route, it doesn't matter what other flags/actions it // has, it is _not_ important for accessibility, so we short circuit. - if (data.hasFlag(SemanticsFlag.scopesRoute)) { + if (data.flagsCollection.scopesRoute) { return false; } @@ -362,7 +352,17 @@ class SemanticsController { return true; } - final bool hasImportantFlag = data.flags & _importantFlagsForAccessibility != 0; + /// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/flutter/blob/main/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java). + final bool hasImportantFlag = + data.flagsCollection.isChecked != ui.CheckedState.none || + data.flagsCollection.isToggled != ui.Tristate.none || + data.flagsCollection.isEnabled != ui.Tristate.none || + data.flagsCollection.isButton || + data.flagsCollection.isTextField || + data.flagsCollection.isFocused != ui.Tristate.none || + data.flagsCollection.isSlider || + data.flagsCollection.isInMutuallyExclusiveGroup; + if (hasImportantFlag) { return true; } diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 2fb1f571b24..8d5444c2209 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -2454,38 +2454,38 @@ class _MatchesSemanticsData extends Matcher { required String? onLongPressHint, required this.customActions, required this.children, - }) : flags = { - SemanticsFlag.hasCheckedState: ?hasCheckedState, - SemanticsFlag.isChecked: ?isChecked, - SemanticsFlag.isCheckStateMixed: ?isCheckStateMixed, - SemanticsFlag.isSelected: ?isSelected, - SemanticsFlag.hasSelectedState: ?hasSelectedState, - SemanticsFlag.isButton: ?isButton, - SemanticsFlag.isSlider: ?isSlider, - SemanticsFlag.isKeyboardKey: ?isKeyboardKey, - SemanticsFlag.isLink: ?isLink, - SemanticsFlag.isTextField: ?isTextField, - SemanticsFlag.isReadOnly: ?isReadOnly, - SemanticsFlag.isFocused: ?isFocused, - SemanticsFlag.isFocusable: ?isFocusable, - SemanticsFlag.hasEnabledState: ?hasEnabledState, - SemanticsFlag.isEnabled: ?isEnabled, - SemanticsFlag.isInMutuallyExclusiveGroup: ?isInMutuallyExclusiveGroup, - SemanticsFlag.isHeader: ?isHeader, - SemanticsFlag.isObscured: ?isObscured, - SemanticsFlag.isMultiline: ?isMultiline, - SemanticsFlag.namesRoute: ?namesRoute, - SemanticsFlag.scopesRoute: ?scopesRoute, - SemanticsFlag.isHidden: ?isHidden, - SemanticsFlag.isImage: ?isImage, - SemanticsFlag.isLiveRegion: ?isLiveRegion, - SemanticsFlag.hasToggledState: ?hasToggledState, - SemanticsFlag.isToggled: ?isToggled, - SemanticsFlag.hasImplicitScrolling: ?hasImplicitScrolling, - SemanticsFlag.hasExpandedState: ?hasExpandedState, - SemanticsFlag.isExpanded: ?isExpanded, - SemanticsFlag.hasRequiredState: ?hasRequiredState, - SemanticsFlag.isRequired: ?isRequired, + }) : flags = { + 'hasCheckedState': ?hasCheckedState, + 'isChecked': ?isChecked, + 'isCheckStateMixed': ?isCheckStateMixed, + 'isSelected': ?isSelected, + 'hasSelectedState': ?hasSelectedState, + 'isButton': ?isButton, + 'isSlider': ?isSlider, + 'isKeyboardKey': ?isKeyboardKey, + 'isLink': ?isLink, + 'isTextField': ?isTextField, + 'isReadOnly': ?isReadOnly, + 'isFocused': ?isFocused, + 'isFocusable': ?isFocusable, + 'hasEnabledState': ?hasEnabledState, + 'isEnabled': ?isEnabled, + 'isInMutuallyExclusiveGroup': ?isInMutuallyExclusiveGroup, + 'isHeader': ?isHeader, + 'isObscured': ?isObscured, + 'isMultiline': ?isMultiline, + 'namesRoute': ?namesRoute, + 'scopesRoute': ?scopesRoute, + 'isHidden': ?isHidden, + 'isImage': ?isImage, + 'isLiveRegion': ?isLiveRegion, + 'hasToggledState': ?hasToggledState, + 'isToggled': ?isToggled, + 'hasImplicitScrolling': ?hasImplicitScrolling, + 'hasExpandedState': ?hasExpandedState, + 'isExpanded': ?isExpanded, + 'hasRequiredState': ?hasRequiredState, + 'isRequired': ?isRequired, }, actions = { SemanticsAction.tap: ?hasTapAction, @@ -2546,7 +2546,7 @@ class _MatchesSemanticsData extends Matcher { /// 2. If the flag/action maps to `false`, then it must not be present in the SemanticData /// 3. If the flag/action is not in the map, then it will not be validated against final Map actions; - final Map flags; + final Map flags; @override Description describe(Description description, [String? index]) { @@ -2598,27 +2598,27 @@ class _MatchesSemanticsData extends Matcher { .toList(); if (expectedActions.isNotEmpty) { - description.add(' with actions: ${_createEnumsSummary(expectedActions)} '); + description.add(' with actions: ${_createSemanticsActionSummary(expectedActions)} '); } if (notExpectedActions.isNotEmpty) { - description.add(' without actions: ${_createEnumsSummary(notExpectedActions)} '); + description.add(' without actions: ${_createSemanticsActionSummary(notExpectedActions)} '); } } if (flags.isNotEmpty) { - final List expectedFlags = flags.entries - .where((MapEntry e) => e.value) - .map((MapEntry e) => e.key) + final List expectedFlags = flags.entries + .where((MapEntry e) => e.value) + .map((MapEntry e) => e.key) .toList(); - final List notExpectedFlags = flags.entries - .where((MapEntry e) => !e.value) - .map((MapEntry e) => e.key) + final List notExpectedFlags = flags.entries + .where((MapEntry e) => !e.value) + .map((MapEntry e) => e.key) .toList(); if (expectedFlags.isNotEmpty) { - description.add(' with flags: ${_createEnumsSummary(expectedFlags)} '); + description.add(' with flags: ${expectedFlags.join(',')} '); } if (notExpectedFlags.isNotEmpty) { - description.add(' without flags: ${_createEnumsSummary(notExpectedFlags)} '); + description.add(' without flags: ${notExpectedFlags.join(', ')} '); } } if (textDirection != null) { @@ -2803,7 +2803,7 @@ class _MatchesSemanticsData extends Matcher { if (unexpectedActions.isNotEmpty || missingActions.isNotEmpty) { return failWithDescription( matchState, - 'missing actions: ${_createEnumsSummary(missingActions)} unexpected actions: ${_createEnumsSummary(unexpectedActions)}', + 'missing actions: ${_createSemanticsActionSummary(missingActions)} unexpected actions: ${_createSemanticsActionSummary(unexpectedActions)}', ); } } @@ -2848,17 +2848,18 @@ class _MatchesSemanticsData extends Matcher { } } if (flags.isNotEmpty) { - final List unexpectedFlags = []; - final List missingFlags = []; - for (final MapEntry flagEntry in flags.entries) { - final ui.SemanticsFlag flag = flagEntry.key; + final Map foundFlags = _stringsMapFromSemanticsFlags(data.flagsCollection); + final List unexpectedFlags = []; + final List missingFlags = []; + for (final MapEntry flagEntry in flags.entries) { + final String flagName = flagEntry.key; final bool flagExpected = flagEntry.value; - final bool flagPresent = flag.index & data.flags == flag.index; + final bool flagPresent = foundFlags[flagName]!; if (flagPresent != flagExpected) { if (flagExpected) { - missingFlags.add(flag); + missingFlags.add(flagName); } else { - unexpectedFlags.add(flag); + unexpectedFlags.add(flagName); } } } @@ -2866,7 +2867,7 @@ class _MatchesSemanticsData extends Matcher { if (unexpectedFlags.isNotEmpty || missingFlags.isNotEmpty) { return failWithDescription( matchState, - 'missing flags: ${_createEnumsSummary(missingFlags)} unexpected flags: ${_createEnumsSummary(unexpectedFlags)}', + 'missing flags: ${missingFlags.join(',')} unexpected flags: ${unexpectedFlags.join(',')}', ); } } @@ -2897,16 +2898,8 @@ class _MatchesSemanticsData extends Matcher { return mismatchDescription.add(matchState['failure'] as String); } - static String _createEnumsSummary(List enums) { - assert( - T == SemanticsAction || T == SemanticsFlag, - 'This method is only intended for lists of SemanticsActions or SemanticsFlags.', - ); - if (T == SemanticsAction) { - return '[${(enums as List).map((SemanticsAction d) => d.name).join(', ')}]'; - } else { - return '[${(enums as List).map((SemanticsFlag d) => d.name).join(', ')}]'; - } + static String _createSemanticsActionSummary(List enums) { + return '[${enums.map((ui.SemanticsAction d) => d.name).join(', ')}]'; } } @@ -2949,3 +2942,39 @@ class _DoesNotMatchAccessibilityGuideline extends AsyncMatcher { return null; } } + +Map _stringsMapFromSemanticsFlags(SemanticsFlags flagsCollection) { + return { + 'hasCheckedState': flagsCollection.isChecked != ui.CheckedState.none, + 'isChecked': flagsCollection.isChecked == ui.CheckedState.isTrue, + 'isCheckStateMixed': flagsCollection.isChecked == ui.CheckedState.mixed, + 'isSelected': flagsCollection.isSelected == ui.Tristate.isTrue, + 'hasSelectedState': flagsCollection.isSelected != ui.Tristate.none, + 'isButton': flagsCollection.isButton, + 'isSlider': flagsCollection.isSlider, + 'isKeyboardKey': flagsCollection.isKeyboardKey, + 'isLink': flagsCollection.isLink, + 'isFocused': flagsCollection.isFocused == ui.Tristate.isTrue, + 'isFocusable': flagsCollection.isFocused != ui.Tristate.none, + 'isTextField': flagsCollection.isTextField, + 'isReadOnly': flagsCollection.isReadOnly, + 'hasEnabledState': flagsCollection.isEnabled != ui.Tristate.none, + 'isEnabled': flagsCollection.isEnabled == ui.Tristate.isTrue, + 'isInMutuallyExclusiveGroup': flagsCollection.isInMutuallyExclusiveGroup, + 'isHeader': flagsCollection.isHeader, + 'isObscured': flagsCollection.isObscured, + 'isMultiline': flagsCollection.isMultiline, + 'namesRoute': flagsCollection.namesRoute, + 'scopesRoute': flagsCollection.scopesRoute, + 'isHidden': flagsCollection.isHidden, + 'isImage': flagsCollection.isImage, + 'isLiveRegion': flagsCollection.isLiveRegion, + 'hasToggledState': flagsCollection.isToggled != ui.Tristate.none, + 'isToggled': flagsCollection.isToggled == ui.Tristate.isTrue, + 'hasImplicitScrolling': flagsCollection.hasImplicitScrolling, + 'hasExpandedState': flagsCollection.isExpanded != ui.Tristate.none, + 'isExpanded': flagsCollection.isExpanded == ui.Tristate.isTrue, + 'hasRequiredState': flagsCollection.isRequired != ui.Tristate.none, + 'isRequired': flagsCollection.isRequired == ui.Tristate.isTrue, + }; +}