From 9e715205b737275707ac1a8cd784fb9dd9b58f75 Mon Sep 17 00:00:00 2001 From: Rami <2364772+rami-a@users.noreply.github.com> Date: Fri, 2 Oct 2020 13:53:36 -0400 Subject: [PATCH] [Time Picker] Double tapping hours/minutes will switch time picker to input mode (#67076) --- .../flutter/lib/src/material/time_picker.dart | 50 +++++++++++ .../test/material/time_picker_test.dart | 83 +++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 747fcf0c51..42e24503c2 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -82,6 +82,8 @@ class _TimePickerFragmentContext { @required this.mode, @required this.onTimeChange, @required this.onModeChange, + @required this.onHourDoubleTapped, + @required this.onMinuteDoubleTapped, @required this.use24HourDials, }) : assert(selectedTime != null), assert(mode != null), @@ -93,6 +95,8 @@ class _TimePickerFragmentContext { final _TimePickerMode mode; final ValueChanged onTimeChange; final ValueChanged<_TimePickerMode> onModeChange; + final GestureTapCallback onHourDoubleTapped; + final GestureTapCallback onMinuteDoubleTapped; final bool use24HourDials; } @@ -103,6 +107,8 @@ class _TimePickerHeader extends StatelessWidget { @required this.orientation, @required this.onModeChanged, @required this.onChanged, + @required this.onHourDoubleTapped, + @required this.onMinuteDoubleTapped, @required this.use24HourDials, @required this.helpText, }) : assert(selectedTime != null), @@ -115,6 +121,8 @@ class _TimePickerHeader extends StatelessWidget { final Orientation orientation; final ValueChanged<_TimePickerMode> onModeChanged; final ValueChanged onChanged; + final GestureTapCallback onHourDoubleTapped; + final GestureTapCallback onMinuteDoubleTapped; final bool use24HourDials; final String helpText; @@ -136,6 +144,8 @@ class _TimePickerHeader extends StatelessWidget { mode: mode, onTimeChange: onChanged, onModeChange: _handleChangeMode, + onHourDoubleTapped: onHourDoubleTapped, + onMinuteDoubleTapped: onMinuteDoubleTapped, use24HourDials: use24HourDials, ); @@ -246,6 +256,7 @@ class _HourMinuteControl extends StatelessWidget { const _HourMinuteControl({ @required this.text, @required this.onTap, + @required this.onDoubleTap, @required this.isSelected, }) : assert(text != null), assert(onTap != null), @@ -253,6 +264,7 @@ class _HourMinuteControl extends StatelessWidget { final String text; final GestureTapCallback onTap; + final GestureTapCallback onDoubleTap; final bool isSelected; @override @@ -284,6 +296,7 @@ class _HourMinuteControl extends StatelessWidget { shape: shape, child: InkWell( onTap: onTap, + onDoubleTap: isSelected ? onDoubleTap : null, child: Center( child: Text( text, @@ -359,6 +372,7 @@ class _HourControl extends StatelessWidget { isSelected: fragmentContext.mode == _TimePickerMode.hour, text: formattedHour, onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context), + onDoubleTap: fragmentContext.onHourDoubleTapped, ), ); } @@ -448,6 +462,7 @@ class _MinuteControl extends StatelessWidget { isSelected: fragmentContext.mode == _TimePickerMode.minute, text: formattedMinute, onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context), + onDoubleTap: fragmentContext.onMinuteDoubleTapped, ), ); } @@ -1265,6 +1280,8 @@ class _TimePickerInput extends StatefulWidget { Key key, @required this.initialSelectedTime, @required this.helpText, + @required this.autofocusHour, + @required this.autofocusMinute, @required this.onChanged, }) : assert(initialSelectedTime != null), assert(onChanged != null), @@ -1276,6 +1293,10 @@ class _TimePickerInput extends StatefulWidget { /// Optionally provide your own help text to the time picker. final String helpText; + final bool autofocusHour; + + final bool autofocusMinute; + final ValueChanged onChanged; @override @@ -1430,6 +1451,7 @@ class _TimePickerInputState extends State<_TimePickerInput> { _HourTextField( selectedTime: _selectedTime, style: hourMinuteStyle, + autofocus: widget.autofocusHour, validator: _validateHour, onSavedSubmitted: _handleHourSavedSubmitted, onChanged: _handleHourChanged, @@ -1460,6 +1482,7 @@ class _TimePickerInputState extends State<_TimePickerInput> { _MinuteTextField( selectedTime: _selectedTime, style: hourMinuteStyle, + autofocus: widget.autofocusMinute, validator: _validateMinute, onSavedSubmitted: _handleMinuteSavedSubmitted, ), @@ -1507,6 +1530,7 @@ class _HourTextField extends StatelessWidget { Key key, @required this.selectedTime, @required this.style, + @required this.autofocus, @required this.validator, @required this.onSavedSubmitted, @required this.onChanged, @@ -1514,6 +1538,7 @@ class _HourTextField extends StatelessWidget { final TimeOfDay selectedTime; final TextStyle style; + final bool autofocus; final FormFieldValidator validator; final ValueChanged onSavedSubmitted; final ValueChanged onChanged; @@ -1523,6 +1548,7 @@ class _HourTextField extends StatelessWidget { return _HourMinuteTextField( selectedTime: selectedTime, isHour: true, + autofocus: autofocus, style: style, semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel, validator: validator, @@ -1537,12 +1563,14 @@ class _MinuteTextField extends StatelessWidget { Key key, @required this.selectedTime, @required this.style, + @required this.autofocus, @required this.validator, @required this.onSavedSubmitted, }) : super(key: key); final TimeOfDay selectedTime; final TextStyle style; + final bool autofocus; final FormFieldValidator validator; final ValueChanged onSavedSubmitted; @@ -1551,6 +1579,7 @@ class _MinuteTextField extends StatelessWidget { return _HourMinuteTextField( selectedTime: selectedTime, isHour: false, + autofocus: autofocus, style: style, semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel, validator: validator, @@ -1564,6 +1593,7 @@ class _HourMinuteTextField extends StatefulWidget { Key key, @required this.selectedTime, @required this.isHour, + @required this.autofocus, @required this.style, @required this.semanticHintText, @required this.validator, @@ -1573,6 +1603,7 @@ class _HourMinuteTextField extends StatefulWidget { final TimeOfDay selectedTime; final bool isHour; + final bool autofocus; final TextStyle style; final String semanticHintText; final FormFieldValidator validator; @@ -1658,6 +1689,7 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> { child: MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), child: TextFormField( + autofocus: widget.autofocus ?? false, expands: true, maxLines: null, inputFormatters: [ @@ -1746,6 +1778,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { _TimePickerMode _mode = _TimePickerMode.hour; _TimePickerMode _lastModeAnnounced; bool _autoValidate; + bool _autofocusHour; + bool _autofocusMinute; TimeOfDay get selectedTime => _selectedTime; TimeOfDay _selectedTime; @@ -1788,6 +1822,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { break; case TimePickerEntryMode.input: _formKey.currentState.save(); + _autofocusHour = false; + _autofocusMinute = false; _entryMode = TimePickerEntryMode.dial; break; } @@ -1833,6 +1869,16 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { }); } + void _handleHourDoubleTapped() { + _autofocusHour = true; + _handleEntryModeToggle(); + } + + void _handleMinuteDoubleTapped() { + _autofocusMinute = true; + _handleEntryModeToggle(); + } + void _handleHourSelected() { setState(() { _mode = _TimePickerMode.minute; @@ -1962,6 +2008,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { orientation: orientation, onModeChanged: _handleModeChanged, onChanged: _handleTimeChanged, + onHourDoubleTapped: _handleHourDoubleTapped, + onMinuteDoubleTapped: _handleMinuteDoubleTapped, use24HourDials: use24HourDials, helpText: widget.helpText, ); @@ -2014,6 +2062,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { _TimePickerInput( initialSelectedTime: _selectedTime, helpText: widget.helpText, + autofocusHour: _autofocusHour, + autofocusMinute: _autofocusMinute, onChanged: _handleTimeChanged, ), actions, diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 23a08f13af..6e2c453ca5 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -803,6 +803,89 @@ void _testsInput() { expect(find.byType(TextField), findsNothing); }); + testWidgets('Can double tap hours (when selected) to enter input mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial); + final Finder hourFinder = find.ancestor( + of: find.text('7'), + matching: find.byType(InkWell), + ); + + expect(find.byType(TextField), findsNothing); + + // Double tap the hour. + await tester.tap(hourFinder); + await tester.pump(const Duration(milliseconds: 100)); + await tester.tap(hourFinder); + await tester.pumpAndSettle(); + + expect(find.byType(TextField), findsWidgets); + }); + + testWidgets('Can not double tap hours (when not selected) to enter input mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial); + final Finder hourFinder = find.ancestor( + of: find.text('7'), + matching: find.byType(InkWell), + ); + final Finder minuteFinder = find.ancestor( + of: find.text('00'), + matching: find.byType(InkWell), + ); + + expect(find.byType(TextField), findsNothing); + + // Switch to minutes mode. + await tester.tap(minuteFinder); + await tester.pumpAndSettle(); + + // Double tap the hour. + await tester.tap(hourFinder); + await tester.pump(const Duration(milliseconds: 100)); + await tester.tap(hourFinder); + await tester.pumpAndSettle(); + + expect(find.byType(TextField), findsNothing); + }); + + testWidgets('Can double tap minutes (when selected) to enter input mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial); + final Finder minuteFinder = find.ancestor( + of: find.text('00'), + matching: find.byType(InkWell), + ); + + expect(find.byType(TextField), findsNothing); + + // Switch to minutes mode. + await tester.tap(minuteFinder); + await tester.pumpAndSettle(); + + // Double tap the minutes. + await tester.tap(minuteFinder); + await tester.pump(const Duration(milliseconds: 100)); + await tester.tap(minuteFinder); + await tester.pumpAndSettle(); + + expect(find.byType(TextField), findsWidgets); + }); + + testWidgets('Can not double tap minutes (when not selected) to enter input mode', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial); + final Finder minuteFinder = find.ancestor( + of: find.text('00'), + matching: find.byType(InkWell), + ); + + expect(find.byType(TextField), findsNothing); + + // Double tap the minutes. + await tester.tap(minuteFinder); + await tester.pump(const Duration(milliseconds: 100)); + await tester.tap(minuteFinder); + await tester.pumpAndSettle(); + + expect(find.byType(TextField), findsNothing); + }); testWidgets('Entered text returns time', (WidgetTester tester) async { TimeOfDay result;