Skip to content

Commit 9e160ad

Browse files
feat: add allowClear and clearIcon to FormBuilderDateTimePicker
chore: add tests for FormBuilderDateTimePicker chore: apply dart fix and format on codebase
1 parent bff073a commit 9e160ad

5 files changed

Lines changed: 233 additions & 18 deletions

File tree

example/lib/sources/complete_form.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,17 @@ class _CompleteFormState extends State<CompleteForm> {
5555
inputType: InputType.both,
5656
decoration: InputDecoration(
5757
labelText: 'Appointment Time',
58-
suffixIcon: IconButton(
59-
icon: const Icon(Icons.close),
60-
onPressed: () {
61-
_formKey.currentState!.fields['date']?.didChange(null);
62-
},
63-
),
58+
// suffixIcon: IconButton(
59+
// icon: const Icon(Icons.close),
60+
// onPressed: () {
61+
// _formKey.currentState!.fields['date']?.didChange(null);
62+
// },
63+
// ),
6464
),
6565
initialTime: const TimeOfDay(hour: 8, minute: 0),
6666
// locale: const Locale.fromSubtags(languageCode: 'fr'),
67+
allowClear: true,
68+
clearIcon: Icon(Icons.clear),
6769
),
6870
FormBuilderDateRangePicker(
6971
name: 'date_range',

example/pubspec.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ packages:
6060
path: ".."
6161
relative: true
6262
source: path
63-
version: "10.3.0+1"
63+
version: "10.3.0+2"
6464
flutter_lints:
6565
dependency: "direct dev"
6666
description:
@@ -131,10 +131,10 @@ packages:
131131
dependency: transitive
132132
description:
133133
name: matcher
134-
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
134+
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
135135
url: "https://pub.dev"
136136
source: hosted
137-
version: "0.12.18"
137+
version: "0.12.19"
138138
material_color_utilities:
139139
dependency: transitive
140140
description:
@@ -208,10 +208,10 @@ packages:
208208
dependency: transitive
209209
description:
210210
name: test_api
211-
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
211+
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
212212
url: "https://pub.dev"
213213
source: hosted
214-
version: "0.7.9"
214+
version: "0.7.10"
215215
vector_math:
216216
dependency: transitive
217217
description:

lib/src/fields/form_builder_date_time_picker.dart

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import 'dart:ui' as ui;
22

33
import 'package:flutter/material.dart';
44
import 'package:flutter/services.dart';
5-
6-
import 'package:intl/intl.dart';
7-
85
import 'package:flutter_form_builder/flutter_form_builder.dart';
6+
import 'package:intl/intl.dart';
97

108
enum InputType { date, time, both }
119

@@ -123,6 +121,9 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration<DateTime> {
123121
final EntryModeChangeCallback? onEntryModeChanged;
124122
final bool barrierDismissible;
125123

124+
final bool allowClear;
125+
final Widget? clearIcon;
126+
126127
/// If true, disables the picker so it's not shown when the field is tapped.
127128
final bool disablePicker;
128129

@@ -197,6 +198,9 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration<DateTime> {
197198
this.onEntryModeChanged,
198199
this.disablePicker = false,
199200
this.barrierDismissible = true,
201+
// TODO: set allClear to true if that's the default behaviour
202+
this.allowClear = false,
203+
this.clearIcon,
200204
}) : super(
201205
builder: (FormFieldState<DateTime?> field) {
202206
final state = field as _FormBuilderDateTimePickerState;
@@ -404,6 +408,27 @@ class _FormBuilderDateTimePickerState
404408
DateTime? convert(TimeOfDay? time) =>
405409
time == null ? null : DateTime(1, 1, 1, time.hour, time.minute);
406410

411+
/// Sets the [allowClear] property for automatic DateTime reset, and [clearIcon] to a default or user defined icon.
412+
@override
413+
InputDecoration get decoration => widget.allowClear
414+
? super.decoration.copyWith(
415+
suffix: value == null
416+
? null
417+
: IconButton(
418+
padding: EdgeInsets.zero,
419+
constraints: const BoxConstraints(
420+
maxWidth: 24,
421+
maxHeight: 24,
422+
),
423+
onPressed: () {
424+
focus();
425+
didChange(null);
426+
},
427+
icon: widget.clearIcon ?? const Icon(Icons.clear),
428+
),
429+
)
430+
: super.decoration;
431+
407432
@override
408433
void didChange(DateTime? value) {
409434
super.didChange(value);

pubspec.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ packages:
111111
dependency: transitive
112112
description:
113113
name: matcher
114-
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
114+
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
115115
url: "https://pub.dev"
116116
source: hosted
117-
version: "0.12.18"
117+
version: "0.12.19"
118118
material_color_utilities:
119119
dependency: transitive
120120
description:
@@ -188,10 +188,10 @@ packages:
188188
dependency: transitive
189189
description:
190190
name: test_api
191-
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
191+
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
192192
url: "https://pub.dev"
193193
source: hosted
194-
version: "0.7.9"
194+
version: "0.7.10"
195195
vector_math:
196196
dependency: transitive
197197
description:

test/src/fields/form_builder_date_time_picker_test.dart

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,194 @@ void main() {
197197
);
198198
});
199199
});
200+
201+
testWidgets('allowClear properly clears value', (
202+
WidgetTester tester,
203+
) async {
204+
const widgetName = 'fdtp_clear';
205+
final widgetKey = UniqueKey();
206+
final initialDate = DateTime(2023, 1, 1);
207+
208+
final testWidget = FormBuilderDateTimePicker(
209+
key: widgetKey,
210+
name: widgetName,
211+
initialValue: initialDate,
212+
allowClear: true,
213+
);
214+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
215+
216+
expect(formInstantValue(widgetName), initialDate);
217+
expect(find.byIcon(Icons.clear), findsOneWidget);
218+
219+
await tester.tap(find.byIcon(Icons.clear));
220+
await tester.pumpAndSettle();
221+
222+
expect(formInstantValue(widgetName), isNull);
223+
expect(find.byIcon(Icons.clear), findsNothing);
224+
});
225+
226+
testWidgets('custom clearIcon is rendered', (WidgetTester tester) async {
227+
const widgetName = 'fdtp_custom_clear';
228+
final initialDate = DateTime(2023, 1, 1);
229+
const customIcon = Icons.delete;
230+
231+
final testWidget = FormBuilderDateTimePicker(
232+
name: widgetName,
233+
initialValue: initialDate,
234+
allowClear: true,
235+
clearIcon: const Icon(customIcon),
236+
);
237+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
238+
239+
expect(find.byIcon(customIcon), findsOneWidget);
240+
expect(find.byIcon(Icons.clear), findsNothing);
241+
});
242+
243+
testWidgets('InputType.date returns midnight time', (
244+
WidgetTester tester,
245+
) async {
246+
const widgetName = 'fdtp_date_only';
247+
final widgetKey = UniqueKey();
248+
final dateNow = DateTime.now();
249+
const confirmText = 'OK';
250+
251+
final testWidget = FormBuilderDateTimePicker(
252+
key: widgetKey,
253+
name: widgetName,
254+
inputType: InputType.date,
255+
confirmText: confirmText,
256+
);
257+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
258+
259+
await tester.tap(find.byKey(widgetKey));
260+
await tester.pumpAndSettle();
261+
262+
final testDay = dateNow.day - 1 <= 0 ? dateNow.day + 1 : dateNow.day - 1;
263+
await tester.tap(find.text(testDay.toString()));
264+
await tester.pumpAndSettle();
265+
await tester.tap(find.text(confirmText));
266+
await tester.pumpAndSettle();
267+
268+
expect(formSave(), isTrue);
269+
expect(
270+
formValue<DateTime>(widgetName),
271+
DateTime(dateNow.year, dateNow.month, testDay),
272+
);
273+
});
274+
275+
testWidgets('InputType.time returns DateTime(1, 1, 1) with time', (
276+
WidgetTester tester,
277+
) async {
278+
const widgetName = 'fdtp_time_only';
279+
final widgetKey = UniqueKey();
280+
const confirmText = 'OK';
281+
282+
final testWidget = FormBuilderDateTimePicker(
283+
key: widgetKey,
284+
name: widgetName,
285+
inputType: InputType.time,
286+
confirmText: confirmText,
287+
);
288+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
289+
290+
await tester.tap(find.byKey(widgetKey));
291+
await tester.pumpAndSettle();
292+
293+
await tester.tap(find.text(confirmText));
294+
await tester.pumpAndSettle();
295+
296+
expect(formSave(), isTrue);
297+
final value = formValue<DateTime>(widgetName);
298+
expect(value.year, 1);
299+
expect(value.month, 1);
300+
expect(value.day, 1);
301+
expect(value.hour, 12); // Default initialTime is 12:00
302+
expect(value.minute, 0);
303+
});
304+
305+
testWidgets('onChanged is called when value changes', (
306+
WidgetTester tester,
307+
) async {
308+
const widgetName = 'fdtp_onChanged';
309+
int changedCount = 0;
310+
DateTime? valueFromOnChanged;
311+
312+
final testWidget = FormBuilderDateTimePicker(
313+
name: widgetName,
314+
allowClear: true,
315+
initialValue: DateTime(2023, 1, 1),
316+
onChanged: (val) {
317+
changedCount++;
318+
valueFromOnChanged = val;
319+
},
320+
);
321+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
322+
323+
await tester.tap(find.byIcon(Icons.clear));
324+
await tester.pumpAndSettle();
325+
326+
expect(changedCount, 1);
327+
expect(valueFromOnChanged, isNull);
328+
});
329+
330+
testWidgets('reset() reverts value to initialValue', (
331+
WidgetTester tester,
332+
) async {
333+
const widgetName = 'fdtp_reset';
334+
final initialDate = DateTime(2023, 1, 1);
335+
336+
final testWidget = FormBuilderDateTimePicker(
337+
name: widgetName,
338+
initialValue: initialDate,
339+
allowClear: true,
340+
);
341+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
342+
343+
await tester.tap(find.byIcon(Icons.clear));
344+
await tester.pumpAndSettle();
345+
expect(formInstantValue(widgetName), isNull);
346+
347+
formKey.currentState!.fields[widgetName]!.reset();
348+
await tester.pumpAndSettle();
349+
350+
expect(formInstantValue(widgetName), initialDate);
351+
});
352+
353+
testWidgets('initialTime is respected for InputType.both', (
354+
WidgetTester tester,
355+
) async {
356+
const widgetName = 'fdtp_initialTime';
357+
final widgetKey = UniqueKey();
358+
const confirmText = 'OK';
359+
final initialTime = const TimeOfDay(hour: 15, minute: 30);
360+
361+
final testWidget = FormBuilderDateTimePicker(
362+
key: widgetKey,
363+
name: widgetName,
364+
inputType: InputType.both,
365+
confirmText: confirmText,
366+
initialTime: initialTime,
367+
);
368+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
369+
370+
await tester.tap(find.byKey(widgetKey));
371+
await tester.pumpAndSettle();
372+
373+
// Date picker OK
374+
await tester.tap(find.text(DateTime.now().day.toString()));
375+
await tester.pumpAndSettle();
376+
await tester.tap(find.text(confirmText));
377+
await tester.pumpAndSettle();
378+
379+
// Time picker should show 15:30.
380+
await tester.tap(find.text(confirmText));
381+
await tester.pumpAndSettle();
382+
383+
expect(formSave(), isTrue);
384+
final value = formValue<DateTime>(widgetName);
385+
expect(value.hour, 15);
386+
expect(value.minute, 30);
387+
});
200388
});
201389

202390
testWidgets('When press tab, field will be focused', (

0 commit comments

Comments
 (0)