diff --git a/engine/src/flutter/lib/ui/fixtures/ui_test.dart b/engine/src/flutter/lib/ui/fixtures/ui_test.dart index ed9470da9c5..128d9fd2a57 100644 --- a/engine/src/flutter/lib/ui/fixtures/ui_test.dart +++ b/engine/src/flutter/lib/ui/fixtures/ui_test.dart @@ -298,6 +298,8 @@ void sendSemanticsUpdate() { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); _semanticsUpdate(builder.build()); } @@ -359,6 +361,8 @@ void sendSemanticsUpdateWithRole() { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); _semanticsUpdate(builder.build()); } @@ -420,6 +424,8 @@ void sendSemanticsUpdateWithLocale() { controlsNodes: null, inputType: SemanticsInputType.none, locale: Locale('es', 'MX'), + minValue: '0', + maxValue: '0', ); _semanticsUpdate(builder.build()); } @@ -476,6 +482,8 @@ void sendSemanticsUpdateWithIsLink() { controlsNodes: null, inputType: SemanticsInputType.none, locale: Locale('es', 'MX'), + minValue: '0', + maxValue: '0', ); _semanticsUpdate(builder.build()); } diff --git a/engine/src/flutter/lib/ui/semantics.dart b/engine/src/flutter/lib/ui/semantics.dart index 99dce73768b..3392ef8a099 100644 --- a/engine/src/flutter/lib/ui/semantics.dart +++ b/engine/src/flutter/lib/ui/semantics.dart @@ -1975,6 +1975,8 @@ abstract class SemanticsUpdateBuilder { SemanticsHitTestBehavior hitTestBehavior = SemanticsHitTestBehavior.defer, required SemanticsInputType inputType, required Locale? locale, + required String minValue, + required String maxValue, }); /// Update the custom semantics action associated with the given `id`. @@ -2056,6 +2058,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 SemanticsHitTestBehavior hitTestBehavior = SemanticsHitTestBehavior.defer, required SemanticsInputType inputType, required Locale? locale, + required String minValue, + required String maxValue, }) { assert(_matrix4IsValid(transform)); assert( @@ -2107,6 +2111,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 hitTestBehavior.index, inputType.index, locale?.toLanguageTag() ?? '', + minValue, + maxValue, ); } @@ -2157,6 +2163,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 Int32, Int32, Handle, + Handle, + Handle, ) >(symbol: 'SemanticsUpdateBuilder::updateNode') external void _updateNode( @@ -2204,6 +2212,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 int hitTestBehaviorIndex, int inputType, String locale, + String minValue, + String maxValue, ); @override diff --git a/engine/src/flutter/lib/ui/semantics/semantics_node.h b/engine/src/flutter/lib/ui/semantics/semantics_node.h index 6bbf7e0eb5c..1bdec7cade7 100644 --- a/engine/src/flutter/lib/ui/semantics/semantics_node.h +++ b/engine/src/flutter/lib/ui/semantics/semantics_node.h @@ -145,6 +145,8 @@ struct SemanticsNode { double scrollPosition = std::nan(""); double scrollExtentMax = std::nan(""); double scrollExtentMin = std::nan(""); + std::string minValue; + std::string maxValue; std::string identifier; std::string label; StringAttributes labelAttributes; diff --git a/engine/src/flutter/lib/ui/semantics/semantics_update_builder.cc b/engine/src/flutter/lib/ui/semantics/semantics_update_builder.cc index a2cc6d65043..b23096cb6ac 100644 --- a/engine/src/flutter/lib/ui/semantics/semantics_update_builder.cc +++ b/engine/src/flutter/lib/ui/semantics/semantics_update_builder.cc @@ -74,7 +74,9 @@ void SemanticsUpdateBuilder::updateNode( int validationResult, int hitTestBehavior, int inputType, - std::string locale) { + std::string locale, + std::string minValue, + std::string maxValue) { FML_CHECK(scrollChildren == 0 || (scrollChildren > 0 && childrenInHitTestOrder.data())) << "Semantics update contained scrollChildren but did not have " @@ -96,6 +98,8 @@ void SemanticsUpdateBuilder::updateNode( node.scrollPosition = scrollPosition; node.scrollExtentMax = scrollExtentMax; node.scrollExtentMin = scrollExtentMin; + node.minValue = std::move(minValue); + node.maxValue = std::move(maxValue); node.rect = SkRect::MakeLTRB(SafeNarrow(left), SafeNarrow(top), SafeNarrow(right), SafeNarrow(bottom)); node.identifier = std::move(identifier); diff --git a/engine/src/flutter/lib/ui/semantics/semantics_update_builder.h b/engine/src/flutter/lib/ui/semantics/semantics_update_builder.h index 14dfccaf7bd..e53b9c08b64 100644 --- a/engine/src/flutter/lib/ui/semantics/semantics_update_builder.h +++ b/engine/src/flutter/lib/ui/semantics/semantics_update_builder.h @@ -73,7 +73,9 @@ class SemanticsUpdateBuilder int validationResult, int hitTestBehavior, int inputType, - std::string locale); + std::string locale, + std::string minValue, + std::string maxValue); void updateCustomAction(int id, std::string label, diff --git a/engine/src/flutter/lib/web_ui/lib/semantics.dart b/engine/src/flutter/lib/web_ui/lib/semantics.dart index 74352902430..395cbc49d69 100644 --- a/engine/src/flutter/lib/web_ui/lib/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/semantics.dart @@ -753,6 +753,8 @@ class SemanticsUpdateBuilder { SemanticsHitTestBehavior hitTestBehavior = SemanticsHitTestBehavior.defer, required SemanticsInputType inputType, required Locale? locale, + required String minValue, + required String maxValue, }) { if (transform.length != 16) { throw ArgumentError('transform argument must have 16 entries.'); @@ -800,6 +802,8 @@ class SemanticsUpdateBuilder { hitTestBehavior: hitTestBehavior, inputType: inputType, locale: locale, + minValue: minValue, + maxValue: maxValue, ), ); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index d56b9ec0602..1eac87647ff 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -116,6 +116,7 @@ export 'engine/semantics/list.dart'; export 'engine/semantics/live_region.dart'; export 'engine/semantics/menus.dart'; export 'engine/semantics/platform_view.dart'; +export 'engine/semantics/progress_bar.dart'; export 'engine/semantics/requirable.dart'; export 'engine/semantics/route.dart'; export 'engine/semantics/scrollable.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart index 600fc3dd6e4..ea30b9e21dd 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart @@ -20,6 +20,7 @@ export 'semantics/list.dart'; export 'semantics/live_region.dart'; export 'semantics/menus.dart'; export 'semantics/platform_view.dart'; +export 'semantics/progress_bar.dart'; export 'semantics/requirable.dart'; export 'semantics/scrollable.dart'; export 'semantics/semantics.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/progress_bar.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/progress_bar.dart new file mode 100644 index 00000000000..5205b86e255 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/progress_bar.dart @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'label_and_value.dart'; +import 'semantics.dart'; + +/// Indicates a progress bar element. +/// +/// Uses aria progressbar role to convey this semantic information to the element. +/// +/// Screen-readers take advantage of "aria-label" to describe the visual. +class SemanticsProgressBar extends SemanticRole { + SemanticsProgressBar(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.progressBar, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('progressbar'); + _updateAriaAttributes(); + } + + void _updateAriaAttributes() { + // Set ARIA attributes for min, max and current value. + if (semanticsObject.minValue?.isNotEmpty ?? false) { + setAttribute('aria-valuemin', semanticsObject.minValue!); + } + if (semanticsObject.maxValue?.isNotEmpty ?? false) { + setAttribute('aria-valuemax', semanticsObject.maxValue!); + } + if (semanticsObject.value?.isNotEmpty ?? false) { + setAttribute('aria-valuenow', semanticsObject.value!); + } + } + + @override + void update() { + super.update(); + _updateAriaAttributes(); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} + +/// Indicates a loading spinner element. +class SemanticsLoadingSpinner extends SemanticRole { + SemanticsLoadingSpinner(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.loadingSpinner, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ); + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 911b206b5c1..30fb16d95ac 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -37,6 +37,7 @@ import 'list.dart'; import 'live_region.dart'; import 'menus.dart'; import 'platform_view.dart'; +import 'progress_bar.dart'; import 'requirable.dart'; import 'route.dart'; import 'scrollable.dart'; @@ -275,6 +276,8 @@ class SemanticsNodeUpdate { this.hitTestBehavior = ui.SemanticsHitTestBehavior.defer, required this.inputType, required this.locale, + required this.minValue, + required this.maxValue, }); /// See [ui.SemanticsUpdateBuilder.updateNode]. @@ -399,6 +402,12 @@ class SemanticsNodeUpdate { /// See [ui.SemanticsUpdateBuilder.updateNode]. final ui.Locale? locale; + + /// See [ui.SemanticsUpdateBuilder.updateNode]. + final String minValue; + + /// See [ui.SemanticsUpdateBuilder.updateNode]. + final String maxValue; } /// Identifies [SemanticRole] implementations. @@ -503,6 +512,12 @@ enum EngineSemanticsRole { /// An item in a [list]. listItem, + /// A graphic object that shows progress with a numeric number. + progressBar, + + /// A graphic object that spins to indicate the application is busy. + loadingSpinner, + /// A role used when a more specific role cannot be assigend to /// a [SemanticsObject]. /// @@ -1550,6 +1565,31 @@ class SemanticsObject { _dirtyFields |= _hitTestBehaviorIndex; } + String? get minValue => _minValue; + String? _minValue; + + static const int _minValueIndex = 1 << 29; + + /// Whether the [minValue] field has been updated but has not been + /// applied to the DOM yet. + bool get isMinValueDirty => _isDirty(_minValueIndex); + void _markMinValueDirty() { + _dirtyFields |= _minValueIndex; + } + + /// See [ui.SemanticsUpdateBuilder.updateNode]. + String? get maxValue => _maxValue; + String? _maxValue; + + static const int _maxValueIndex = 1 << 30; + + /// Whether the [maxValue] field has been updated but has not been + /// applied to the DOM yet. + bool get isMaxValueDirty => _isDirty(_maxValueIndex); + void _markMaxValueDirty() { + _dirtyFields |= _maxValueIndex; + } + /// A unique permanent identifier of the semantics node in the tree. final int id; @@ -1887,6 +1927,16 @@ class SemanticsObject { _markHitTestBehaviorDirty(); } + if (_minValue != update.minValue) { + _minValue = update.minValue; + _markMinValueDirty(); + } + + if (_maxValue != update.maxValue) { + _maxValue = update.maxValue; + _markMaxValueDirty(); + } + role = update.role; inputType = update.inputType; @@ -2139,14 +2189,16 @@ class SemanticsObject { return EngineSemanticsRole.region; case ui.SemanticsRole.form: return EngineSemanticsRole.form; + case ui.SemanticsRole.loadingSpinner: + return EngineSemanticsRole.loadingSpinner; + case ui.SemanticsRole.progressBar: + return EngineSemanticsRole.progressBar; // TODO(chunhtai): implement these roles. // https://github.com/flutter/flutter/issues/159741. case ui.SemanticsRole.dragHandle: case ui.SemanticsRole.spinButton: case ui.SemanticsRole.comboBox: case ui.SemanticsRole.tooltip: - case ui.SemanticsRole.loadingSpinner: - case ui.SemanticsRole.progressBar: case ui.SemanticsRole.hotKey: case ui.SemanticsRole.none: // fallback to checking semantics properties. @@ -2213,6 +2265,8 @@ class SemanticsObject { EngineSemanticsRole.menuItemRadio => SemanticMenuItemRadio(this), EngineSemanticsRole.alert => SemanticAlert(this), EngineSemanticsRole.status => SemanticStatus(this), + EngineSemanticsRole.progressBar => SemanticsProgressBar(this), + EngineSemanticsRole.loadingSpinner => SemanticsLoadingSpinner(this), EngineSemanticsRole.generic => GenericRole(this), EngineSemanticsRole.complementary => SemanticComplementary(this), EngineSemanticsRole.contentInfo => SemanticContentInfo(this), diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 6ec795148ea..2b655df58b1 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -160,6 +160,12 @@ void runSemanticsTests() { group('forms', () { _testForms(); }); + group('progressBar', () { + _testProgressBar(); + }); + group('loadingSpinner', () { + _testLoadingSpinner(); + }); } void _testSemanticRole() { @@ -6151,6 +6157,55 @@ void _testForms() { semantics().semanticsEnabled = false; } +void _testProgressBar() { + test('nodes with progress bar role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.progressBar, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.progressBar); + expect(object.element.getAttribute('role'), 'progressbar'); + }); + + semantics().semanticsEnabled = false; +} + +void _testLoadingSpinner() { + test('nodes with loading spinner role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.loadingSpinner, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.loadingSpinner); + }); + + semantics().semanticsEnabled = false; +} + /// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that /// supplies default values for semantics attributes. void updateNode( @@ -6195,6 +6250,8 @@ void updateNode( ui.SemanticsHitTestBehavior hitTestBehavior = ui.SemanticsHitTestBehavior.defer, ui.SemanticsInputType inputType = ui.SemanticsInputType.none, ui.Locale? locale, + String minValue = '0', + String maxValue = '0', }) { transform ??= Float64List.fromList(Matrix4.identity().storage); hitTestTransform ??= Float64List.fromList(Matrix4.identity().storage); @@ -6242,6 +6299,8 @@ void updateNode( hitTestBehavior: hitTestBehavior, inputType: inputType, locale: locale, + minValue: minValue, + maxValue: maxValue, ); } diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart index ed7fcae84c8..33612f9c411 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -92,6 +92,8 @@ class SemanticsTester { ui.SemanticsHitTestBehavior hitTestBehavior = ui.SemanticsHitTestBehavior.defer, ui.SemanticsInputType inputType = ui.SemanticsInputType.none, ui.Locale? locale, + String? minValue, + String? maxValue, }) { // Actions if (hasTap ?? false) { @@ -228,6 +230,8 @@ class SemanticsTester { hitTestBehavior: hitTestBehavior, inputType: inputType, locale: locale, + minValue: minValue ?? '0', + maxValue: maxValue ?? '0', ); _nodeUpdates.add(update); return update; diff --git a/engine/src/flutter/runtime/fixtures/runtime_test.dart b/engine/src/flutter/runtime/fixtures/runtime_test.dart index 309ff2ad214..3f2cbaf4319 100644 --- a/engine/src/flutter/runtime/fixtures/runtime_test.dart +++ b/engine/src/flutter/runtime/fixtures/runtime_test.dart @@ -316,6 +316,8 @@ void sendSemanticsUpdate() { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); _semanticsUpdate(builder.build()); } diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart index 67df7baa684..ea4f16371a5 100644 --- a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart @@ -200,6 +200,8 @@ Future a11y_main() async { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ) ..updateNode( id: 84, @@ -238,6 +240,8 @@ Future a11y_main() async { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ) ..updateNode( id: 96, @@ -276,6 +280,8 @@ Future a11y_main() async { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ) ..updateNode( id: 128, @@ -314,6 +320,8 @@ Future a11y_main() async { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ) ..updateCustomAction(id: 21, label: 'Archive', hint: 'archive message'); @@ -399,6 +407,8 @@ Future a11y_string_attributes() async { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); PlatformDispatcher.instance.setSemanticsTreeEnabled(true); @@ -1689,6 +1699,8 @@ Future a11y_main_multi_view() async { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); } diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 290ccec4fd8..306e62afd9f 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -477,6 +477,8 @@ Future sendSemanticsTreeInfo() async { controlsNodes: null, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); return builder.build(); } diff --git a/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart b/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart index eb5556d80a1..d952c5ef8b2 100644 --- a/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart +++ b/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart @@ -77,6 +77,8 @@ class LocaleInitialization extends Scenario { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build(); @@ -139,6 +141,8 @@ class LocaleInitialization extends Scenario { controlsNodes: null, inputType: SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build(); diff --git a/packages/flutter/lib/src/material/progress_indicator.dart b/packages/flutter/lib/src/material/progress_indicator.dart index 5022cee5f83..a6f16c77d41 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -8,6 +8,7 @@ library; import 'dart:math' as math; +import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -143,11 +144,20 @@ abstract class ProgressIndicator extends StatefulWidget { } Widget _buildSemanticsWrapper({required BuildContext context, required Widget child}) { + var isProgressBar = false; String? expandedSemanticsValue = semanticsValue; if (value != null) { - expandedSemanticsValue ??= '${(_effectiveValue! * 100).round()}%'; + expandedSemanticsValue ??= '${(_effectiveValue! * 100).round()}'; + isProgressBar = true; } - return Semantics(label: semanticsLabel, value: expandedSemanticsValue, child: child); + return Semantics( + label: semanticsLabel, + role: isProgressBar ? SemanticsRole.progressBar : SemanticsRole.loadingSpinner, + minValue: isProgressBar ? '0' : null, + maxValue: isProgressBar ? '100' : null, + value: expandedSemanticsValue, + child: child, + ); } } @@ -1194,28 +1204,32 @@ class _CircularProgressIndicatorState extends State @override Widget build(BuildContext context) { - switch (widget._indicatorType) { - case _ActivityIndicatorType.material: - if (widget._effectiveValue != null) { - return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); - } - return _buildAnimation(); - case _ActivityIndicatorType.adaptive: - final ThemeData theme = Theme.of(context); - switch (theme.platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - return _buildCupertinoIndicator(context); - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: + return Builder( + builder: (BuildContext context) { + switch (widget._indicatorType) { + case _ActivityIndicatorType.material: if (widget._effectiveValue != null) { return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); } return _buildAnimation(); + case _ActivityIndicatorType.adaptive: + final ThemeData theme = Theme.of(context); + switch (theme.platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return _buildCupertinoIndicator(context); + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + if (widget._effectiveValue != null) { + return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); + } + return _buildAnimation(); + } } - } + }, + ); } } diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart index d10dfe738a3..b2480822493 100644 --- a/packages/flutter/lib/src/rendering/custom_paint.dart +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -1049,6 +1049,12 @@ class RenderCustomPaint extends RenderProxyBox { if (properties.inputType != null) { config.inputType = properties.inputType!; } + if (properties.minValue != null) { + config.minValue = properties.minValue; + } + if (properties.maxValue != null) { + config.maxValue = properties.maxValue; + } if (properties.onTap != null) { config.onTap = properties.onTap; } diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 15da70fd1af..2a01de7f330 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -4945,6 +4945,12 @@ mixin SemanticsAnnotationsMixin on RenderObject { if (_properties.inputType != null) { config.inputType = _properties.inputType!; } + if (_properties.minValue != null) { + config.minValue = _properties.minValue; + } + if (_properties.maxValue != null) { + config.maxValue = _properties.maxValue; + } // Registering _perform* as action handlers instead of the user provided // ones to ensure that changing a user provided handler from a non-null to diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 0c545e4e49e..f6eac48cabd 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -183,14 +183,14 @@ sealed class _DebugSemanticsRoleChecks { SemanticsRole.navigation => _semanticsNavigation, SemanticsRole.region => _semanticsRegion, SemanticsRole.form => _noCheckRequired, + SemanticsRole.loadingSpinner => _noCheckRequired, + SemanticsRole.progressBar => _semanticsProgressBar, // TODO(chunhtai): add checks when the roles are used in framework. // https://github.com/flutter/flutter/issues/159741. SemanticsRole.dragHandle => _unimplemented, SemanticsRole.spinButton => _unimplemented, SemanticsRole.comboBox => _unimplemented, SemanticsRole.tooltip => _unimplemented, - SemanticsRole.loadingSpinner => _unimplemented, - SemanticsRole.progressBar => _unimplemented, SemanticsRole.hotKey => _unimplemented, }(node); @@ -206,6 +206,48 @@ sealed class _DebugSemanticsRoleChecks { static FlutterError? _noCheckRequired(SemanticsNode node) => null; + static FlutterError? _semanticsProgressBar(SemanticsNode node) { + final SemanticsData data = node.getSemanticsData(); + + // Check if value is present + if (data.value.isEmpty) { + return FlutterError('A progress bar must have a value'); + } + + // Check if minValue and maxValue are present + if (data.minValue?.isEmpty ?? true) { + return FlutterError('A progress bar must have a minValue'); + } + + if (data.maxValue?.isEmpty ?? true) { + return FlutterError('A progress bar must have a maxValue'); + } + + // Validate that value is within min and max range + try { + final double currentValue = double.parse(data.value); + final double minVal = double.parse(data.minValue!); + final double maxVal = double.parse(data.maxValue!); + + if (currentValue < minVal || currentValue > maxVal) { + return FlutterError( + 'Progress bar value ($currentValue) must be between minValue ($minVal) and maxValue ($maxVal)', + ); + } + + if (minVal >= maxVal) { + return FlutterError('Progress bar minValue ($minVal) must be less than maxValue ($maxVal)'); + } + } catch (e) { + return FlutterError( + 'Progress bar value, minValue, and maxValue must be valid numbers. ' + 'value: "${data.value}", minValue: "${data.minValue}", maxValue: "${data.maxValue}"', + ); + } + + return null; + } + static FlutterError? _semanticsTab(SemanticsNode node) { final SemanticsData data = node.getSemanticsData(); if (data.flagsCollection.isSelected == Tristate.none) { @@ -1024,6 +1066,8 @@ class SemanticsData with Diagnosticable { required this.hitTestBehavior, required this.inputType, required this.locale, + required this.minValue, + required this.maxValue, this.tags, this.transform, this.customSemanticsActionIds, @@ -1302,6 +1346,12 @@ class SemanticsData with Diagnosticable { /// content of this semantics node. final Locale? locale; + /// {@macro flutter.semantics.SemanticsProperties.maxValue} + final String? maxValue; + + /// {@macro flutter.semantics.SemanticsProperties.minValue} + final String? minValue; + /// Whether [flags] contains the given flag. @Deprecated( 'Use flagsCollection instead. ' @@ -1386,6 +1436,8 @@ class SemanticsData with Diagnosticable { ), ); } + properties.add(StringProperty('minValue', minValue, defaultValue: null)); + properties.add(StringProperty('maxValue', maxValue, defaultValue: null)); } @override @@ -1422,7 +1474,11 @@ class SemanticsData with Diagnosticable { other.inputType == inputType && other.hitTestBehavior == hitTestBehavior && _sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) && - setEquals(controlsNodes, other.controlsNodes); + setEquals(controlsNodes, other.controlsNodes) && + other.traversalParentIdentifier == traversalParentIdentifier && + other.traversalChildIdentifier == traversalChildIdentifier && + other.minValue == minValue && + other.maxValue == maxValue; } @override @@ -1458,6 +1514,10 @@ class SemanticsData with Diagnosticable { controlsNodes == null ? null : Object.hashAll(controlsNodes!), inputType, hitTestBehavior, + traversalParentIdentifier, + traversalChildIdentifier, + minValue, + maxValue, ), ); @@ -1641,6 +1701,8 @@ class SemanticsProperties extends DiagnosticableTree { this.onExpand, this.onCollapse, this.customSemanticsActions, + this.minValue, + this.maxValue, }) : assert( label == null || attributedLabel == null, 'Only one of label or attributedLabel should be provided', @@ -2579,6 +2641,28 @@ class SemanticsProperties extends DiagnosticableTree { /// {@endtemplate} final SemanticsInputType? inputType; + /// {@template flutter.semantics.SemanticsProperties.maxValue} + /// The maximum value of the node. + /// + /// Used in conjunction with [value] to define the current value and range + /// of a node. A typical usage is for progress indicators, where [value] + /// represents the current progress and [maxValue] defines the maximum + /// possible value. + /// + /// {@endtemplate} + final String? maxValue; + + /// {@template flutter.semantics.SemanticsProperties.minValue} + /// The minimum value of the node. + /// + /// Used in conjunction with [value] to define the current value and range + /// of a node. A typical usage is for progress indicators, where [value] + /// represents the current progress and [minValue] defines the minimum + /// possible value. + /// + /// {@endtemplate} + final String? minValue; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -3249,7 +3333,11 @@ class SemanticsNode with DiagnosticableTreeMixin { _linkUrl != config._linkUrl || _role != config.role || _validationResult != config.validationResult || - _hitTestBehavior != config.hitTestBehavior; + _hitTestBehavior != config.hitTestBehavior || + _traversalChildIdentifier != config._traversalChildIdentifier || + _traversalParentIdentifier != config._traversalParentIdentifier || + _minValue != config._minValue || + _maxValue != config._maxValue; } // TAGS, LABELS, ACTIONS @@ -3539,6 +3627,14 @@ class SemanticsNode with DiagnosticableTreeMixin { Set? get controlsNodes => _controlsNodes; Set? _controlsNodes = _kEmptyConfig.controlsNodes; + /// {@macro flutter.semantics.SemanticsProperties.minValue} + String? get minValue => _minValue; + String? _minValue; + + /// {@macro flutter.semantics.SemanticsProperties.maxValue} + String? get maxValue => _maxValue; + String? _maxValue; + /// {@macro flutter.semantics.SemanticsProperties.validationResult} SemanticsValidationResult get validationResult => _validationResult; SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult; @@ -3631,6 +3727,8 @@ class SemanticsNode with DiagnosticableTreeMixin { _inputType = config._inputType; _locale = config.locale; + _minValue = config.minValue; + _maxValue = config.maxValue; _replaceChildren(childrenInInversePaintOrder ?? const []); if (mergeAllDescendantsIntoThisNodeValueChanged) { @@ -3686,6 +3784,8 @@ class SemanticsNode with DiagnosticableTreeMixin { SemanticsInputType inputType = _inputType; final Locale? locale = _locale; final customSemanticsActionIds = {}; + String? minValue = _minValue; + String? maxValue = _maxValue; for (final CustomSemanticsAction action in _customSemanticsActions.keys) { customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); } @@ -3795,6 +3895,9 @@ class SemanticsNode with DiagnosticableTreeMixin { controlsNodes = {...controlsNodes!, ...node._controlsNodes!}; } + minValue ??= node._minValue; + maxValue ??= node._maxValue; + if (validationResult == SemanticsValidationResult.none) { validationResult = node._validationResult; } else if (validationResult == SemanticsValidationResult.valid) { @@ -3844,6 +3947,8 @@ class SemanticsNode with DiagnosticableTreeMixin { hitTestBehavior: hitTestBehavior, inputType: inputType, locale: locale, + minValue: minValue, + maxValue: maxValue, ); } @@ -4016,6 +4121,8 @@ class SemanticsNode with DiagnosticableTreeMixin { hitTestBehavior: data.hitTestBehavior, inputType: data.inputType, locale: data.locale, + minValue: data.minValue ?? '', + maxValue: data.maxValue ?? '', ); _dirty = false; } @@ -4308,6 +4415,8 @@ class SemanticsNode with DiagnosticableTreeMixin { ), ); } + properties.add(StringProperty('minValue', _minValue, defaultValue: null)); + properties.add(StringProperty('maxValue', _maxValue, defaultValue: null)); } /// Returns a string representation of this node and its descendants. @@ -6453,6 +6562,22 @@ class SemanticsConfiguration { _hasBeenAnnotated = true; } + /// {@macro flutter.semantics.SemanticsProperties.maxValue} + String? get maxValue => _maxValue; + String? _maxValue; + set maxValue(String? value) { + _maxValue = value; + _hasBeenAnnotated = true; + } + + /// {@macro flutter.semantics.SemanticsProperties.minValue} + String? get minValue => _minValue; + String? _minValue; + set minValue(String? value) { + _minValue = value; + _hasBeenAnnotated = true; + } + // TAGS /// The set of tags that this configuration wants to add to all child @@ -6560,6 +6685,12 @@ class SemanticsConfiguration { other._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) { return false; } + if (_minValue != null && other._minValue != null) { + return false; + } + if (_maxValue != null && other._maxValue != null) { + return false; + } return true; } @@ -6669,6 +6800,8 @@ class SemanticsConfiguration { _accessiblityFocusBlockType = _accessiblityFocusBlockType._merge( child._accessiblityFocusBlockType, ); + _minValue ??= child._minValue; + _maxValue ??= child._maxValue; if (_hitTestBehavior == ui.SemanticsHitTestBehavior.defer && child._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) { @@ -6721,7 +6854,11 @@ class SemanticsConfiguration { .._controlsNodes = _controlsNodes .._validationResult = _validationResult .._inputType = _inputType - .._hitTestBehavior = _hitTestBehavior; + .._hitTestBehavior = _hitTestBehavior + .._traversalChildIdentifier = _traversalChildIdentifier + .._traversalParentIdentifier = _traversalParentIdentifier + .._minValue = _minValue + .._maxValue = _maxValue; } } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 9fe39791139..66c7914b3a0 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4108,6 +4108,8 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget { required ui.SemanticsHitTestBehavior? hitTestBehavior, required ui.SemanticsInputType? inputType, required Locale? localeForSubtree, + required String? minValue, + required String? maxValue, }) : this.fromProperties( key: key, child: child, @@ -4193,6 +4195,8 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget { validationResult: validationResult, hitTestBehavior: hitTestBehavior, inputType: inputType, + minValue: minValue, + maxValue: maxValue, ), ); @@ -4440,6 +4444,8 @@ class SliverSemantics extends _SemanticsBase { super.hitTestBehavior, super.inputType, super.localeForSubtree, + super.minValue, + super.maxValue, }) : super(child: sliver); /// {@macro flutter.widgets.SemanticsBase.fromProperties} @@ -8023,6 +8029,8 @@ class Semantics extends _SemanticsBase { super.hitTestBehavior, super.inputType, super.localeForSubtree, + super.minValue, + super.maxValue, }); /// {@macro flutter.widgets.SemanticsBase.fromProperties} diff --git a/packages/flutter/test/material/progress_indicator_test.dart b/packages/flutter/test/material/progress_indicator_test.dart index 4044d7b6b2a..15516fb27f9 100644 --- a/packages/flutter/test/material/progress_indicator_test.dart +++ b/packages/flutter/test/material/progress_indicator_test.dart @@ -378,7 +378,12 @@ void main() { expect( tester.getSemantics(find.byType(CircularProgressIndicator)), - matchesSemantics(value: '0%', textDirection: TextDirection.ltr), + matchesSemantics( + value: '0', + textDirection: TextDirection.ltr, + minValue: '0', + maxValue: '100', + ), ); handle.dispose(); }, @@ -936,7 +941,8 @@ void main() { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const label = 'Label'; - const value = '25%'; + const value = '25'; + await tester.pumpWidget( Theme( data: theme, @@ -978,7 +984,7 @@ void main() { expect( tester.getSemantics(find.byKey(key)), - matchesSemantics(textDirection: TextDirection.ltr, label: label, value: '25%'), + matchesSemantics(textDirection: TextDirection.ltr, label: label, value: '25'), ); handle.dispose(); @@ -1033,7 +1039,8 @@ void main() { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const label = 'Label'; - const value = '25%'; + const value = '25'; + await tester.pumpWidget( Theme( data: theme, @@ -1061,7 +1068,7 @@ void main() { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const label = 'Label'; - const value = '25%'; + const value = '25'; await tester.pumpWidget( Theme( data: theme, @@ -1995,7 +2002,7 @@ void main() { expect( tester.getSemantics(find.byType(LinearProgressIndicator)), - matchesSemantics(value: '100%', textDirection: TextDirection.ltr), + matchesSemantics(value: '100', textDirection: TextDirection.ltr), ); // Test value < 0.0 @@ -2005,7 +2012,7 @@ void main() { expect( tester.getSemantics(find.byType(LinearProgressIndicator)), - matchesSemantics(value: '0%', textDirection: TextDirection.ltr), + matchesSemantics(value: '0', textDirection: TextDirection.ltr), ); handle.dispose(); @@ -2021,7 +2028,7 @@ void main() { expect( tester.getSemantics(find.byType(CircularProgressIndicator)), - matchesSemantics(value: '100%', textDirection: TextDirection.ltr), + matchesSemantics(value: '100', textDirection: TextDirection.ltr), ); // Test value < 0.0 @@ -2031,7 +2038,7 @@ void main() { expect( tester.getSemantics(find.byType(CircularProgressIndicator)), - matchesSemantics(value: '0%', textDirection: TextDirection.ltr), + matchesSemantics(value: '0', textDirection: TextDirection.ltr), ); handle.dispose(); @@ -2047,7 +2054,7 @@ void main() { expect( tester.getSemantics(find.byType(RefreshProgressIndicator)), - matchesSemantics(value: '100%', textDirection: TextDirection.ltr), + matchesSemantics(value: '100', textDirection: TextDirection.ltr), ); // Test value < 0.0 @@ -2057,7 +2064,7 @@ void main() { expect( tester.getSemantics(find.byType(RefreshProgressIndicator)), - matchesSemantics(value: '0%', textDirection: TextDirection.ltr), + matchesSemantics(value: '0', textDirection: TextDirection.ltr), ); handle.dispose(); diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index b34f74baf94..7c89b10c813 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -720,7 +720,9 @@ void main() { ' scrollPosition: null\n' ' scrollExtentMax: null\n' ' indexInParent: null\n' - ' headingLevel: 0\n', + ' headingLevel: 0\n' + ' minValue: null\n' + ' maxValue: null\n', ); final config = SemanticsConfiguration() @@ -868,7 +870,9 @@ void main() { ' scrollPosition: null\n' ' scrollExtentMax: null\n' ' indexInParent: null\n' - ' headingLevel: 0\n', + ' headingLevel: 0\n' + ' minValue: null\n' + ' maxValue: null\n', ); }); diff --git a/packages/flutter/test/semantics/semantics_update_test.dart b/packages/flutter/test/semantics/semantics_update_test.dart index ec7c738d1eb..9399b5cb58d 100644 --- a/packages/flutter/test/semantics/semantics_update_test.dart +++ b/packages/flutter/test/semantics/semantics_update_test.dart @@ -322,6 +322,8 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde ui.SemanticsHitTestBehavior hitTestBehavior = ui.SemanticsHitTestBehavior.defer, required ui.SemanticsInputType inputType, required ui.Locale? locale, + required String minValue, + required String maxValue, }) { // Makes sure we don't send the same id twice. assert(!observations.containsKey(id)); diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index f03a4ec9ad0..b144d0a50a9 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -726,6 +726,8 @@ class SemanticsTester { double? scrollExtentMin, int? currentValueLength, int? maxValueLength, + String? maxValue, + String? minValue, SemanticsNode? ancestor, SemanticsInputType? inputType, }) { @@ -823,6 +825,12 @@ class SemanticsTester { if (inputType != null && node.inputType != inputType) { return false; } + if (maxValue != null && node.maxValue != maxValue) { + return false; + } + if (minValue != null && node.minValue != minValue) { + return false; + } return true; } @@ -1133,6 +1141,8 @@ class _IncludesNodeWith extends Matcher { this.maxValueLength, this.currentValueLength, this.inputType, + this.minValue, + this.maxValue, }) : assert( label != null || value != null || @@ -1148,6 +1158,7 @@ class _IncludesNodeWith extends Matcher { maxValueLength != null || currentValueLength != null || inputType != null, + minValue != null || maxValue != null, ); final AttributedString? attributedLabel; final AttributedString? attributedValue; @@ -1168,6 +1179,8 @@ class _IncludesNodeWith extends Matcher { final int? currentValueLength; final int? maxValueLength; final SemanticsInputType? inputType; + final String? minValue; + final String? maxValue; @override bool matches(covariant SemanticsTester item, Map matchState) { @@ -1192,6 +1205,8 @@ class _IncludesNodeWith extends Matcher { currentValueLength: currentValueLength, maxValueLength: maxValueLength, inputType: inputType, + minValue: minValue, + maxValue: maxValue, ) .isNotEmpty; } @@ -1228,6 +1243,8 @@ class _IncludesNodeWith extends Matcher { if (currentValueLength != null) 'currentValueLength "$currentValueLength"', if (maxValueLength != null) 'maxValueLength "$maxValueLength"', if (inputType != null) 'inputType $inputType', + if (minValue != null) 'minValue "$minValue"', + if (maxValue != null) 'maxValue "$maxValue"', ]; return strings.join(', '); } @@ -1257,6 +1274,8 @@ Matcher includesNodeWith({ int? maxValueLength, int? currentValueLength, SemanticsInputType? inputType, + String? minValue, + String? maxValue, }) { return _IncludesNodeWith( label: label, @@ -1278,5 +1297,7 @@ Matcher includesNodeWith({ maxValueLength: maxValueLength, currentValueLength: currentValueLength, inputType: inputType, + minValue: minValue, + maxValue: maxValue, ); } diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 389d29c4a51..7faf4266b8b 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -687,6 +687,8 @@ Matcher matchesSemantics({ int? currentValueLength, SemanticsValidationResult validationResult = SemanticsValidationResult.none, ui.SemanticsInputType? inputType, + String? maxValue, + String? minValue, // Flags // bool hasCheckedState = false, bool isChecked = false, @@ -772,6 +774,8 @@ Matcher matchesSemantics({ currentValueLength: currentValueLength, validationResult: validationResult, inputType: inputType, + minValue: minValue, + maxValue: maxValue, // Flags hasCheckedState: hasCheckedState, isChecked: isChecked, @@ -887,6 +891,8 @@ Matcher containsSemantics({ int? currentValueLength, SemanticsValidationResult? validationResult, ui.SemanticsInputType? inputType, + String? maxValue, + String? minValue, // Flags bool? hasCheckedState, bool? isChecked, @@ -972,6 +978,8 @@ Matcher containsSemantics({ currentValueLength: currentValueLength, validationResult: validationResult, inputType: inputType, + minValue: minValue, + maxValue: maxValue, // Flags hasCheckedState: hasCheckedState, isChecked: isChecked, @@ -2404,6 +2412,8 @@ class _MatchesSemanticsData extends Matcher { required this.currentValueLength, required this.validationResult, required this.inputType, + required this.minValue, + required this.maxValue, // Flags required bool? hasCheckedState, required bool? isChecked, @@ -2551,6 +2561,8 @@ class _MatchesSemanticsData extends Matcher { final ui.SemanticsInputType? inputType; final List? children; final SemanticsValidationResult? validationResult; + final String? maxValue; + final String? minValue; /// There are three possible states for these two maps: /// @@ -2664,6 +2676,12 @@ class _MatchesSemanticsData extends Matcher { if (validationResult != null) { description.add(' with validation result: $validationResult'); } + if (minValue != null) { + description.add(' with minValue: $minValue'); + } + if (maxValue != null) { + description.add(' with maxValue: $maxValue'); + } if (children != null) { description.add(' with children:\n '); final List<_MatchesSemanticsData> childMatches = children!.cast<_MatchesSemanticsData>(); @@ -2823,6 +2841,12 @@ class _MatchesSemanticsData extends Matcher { if (inputType != null && inputType != data.inputType) { return failWithDescription(matchState, 'inputType was: ${data.inputType}'); } + if (minValue != null && minValue != data.minValue) { + return failWithDescription(matchState, 'minValue was: ${data.minValue}'); + } + if (maxValue != null && maxValue != data.maxValue) { + return failWithDescription(matchState, 'maxValue was: ${data.maxValue}'); + } if (actions.isNotEmpty) { final unexpectedActions = []; final missingActions = []; diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index c8eabeb8212..a1d6058b162 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -756,6 +756,8 @@ void main() { hitTestBehavior: ui.SemanticsHitTestBehavior.defer, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final node = _FakeSemanticsNode(data); @@ -948,6 +950,8 @@ void main() { inputType: ui.SemanticsInputType.none, locale: null, hitTestBehavior: ui.SemanticsHitTestBehavior.defer, + maxValue: '', + minValue: '', ); final node = _FakeSemanticsNode(data); @@ -1114,6 +1118,8 @@ void main() { inputType: ui.SemanticsInputType.none, locale: null, hitTestBehavior: ui.SemanticsHitTestBehavior.defer, + minValue: '', + maxValue: '', ); final node = _FakeSemanticsNode(data); @@ -1417,6 +1423,8 @@ void main() { hitTestBehavior: ui.SemanticsHitTestBehavior.defer, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final node = _FakeSemanticsNode(data); @@ -1520,6 +1528,8 @@ void main() { hitTestBehavior: ui.SemanticsHitTestBehavior.defer, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final node = _FakeSemanticsNode(data); @@ -1628,6 +1638,8 @@ void main() { hitTestBehavior: ui.SemanticsHitTestBehavior.defer, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final emptyNode = _FakeSemanticsNode(emptyData); @@ -1664,6 +1676,8 @@ void main() { hitTestBehavior: ui.SemanticsHitTestBehavior.defer, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final fullNode = _FakeSemanticsNode(fullData); @@ -1799,6 +1813,8 @@ void main() { hitTestBehavior: ui.SemanticsHitTestBehavior.defer, inputType: ui.SemanticsInputType.none, locale: null, + minValue: '0', + maxValue: '0', ); final node = _FakeSemanticsNode(data);