[Time Picker] Double tapping hours/minutes will switch time picker to input mode (#67076)

This commit is contained in:
Rami 2020-10-02 13:53:36 -04:00 committed by GitHub
parent a2eef79fe4
commit 9e715205b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 133 additions and 0 deletions

View file

@ -82,6 +82,8 @@ class _TimePickerFragmentContext {
@required this.mode, @required this.mode,
@required this.onTimeChange, @required this.onTimeChange,
@required this.onModeChange, @required this.onModeChange,
@required this.onHourDoubleTapped,
@required this.onMinuteDoubleTapped,
@required this.use24HourDials, @required this.use24HourDials,
}) : assert(selectedTime != null), }) : assert(selectedTime != null),
assert(mode != null), assert(mode != null),
@ -93,6 +95,8 @@ class _TimePickerFragmentContext {
final _TimePickerMode mode; final _TimePickerMode mode;
final ValueChanged<TimeOfDay> onTimeChange; final ValueChanged<TimeOfDay> onTimeChange;
final ValueChanged<_TimePickerMode> onModeChange; final ValueChanged<_TimePickerMode> onModeChange;
final GestureTapCallback onHourDoubleTapped;
final GestureTapCallback onMinuteDoubleTapped;
final bool use24HourDials; final bool use24HourDials;
} }
@ -103,6 +107,8 @@ class _TimePickerHeader extends StatelessWidget {
@required this.orientation, @required this.orientation,
@required this.onModeChanged, @required this.onModeChanged,
@required this.onChanged, @required this.onChanged,
@required this.onHourDoubleTapped,
@required this.onMinuteDoubleTapped,
@required this.use24HourDials, @required this.use24HourDials,
@required this.helpText, @required this.helpText,
}) : assert(selectedTime != null), }) : assert(selectedTime != null),
@ -115,6 +121,8 @@ class _TimePickerHeader extends StatelessWidget {
final Orientation orientation; final Orientation orientation;
final ValueChanged<_TimePickerMode> onModeChanged; final ValueChanged<_TimePickerMode> onModeChanged;
final ValueChanged<TimeOfDay> onChanged; final ValueChanged<TimeOfDay> onChanged;
final GestureTapCallback onHourDoubleTapped;
final GestureTapCallback onMinuteDoubleTapped;
final bool use24HourDials; final bool use24HourDials;
final String helpText; final String helpText;
@ -136,6 +144,8 @@ class _TimePickerHeader extends StatelessWidget {
mode: mode, mode: mode,
onTimeChange: onChanged, onTimeChange: onChanged,
onModeChange: _handleChangeMode, onModeChange: _handleChangeMode,
onHourDoubleTapped: onHourDoubleTapped,
onMinuteDoubleTapped: onMinuteDoubleTapped,
use24HourDials: use24HourDials, use24HourDials: use24HourDials,
); );
@ -246,6 +256,7 @@ class _HourMinuteControl extends StatelessWidget {
const _HourMinuteControl({ const _HourMinuteControl({
@required this.text, @required this.text,
@required this.onTap, @required this.onTap,
@required this.onDoubleTap,
@required this.isSelected, @required this.isSelected,
}) : assert(text != null), }) : assert(text != null),
assert(onTap != null), assert(onTap != null),
@ -253,6 +264,7 @@ class _HourMinuteControl extends StatelessWidget {
final String text; final String text;
final GestureTapCallback onTap; final GestureTapCallback onTap;
final GestureTapCallback onDoubleTap;
final bool isSelected; final bool isSelected;
@override @override
@ -284,6 +296,7 @@ class _HourMinuteControl extends StatelessWidget {
shape: shape, shape: shape,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onDoubleTap: isSelected ? onDoubleTap : null,
child: Center( child: Center(
child: Text( child: Text(
text, text,
@ -359,6 +372,7 @@ class _HourControl extends StatelessWidget {
isSelected: fragmentContext.mode == _TimePickerMode.hour, isSelected: fragmentContext.mode == _TimePickerMode.hour,
text: formattedHour, text: formattedHour,
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context), onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
onDoubleTap: fragmentContext.onHourDoubleTapped,
), ),
); );
} }
@ -448,6 +462,7 @@ class _MinuteControl extends StatelessWidget {
isSelected: fragmentContext.mode == _TimePickerMode.minute, isSelected: fragmentContext.mode == _TimePickerMode.minute,
text: formattedMinute, text: formattedMinute,
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context), onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
onDoubleTap: fragmentContext.onMinuteDoubleTapped,
), ),
); );
} }
@ -1265,6 +1280,8 @@ class _TimePickerInput extends StatefulWidget {
Key key, Key key,
@required this.initialSelectedTime, @required this.initialSelectedTime,
@required this.helpText, @required this.helpText,
@required this.autofocusHour,
@required this.autofocusMinute,
@required this.onChanged, @required this.onChanged,
}) : assert(initialSelectedTime != null), }) : assert(initialSelectedTime != null),
assert(onChanged != null), assert(onChanged != null),
@ -1276,6 +1293,10 @@ class _TimePickerInput extends StatefulWidget {
/// Optionally provide your own help text to the time picker. /// Optionally provide your own help text to the time picker.
final String helpText; final String helpText;
final bool autofocusHour;
final bool autofocusMinute;
final ValueChanged<TimeOfDay> onChanged; final ValueChanged<TimeOfDay> onChanged;
@override @override
@ -1430,6 +1451,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
_HourTextField( _HourTextField(
selectedTime: _selectedTime, selectedTime: _selectedTime,
style: hourMinuteStyle, style: hourMinuteStyle,
autofocus: widget.autofocusHour,
validator: _validateHour, validator: _validateHour,
onSavedSubmitted: _handleHourSavedSubmitted, onSavedSubmitted: _handleHourSavedSubmitted,
onChanged: _handleHourChanged, onChanged: _handleHourChanged,
@ -1460,6 +1482,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
_MinuteTextField( _MinuteTextField(
selectedTime: _selectedTime, selectedTime: _selectedTime,
style: hourMinuteStyle, style: hourMinuteStyle,
autofocus: widget.autofocusMinute,
validator: _validateMinute, validator: _validateMinute,
onSavedSubmitted: _handleMinuteSavedSubmitted, onSavedSubmitted: _handleMinuteSavedSubmitted,
), ),
@ -1507,6 +1530,7 @@ class _HourTextField extends StatelessWidget {
Key key, Key key,
@required this.selectedTime, @required this.selectedTime,
@required this.style, @required this.style,
@required this.autofocus,
@required this.validator, @required this.validator,
@required this.onSavedSubmitted, @required this.onSavedSubmitted,
@required this.onChanged, @required this.onChanged,
@ -1514,6 +1538,7 @@ class _HourTextField extends StatelessWidget {
final TimeOfDay selectedTime; final TimeOfDay selectedTime;
final TextStyle style; final TextStyle style;
final bool autofocus;
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
final ValueChanged<String> onSavedSubmitted; final ValueChanged<String> onSavedSubmitted;
final ValueChanged<String> onChanged; final ValueChanged<String> onChanged;
@ -1523,6 +1548,7 @@ class _HourTextField extends StatelessWidget {
return _HourMinuteTextField( return _HourMinuteTextField(
selectedTime: selectedTime, selectedTime: selectedTime,
isHour: true, isHour: true,
autofocus: autofocus,
style: style, style: style,
semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel, semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel,
validator: validator, validator: validator,
@ -1537,12 +1563,14 @@ class _MinuteTextField extends StatelessWidget {
Key key, Key key,
@required this.selectedTime, @required this.selectedTime,
@required this.style, @required this.style,
@required this.autofocus,
@required this.validator, @required this.validator,
@required this.onSavedSubmitted, @required this.onSavedSubmitted,
}) : super(key: key); }) : super(key: key);
final TimeOfDay selectedTime; final TimeOfDay selectedTime;
final TextStyle style; final TextStyle style;
final bool autofocus;
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
final ValueChanged<String> onSavedSubmitted; final ValueChanged<String> onSavedSubmitted;
@ -1551,6 +1579,7 @@ class _MinuteTextField extends StatelessWidget {
return _HourMinuteTextField( return _HourMinuteTextField(
selectedTime: selectedTime, selectedTime: selectedTime,
isHour: false, isHour: false,
autofocus: autofocus,
style: style, style: style,
semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel, semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel,
validator: validator, validator: validator,
@ -1564,6 +1593,7 @@ class _HourMinuteTextField extends StatefulWidget {
Key key, Key key,
@required this.selectedTime, @required this.selectedTime,
@required this.isHour, @required this.isHour,
@required this.autofocus,
@required this.style, @required this.style,
@required this.semanticHintText, @required this.semanticHintText,
@required this.validator, @required this.validator,
@ -1573,6 +1603,7 @@ class _HourMinuteTextField extends StatefulWidget {
final TimeOfDay selectedTime; final TimeOfDay selectedTime;
final bool isHour; final bool isHour;
final bool autofocus;
final TextStyle style; final TextStyle style;
final String semanticHintText; final String semanticHintText;
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
@ -1658,6 +1689,7 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
child: MediaQuery( child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: TextFormField( child: TextFormField(
autofocus: widget.autofocus ?? false,
expands: true, expands: true,
maxLines: null, maxLines: null,
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
@ -1746,6 +1778,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
_TimePickerMode _mode = _TimePickerMode.hour; _TimePickerMode _mode = _TimePickerMode.hour;
_TimePickerMode _lastModeAnnounced; _TimePickerMode _lastModeAnnounced;
bool _autoValidate; bool _autoValidate;
bool _autofocusHour;
bool _autofocusMinute;
TimeOfDay get selectedTime => _selectedTime; TimeOfDay get selectedTime => _selectedTime;
TimeOfDay _selectedTime; TimeOfDay _selectedTime;
@ -1788,6 +1822,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
break; break;
case TimePickerEntryMode.input: case TimePickerEntryMode.input:
_formKey.currentState.save(); _formKey.currentState.save();
_autofocusHour = false;
_autofocusMinute = false;
_entryMode = TimePickerEntryMode.dial; _entryMode = TimePickerEntryMode.dial;
break; break;
} }
@ -1833,6 +1869,16 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
}); });
} }
void _handleHourDoubleTapped() {
_autofocusHour = true;
_handleEntryModeToggle();
}
void _handleMinuteDoubleTapped() {
_autofocusMinute = true;
_handleEntryModeToggle();
}
void _handleHourSelected() { void _handleHourSelected() {
setState(() { setState(() {
_mode = _TimePickerMode.minute; _mode = _TimePickerMode.minute;
@ -1962,6 +2008,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
orientation: orientation, orientation: orientation,
onModeChanged: _handleModeChanged, onModeChanged: _handleModeChanged,
onChanged: _handleTimeChanged, onChanged: _handleTimeChanged,
onHourDoubleTapped: _handleHourDoubleTapped,
onMinuteDoubleTapped: _handleMinuteDoubleTapped,
use24HourDials: use24HourDials, use24HourDials: use24HourDials,
helpText: widget.helpText, helpText: widget.helpText,
); );
@ -2014,6 +2062,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
_TimePickerInput( _TimePickerInput(
initialSelectedTime: _selectedTime, initialSelectedTime: _selectedTime,
helpText: widget.helpText, helpText: widget.helpText,
autofocusHour: _autofocusHour,
autofocusMinute: _autofocusMinute,
onChanged: _handleTimeChanged, onChanged: _handleTimeChanged,
), ),
actions, actions,

View file

@ -803,6 +803,89 @@ void _testsInput() {
expect(find.byType(TextField), findsNothing); 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 { testWidgets('Entered text returns time', (WidgetTester tester) async {
TimeOfDay result; TimeOfDay result;