diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 0f7068b4e5c..e36cdfcf190 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -1290,6 +1290,9 @@ class _TimePickerInput extends StatefulWidget { Key? key, required this.initialSelectedTime, required this.helpText, + required this.errorInvalidText, + required this.hourLabelText, + required this.minuteLabelText, required this.autofocusHour, required this.autofocusMinute, required this.onChanged, @@ -1304,6 +1307,15 @@ class _TimePickerInput extends StatefulWidget { /// Optionally provide your own help text to the time picker. final String? helpText; + /// Optionally provide your own validation error text. + final String? errorInvalidText; + + /// Optionally provide your own hour label text. + final String? hourLabelText; + + /// Optionally provide your own minute label text. + final String? minuteLabelText; + final bool? autofocusHour; final bool? autofocusMinute; @@ -1480,12 +1492,13 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi validator: _validateHour, onSavedSubmitted: _handleHourSavedSubmitted, onChanged: _handleHourChanged, + hourLabelText: widget.hourLabelText, ), const SizedBox(height: 8.0), if (!hourHasError.value && !minuteHasError.value) ExcludeSemantics( child: Text( - MaterialLocalizations.of(context).timePickerHourLabel, + widget.hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel, style: theme.textTheme.caption, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -1511,12 +1524,13 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi autofocus: widget.autofocusMinute, validator: _validateMinute, onSavedSubmitted: _handleMinuteSavedSubmitted, + minuteLabelText: widget.minuteLabelText, ), const SizedBox(height: 8.0), if (!hourHasError.value && !minuteHasError.value) ExcludeSemantics( child: Text( - MaterialLocalizations.of(context).timePickerMinuteLabel, + widget.minuteLabelText ?? MaterialLocalizations.of(context).timePickerMinuteLabel, style: theme.textTheme.caption, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -1540,7 +1554,7 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi ), if (hourHasError.value || minuteHasError.value) Text( - MaterialLocalizations.of(context).invalidTimeLabel, + widget.errorInvalidText ?? MaterialLocalizations.of(context).invalidTimeLabel, style: theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.error), ) else @@ -1560,6 +1574,7 @@ class _HourTextField extends StatelessWidget { required this.validator, required this.onSavedSubmitted, required this.onChanged, + required this.hourLabelText, this.restorationId, }) : super(key: key); @@ -1569,6 +1584,7 @@ class _HourTextField extends StatelessWidget { final FormFieldValidator validator; final ValueChanged onSavedSubmitted; final ValueChanged onChanged; + final String? hourLabelText; final String? restorationId; @override @@ -1579,7 +1595,7 @@ class _HourTextField extends StatelessWidget { isHour: true, autofocus: autofocus, style: style, - semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel, + semanticHintText: hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel, validator: validator, onSavedSubmitted: onSavedSubmitted, onChanged: onChanged, @@ -1595,6 +1611,7 @@ class _MinuteTextField extends StatelessWidget { required this.autofocus, required this.validator, required this.onSavedSubmitted, + required this.minuteLabelText, this.restorationId, }) : super(key: key); @@ -1603,6 +1620,7 @@ class _MinuteTextField extends StatelessWidget { final bool? autofocus; final FormFieldValidator validator; final ValueChanged onSavedSubmitted; + final String? minuteLabelText; final String? restorationId; @override @@ -1613,7 +1631,7 @@ class _MinuteTextField extends StatelessWidget { isHour: false, autofocus: autofocus, style: style, - semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel, + semanticHintText: minuteLabelText ?? MaterialLocalizations.of(context).timePickerMinuteLabel, validator: validator, onSavedSubmitted: onSavedSubmitted, ); @@ -1784,6 +1802,9 @@ class TimePickerDialog extends StatefulWidget { this.cancelText, this.confirmText, this.helpText, + this.errorInvalidText, + this.hourLabelText, + this.minuteLabelText, this.restorationId, this.initialEntryMode = TimePickerEntryMode.dial, }) : assert(initialTime != null), @@ -1808,6 +1829,15 @@ class TimePickerDialog extends StatefulWidget { /// Optionally provide your own help text to the header of the time picker. final String? helpText; + /// Optionally provide your own validation error text. + final String? errorInvalidText; + + /// Optionally provide your own hour label text. + final String? hourLabelText; + + /// Optionally provide your own minute label text. + final String? minuteLabelText; + /// Restoration ID to save and restore the state of the [TimePickerDialog]. /// /// If it is non-null, the time picker will persist and restore the @@ -2220,6 +2250,9 @@ class _TimePickerDialogState extends State with RestorationMix _TimePickerInput( initialSelectedTime: _selectedTime.value, helpText: widget.helpText, + errorInvalidText: widget.errorInvalidText, + hourLabelText: widget.hourLabelText, + minuteLabelText: widget.minuteLabelText, autofocusHour: _autofocusHour.value, autofocusMinute: _autofocusMinute.value, onChanged: _handleTimeChanged, @@ -2286,8 +2319,9 @@ class _TimePickerDialogState extends State with RestorationMix /// determine the initial time entry selection of the picker (either a clock /// dial or text input). /// -/// Optional strings for the [helpText], [cancelText], and [confirmText] can be -/// provided to override the default values. +/// Optional strings for the [helpText], [cancelText], [errorInvalidText], +/// [hourLabelText], [minuteLabelText] and [confirmText] can be provided to +/// override the default values. /// /// By default, the time picker gets its colors from the overall theme's /// [ColorScheme]. The time picker can be further customized by providing a @@ -2342,6 +2376,9 @@ Future showTimePicker({ String? cancelText, String? confirmText, String? helpText, + String? errorInvalidText, + String? hourLabelText, + String? minuteLabelText, RouteSettings? routeSettings, }) async { assert(context != null); @@ -2356,6 +2393,9 @@ Future showTimePicker({ cancelText: cancelText, confirmText: confirmText, helpText: helpText, + errorInvalidText: errorInvalidText, + hourLabelText: hourLabelText, + minuteLabelText: minuteLabelText, ); return showDialog( context: context, diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 3858085fe12..a100b08412c 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -861,6 +861,31 @@ void _testsInput() { expect(find.text(helpText), findsOneWidget); }); + testWidgets('Hour label text is used - Input', (WidgetTester tester) async { + const String hourLabelText = 'Custom hour label'; + await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, hourLabelText: hourLabelText); + expect(find.text(hourLabelText), findsOneWidget); + }); + + + testWidgets('Minute label text is used - Input', (WidgetTester tester) async { + const String minuteLabelText = 'Custom minute label'; + await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, minuteLabelText: minuteLabelText); + expect(find.text(minuteLabelText), findsOneWidget); + }); + + testWidgets('Invalid error text is used - Input', (WidgetTester tester) async { + const String errorInvalidText = 'Custom validation error'; + await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, errorInvalidText: errorInvalidText); + // Input invalid time (hour) to force validation error + await tester.enterText(find.byType(TextField).first, '88'); + final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(TextButton).first)); + // Tap the ok button to trigger the validation error with custom translation + await tester.tap(find.text(materialLocalizations.okButtonLabel)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.text(errorInvalidText), findsOneWidget); + }); + testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async { await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input); await tester.tap(find.byIcon(Icons.access_time)); @@ -1124,6 +1149,9 @@ Future mediaQueryBoilerplate( double textScaleFactor = 1.0, TimePickerEntryMode entryMode = TimePickerEntryMode.dial, String? helpText, + String? hourLabelText, + String? minuteLabelText, + String? errorInvalidText, bool accessibleNavigation = false, }) async { await tester.pumpWidget( @@ -1152,6 +1180,9 @@ Future mediaQueryBoilerplate( initialTime: initialTime, initialEntryMode: entryMode, helpText: helpText, + hourLabelText: hourLabelText, + minuteLabelText: minuteLabelText, + errorInvalidText: errorInvalidText ); }, child: const Text('X'),