From c150c5a03dd8f17d050521a2bd94e61d6b56f997 Mon Sep 17 00:00:00 2001 From: Benjamin Lempereur Date: Wed, 8 Jun 2022 22:18:14 +0200 Subject: [PATCH] TimePicker : Ability to define dialOnly / inputOnly modes (#104491) --- .../flutter/lib/src/material/time_picker.dart | 49 ++++++++++++++----- .../test/material/time_picker_test.dart | 21 +++++++- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index d15d763f9fb..c8fc468944c 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -65,11 +65,25 @@ const ShapeBorder _kDefaultShape = RoundedRectangleBorder(borderRadius: _kDefaul /// TimePickerEntryMode.input] mode, [TextField]s are displayed and the user /// types in the time they wish to select. enum TimePickerEntryMode { - /// Tapping/dragging on a clock dial. + /// User picks time from a clock dial. + /// + /// Can switch to [input] by activating a mode button in the dialog. dial, - /// Text input. + /// User can input the time by typing it into text fields. + /// + /// Can switch to [dial] by activating a mode button in the dialog. input, + + /// User can only pick time from a clock dial. + /// + /// There is no user interface to switch to another mode. + dialOnly, + + /// User can only input the time by typing it into text fields. + /// + /// There is no user interface to switch to another mode. + inputOnly } /// Provides properties for rendering time picker header fragments. @@ -2055,6 +2069,10 @@ class _TimePickerDialogState extends State with RestorationMix _autofocusMinute.value = false; _entryMode.value = TimePickerEntryMode.dial; break; + case TimePickerEntryMode.dialOnly: + case TimePickerEntryMode.inputOnly: + FlutterError('Can not change entry mode from $_entryMode'); + break; } }); } @@ -2118,7 +2136,7 @@ class _TimePickerDialogState extends State with RestorationMix } void _handleOk() { - if (_entryMode.value == TimePickerEntryMode.input) { + if (_entryMode.value == TimePickerEntryMode.input || _entryMode.value == TimePickerEntryMode.inputOnly) { final FormState form = _formKey.currentState!; if (!form.validate()) { setState(() { _autovalidateMode.value = AutovalidateMode.always; }); @@ -2141,6 +2159,7 @@ class _TimePickerDialogState extends State with RestorationMix final double timePickerHeight; switch (_entryMode.value) { case TimePickerEntryMode.dial: + case TimePickerEntryMode.dialOnly: switch (orientation) { case Orientation.portrait: timePickerWidth = _kTimePickerWidthPortrait; @@ -2157,6 +2176,7 @@ class _TimePickerDialogState extends State with RestorationMix } break; case TimePickerEntryMode.input: + case TimePickerEntryMode.inputOnly: timePickerWidth = _kTimePickerWidthPortrait; timePickerHeight = _kTimePickerHeightInput; break; @@ -2177,16 +2197,17 @@ class _TimePickerDialogState extends State with RestorationMix final Widget actions = Row( children: [ const SizedBox(width: 10.0), - IconButton( - color: TimePickerTheme.of(context).entryModeIconColor ?? theme.colorScheme.onSurface.withOpacity( - theme.colorScheme.brightness == Brightness.dark ? 1.0 : 0.6, + if (_entryMode.value == TimePickerEntryMode.dial || _entryMode.value == TimePickerEntryMode.input) + IconButton( + color: TimePickerTheme.of(context).entryModeIconColor ?? theme.colorScheme.onSurface.withOpacity( + theme.colorScheme.brightness == Brightness.dark ? 1.0 : 0.6, + ), + onPressed: _handleEntryModeToggle, + icon: Icon(_entryMode.value == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time), + tooltip: _entryMode.value == TimePickerEntryMode.dial + ? MaterialLocalizations.of(context).inputTimeModeButtonLabel + : MaterialLocalizations.of(context).dialModeButtonLabel, ), - onPressed: _handleEntryModeToggle, - icon: Icon(_entryMode.value == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time), - tooltip: _entryMode.value == TimePickerEntryMode.dial - ? MaterialLocalizations.of(context).inputTimeModeButtonLabel - : MaterialLocalizations.of(context).dialModeButtonLabel, - ), Expanded( child: Container( alignment: AlignmentDirectional.centerEnd, @@ -2214,6 +2235,7 @@ class _TimePickerDialogState extends State with RestorationMix final Widget picker; switch (_entryMode.value) { case TimePickerEntryMode.dial: + case TimePickerEntryMode.dialOnly: final Widget dial = Padding( padding: orientation == Orientation.portrait ? const EdgeInsets.symmetric(horizontal: 36, vertical: 24) : const EdgeInsets.all(24), child: ExcludeSemantics( @@ -2280,6 +2302,7 @@ class _TimePickerDialogState extends State with RestorationMix } break; case TimePickerEntryMode.input: + case TimePickerEntryMode.inputOnly: picker = Form( key: _formKey, autovalidateMode: _autovalidateMode.value, @@ -2313,7 +2336,7 @@ class _TimePickerDialogState extends State with RestorationMix backgroundColor: TimePickerTheme.of(context).backgroundColor ?? theme.colorScheme.surface, insetPadding: EdgeInsets.symmetric( horizontal: 16.0, - vertical: _entryMode.value == TimePickerEntryMode.input ? 0.0 : 24.0, + vertical: (_entryMode.value == TimePickerEntryMode.input || _entryMode.value == TimePickerEntryMode.inputOnly) ? 0.0 : 24.0, ), child: AnimatedContainer( width: dialogSize.width, diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 8efafcbe28d..3447bf1ea8c 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -1007,13 +1007,32 @@ void _testsInput() { expect(find.text(errorInvalidText), findsOneWidget); }); - testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async { + testWidgets('Can switch from input to dial entry mode', (WidgetTester tester) async { await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input); await tester.tap(find.byIcon(Icons.access_time)); await tester.pumpAndSettle(); expect(find.byType(TextField), findsNothing); }); + testWidgets('Can switch from dial to input entry mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, true); + await tester.tap(find.byIcon(Icons.keyboard)); + await tester.pumpAndSettle(); + expect(find.byType(TextField), findsWidgets); + }); + + testWidgets('Can not switch out of inputOnly mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.inputOnly); + expect(find.byType(TextField), findsWidgets); + expect(find.byIcon(Icons.access_time), findsNothing); + }); + + testWidgets('Can not switch out of dialOnly mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.dialOnly); + expect(find.byType(TextField), findsNothing); + expect(find.byIcon(Icons.keyboard), findsNothing); + }); + testWidgets('Switching to dial entry mode triggers entry callback', (WidgetTester tester) async { bool triggeredCallback = false;