Skip to content

Commit e1eb1a6

Browse files
azatechdkwingsmt
andauthored
Allow empty initial time when using text input mode in showTimePicker dialog (flutter#172847)
Added ability to allow empty initial time when using text input mode in showTimePicker dialog flutter#169131 - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Tong Mu <[email protected]>
1 parent bfee618 commit e1eb1a6

File tree

2 files changed

+127
-2
lines changed

2 files changed

+127
-2
lines changed

packages/flutter/lib/src/material/time_picker.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,7 @@ class _TimePickerInput extends StatefulWidget {
16361636
required this.helpText,
16371637
required this.autofocusHour,
16381638
required this.autofocusMinute,
1639+
required this.emptyInitialTime,
16391640
this.restorationId,
16401641
});
16411642

@@ -1666,6 +1667,13 @@ class _TimePickerInput extends StatefulWidget {
16661667
/// from the surrounding [RestorationScope] using the provided restoration ID.
16671668
final String? restorationId;
16681669

1670+
/// If true and [TimePickerEntryMode.input] is used, hour and minute fields
1671+
/// start empty instead of using [initialSelectedTime].
1672+
///
1673+
/// Useful when users prefer manual input without clearing pre-filled values.
1674+
/// Ignored in dial mode.
1675+
final bool emptyInitialTime;
1676+
16691677
@override
16701678
_TimePickerInputState createState() => _TimePickerInputState();
16711679
}
@@ -1851,6 +1859,7 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi
18511859
onSavedSubmitted: _handleHourSavedSubmitted,
18521860
onChanged: _handleHourChanged,
18531861
hourLabelText: widget.hourLabelText,
1862+
emptyInitialTime: widget.emptyInitialTime,
18541863
),
18551864
),
18561865
if (!hourHasError.value && !minuteHasError.value)
@@ -1885,6 +1894,7 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi
18851894
validator: _validateMinute,
18861895
onSavedSubmitted: _handleMinuteSavedSubmitted,
18871896
minuteLabelText: widget.minuteLabelText,
1897+
emptyInitialTime: widget.emptyInitialTime,
18881898
),
18891899
),
18901900
if (!hourHasError.value && !minuteHasError.value)
@@ -1935,6 +1945,7 @@ class _HourTextField extends StatelessWidget {
19351945
required this.onSavedSubmitted,
19361946
required this.onChanged,
19371947
required this.hourLabelText,
1948+
required this.emptyInitialTime,
19381949
this.restorationId,
19391950
});
19401951

@@ -1947,6 +1958,7 @@ class _HourTextField extends StatelessWidget {
19471958
final ValueChanged<String> onChanged;
19481959
final String? hourLabelText;
19491960
final String? restorationId;
1961+
final bool emptyInitialTime;
19501962

19511963
@override
19521964
Widget build(BuildContext context) {
@@ -1960,6 +1972,7 @@ class _HourTextField extends StatelessWidget {
19601972
semanticHintText: hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel,
19611973
validator: validator,
19621974
onSavedSubmitted: onSavedSubmitted,
1975+
emptyInitialTime: emptyInitialTime,
19631976
onChanged: onChanged,
19641977
);
19651978
}
@@ -1974,6 +1987,7 @@ class _MinuteTextField extends StatelessWidget {
19741987
required this.validator,
19751988
required this.onSavedSubmitted,
19761989
required this.minuteLabelText,
1990+
required this.emptyInitialTime,
19771991
this.restorationId,
19781992
});
19791993

@@ -1985,6 +1999,7 @@ class _MinuteTextField extends StatelessWidget {
19851999
final ValueChanged<String?> onSavedSubmitted;
19862000
final String? minuteLabelText;
19872001
final String? restorationId;
2002+
final bool emptyInitialTime;
19882003

19892004
@override
19902005
Widget build(BuildContext context) {
@@ -1997,6 +2012,7 @@ class _MinuteTextField extends StatelessWidget {
19972012
style: style,
19982013
semanticHintText: minuteLabelText ?? MaterialLocalizations.of(context).timePickerMinuteLabel,
19992014
validator: validator,
2015+
emptyInitialTime: emptyInitialTime,
20002016
onSavedSubmitted: onSavedSubmitted,
20012017
);
20022018
}
@@ -2013,6 +2029,7 @@ class _HourMinuteTextField extends StatefulWidget {
20132029
required this.validator,
20142030
required this.onSavedSubmitted,
20152031
this.restorationId,
2032+
required this.emptyInitialTime,
20162033
this.onChanged,
20172034
});
20182035

@@ -2026,6 +2043,7 @@ class _HourMinuteTextField extends StatefulWidget {
20262043
final ValueChanged<String?> onSavedSubmitted;
20272044
final ValueChanged<String>? onChanged;
20282045
final String? restorationId;
2046+
final bool emptyInitialTime;
20292047

20302048
@override
20312049
_HourMinuteTextFieldState createState() => _HourMinuteTextFieldState();
@@ -2058,9 +2076,12 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with Restora
20582076
super.didChangeDependencies();
20592077
// Only set the text value if it has not been populated with a localized
20602078
// version yet.
2079+
// If emptyInitialTime is true, set it to an empty string to indicate no
2080+
// initial time.
20612081
if (!controllerHasBeenSet.value) {
20622082
controllerHasBeenSet.value = true;
2063-
controller.value.value = TextEditingValue(text: _formattedValue);
2083+
final String initialTextValue = widget.emptyInitialTime ? '' : _formattedValue;
2084+
controller.value.value = TextEditingValue(text: initialTextValue);
20642085
}
20652086
}
20662087

@@ -2113,7 +2134,8 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with Restora
21132134
).applyDefaults(inputDecorationTheme);
21142135
// Remove the hint text when focused because the centered cursor
21152136
// appears odd above the hint text.
2116-
final String? hintText = focusNode.hasFocus ? null : _formattedValue;
2137+
// Clear the hint text when emptyInitialTime is true.
2138+
final String? hintText = focusNode.hasFocus || widget.emptyInitialTime ? null : _formattedValue;
21172139

21182140
// Because the fill color is specified in both the inputDecorationTheme and
21192141
// the TimePickerTheme, if there's one in the user's input decoration theme,
@@ -2213,6 +2235,7 @@ class TimePickerDialog extends StatefulWidget {
22132235
this.onEntryModeChanged,
22142236
this.switchToInputEntryModeIcon,
22152237
this.switchToTimerEntryModeIcon,
2238+
this.emptyInitialInput = false,
22162239
});
22172240

22182241
/// The time initially selected when the dialog is shown.
@@ -2279,6 +2302,12 @@ class TimePickerDialog extends StatefulWidget {
22792302
/// {@macro flutter.material.time_picker.switchToTimerEntryModeIcon}
22802303
final Icon? switchToTimerEntryModeIcon;
22812304

2305+
/// If true and entry mode is [TimePickerEntryMode.input], the hour and minute
2306+
/// fields will be empty on start instead of pre-filled with [initialTime].
2307+
///
2308+
/// Has no effect in dial mode.
2309+
final bool emptyInitialInput;
2310+
22822311
@override
22832312
State<TimePickerDialog> createState() => _TimePickerDialogState();
22842313
}
@@ -2617,6 +2646,7 @@ class _TimePickerDialogState extends State<TimePickerDialog> with RestorationMix
26172646
onEntryModeChanged: _handleEntryModeChanged,
26182647
switchToInputEntryModeIcon: widget.switchToInputEntryModeIcon,
26192648
switchToTimerEntryModeIcon: widget.switchToTimerEntryModeIcon,
2649+
emptyInitialInput: widget.emptyInitialInput,
26202650
),
26212651
);
26222652
if (_entryMode.value != TimePickerEntryMode.input &&
@@ -2661,6 +2691,7 @@ class _TimePicker extends StatefulWidget {
26612691
this.onEntryModeChanged,
26622692
this.switchToInputEntryModeIcon,
26632693
this.switchToTimerEntryModeIcon,
2694+
required this.emptyInitialInput,
26642695
});
26652696

26662697
/// Optionally provide your own text for the help text at the top of the
@@ -2735,6 +2766,9 @@ class _TimePicker extends StatefulWidget {
27352766
/// {@macro flutter.material.time_picker.switchToTimerEntryModeIcon}
27362767
final Icon? switchToTimerEntryModeIcon;
27372768

2769+
/// If true, input fields start empty in input mode.
2770+
final bool emptyInitialInput;
2771+
27382772
@override
27392773
State<_TimePicker> createState() => _TimePickerState();
27402774
}
@@ -2996,6 +3030,7 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin {
29963030
autofocusHour: _autofocusHour.value,
29973031
autofocusMinute: _autofocusMinute.value,
29983032
restorationId: 'time_picker_input',
3033+
emptyInitialTime: widget.emptyInitialInput,
29993034
),
30003035
],
30013036
);
@@ -3148,6 +3183,7 @@ Future<TimeOfDay?> showTimePicker({
31483183
Orientation? orientation,
31493184
Icon? switchToInputEntryModeIcon,
31503185
Icon? switchToTimerEntryModeIcon,
3186+
bool emptyInitialInput = false,
31513187
}) async {
31523188
assert(debugCheckHasMaterialLocalizations(context));
31533189

@@ -3164,6 +3200,7 @@ Future<TimeOfDay?> showTimePicker({
31643200
onEntryModeChanged: onEntryModeChanged,
31653201
switchToInputEntryModeIcon: switchToInputEntryModeIcon,
31663202
switchToTimerEntryModeIcon: switchToTimerEntryModeIcon,
3203+
emptyInitialInput: emptyInitialInput,
31673204
);
31683205
return showDialog<TimeOfDay>(
31693206
context: context,

packages/flutter/test/material/time_picker_test.dart

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,6 +2273,89 @@ void main() {
22732273
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
22742274
});
22752275
});
2276+
2277+
group('Time picker - emptyInitialInput (${materialType.name})', () {
2278+
testWidgets('Fields are empty and show correct hints when emptyInitialInput is true', (
2279+
WidgetTester tester,
2280+
) async {
2281+
await startPicker(
2282+
tester,
2283+
(_) {},
2284+
entryMode: TimePickerEntryMode.input,
2285+
materialType: materialType,
2286+
emptyInitialInput: true,
2287+
);
2288+
await tester.pump();
2289+
2290+
final List<TextField> textFields = tester
2291+
.widgetList<TextField>(find.byType(TextField))
2292+
.toList();
2293+
2294+
expect(textFields[0].controller?.text, isEmpty); // hour
2295+
expect(textFields[1].controller?.text, isEmpty); // minute
2296+
expect(textFields[0].decoration?.hintText, isNull);
2297+
expect(textFields[1].decoration?.hintText, isNull);
2298+
await finishPicker(tester);
2299+
});
2300+
2301+
testWidgets('User sets hour/minute after initially empty fields', (
2302+
WidgetTester tester,
2303+
) async {
2304+
late TimeOfDay result;
2305+
await startPicker(
2306+
tester,
2307+
(TimeOfDay? time) {
2308+
result = time!;
2309+
},
2310+
entryMode: TimePickerEntryMode.input,
2311+
materialType: materialType,
2312+
emptyInitialInput: true,
2313+
);
2314+
2315+
final List<TextField> textFields = tester
2316+
.widgetList<TextField>(find.byType(TextField))
2317+
.toList();
2318+
2319+
expect(textFields[0].controller?.text, isEmpty); // hour
2320+
expect(textFields[1].controller?.text, isEmpty); // minute
2321+
expect(textFields[0].decoration?.hintText, isNull);
2322+
expect(textFields[1].decoration?.hintText, isNull);
2323+
2324+
await tester.enterText(find.byType(TextField).first, '11');
2325+
await tester.enterText(find.byType(TextField).last, '30');
2326+
await finishPicker(tester);
2327+
2328+
expect(result, equals(const TimeOfDay(hour: 11, minute: 30)));
2329+
});
2330+
2331+
testWidgets('User overrides default values when emptyInitialInput is false', (
2332+
WidgetTester tester,
2333+
) async {
2334+
late TimeOfDay result;
2335+
await startPicker(
2336+
tester,
2337+
(TimeOfDay? time) {
2338+
result = time!;
2339+
},
2340+
entryMode: TimePickerEntryMode.input,
2341+
materialType: materialType,
2342+
);
2343+
2344+
final List<TextField> textFields = tester
2345+
.widgetList<TextField>(find.byType(TextField))
2346+
.toList();
2347+
2348+
expect(textFields[0].controller?.text, '7'); // hour
2349+
expect(textFields[1].controller?.text, '00'); // minute
2350+
2351+
await tester.enterText(find.byType(TextField).first, '8');
2352+
await tester.enterText(find.byType(TextField).last, '15');
2353+
await tester.pump();
2354+
await finishPicker(tester);
2355+
2356+
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
2357+
});
2358+
});
22762359
}
22772360

22782361
testWidgets('Material3 - Time selector separator default text style', (
@@ -2569,13 +2652,15 @@ class _TimePickerLauncher extends StatefulWidget {
25692652
this.restorationId,
25702653
this.cancelText,
25712654
this.confirmText,
2655+
required this.emptyInitialInput,
25722656
});
25732657

25742658
final ValueChanged<TimeOfDay?> onChanged;
25752659
final TimePickerEntryMode entryMode;
25762660
final String? restorationId;
25772661
final String? cancelText;
25782662
final String? confirmText;
2663+
final bool emptyInitialInput;
25792664

25802665
@override
25812666
_TimePickerLauncherState createState() => _TimePickerLauncherState();
@@ -2653,6 +2738,7 @@ class _TimePickerLauncherState extends State<_TimePickerLauncher> with Restorati
26532738
context: context,
26542739
initialTime: const TimeOfDay(hour: 7, minute: 0),
26552740
initialEntryMode: widget.entryMode,
2741+
emptyInitialInput: widget.emptyInitialInput,
26562742
),
26572743
);
26582744
} else {
@@ -2682,6 +2768,7 @@ Future<Offset?> startPicker(
26822768
MaterialType? materialType,
26832769
String? cancelText,
26842770
String? confirmText,
2771+
bool emptyInitialInput = false,
26852772
}) async {
26862773
await tester.pumpWidget(
26872774
MaterialApp(
@@ -2694,6 +2781,7 @@ Future<Offset?> startPicker(
26942781
restorationId: restorationId,
26952782
cancelText: cancelText,
26962783
confirmText: confirmText,
2784+
emptyInitialInput: emptyInitialInput,
26972785
),
26982786
),
26992787
);

0 commit comments

Comments
 (0)