Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions packages/material_ui/example/lib/date_picker/show_date_picker.2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';

/// Flutter code sample showing how to use [showDatePicker] with a custom
/// [DateInputCalendarDelegate] to support configurable text input formats.

void main() => runApp(const DatePickerSampleApp());

class DatePickerSampleApp extends StatelessWidget {
const DatePickerSampleApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(home: DatePickerSample());
}
}

class DatePickerSample extends StatefulWidget {
const DatePickerSample({super.key});

@override
State<DatePickerSample> createState() => _DatePickerSampleState();
}

class _DatePickerSampleState extends State<DatePickerSample> {
DateTime? _selectedDate;

DateInputFormat _formatType = DateInputFormat.dayMonthYear;
DateSeparator _separator = DateSeparator.dot;

Future<void> _showPicker() async {
final DateTime? result = await showDatePicker(
context: context,
initialDate: _selectedDate ?? DateTime(2021, 7, 25),
firstDate: DateTime(2021),
lastDate: DateTime(2999, 7, 25),
initialEntryMode: DatePickerEntryMode.input,
calendarDelegate: ConfigurableDateDelegate(formatType: _formatType, separator: _separator),
);

if (result != null) {
setState(() {
_selectedDate = result;
});
}
}

@override
Widget build(BuildContext context) {
final String label = _selectedDate == null
? 'No date selected'
: DateInputFormatter(formatType: _formatType, separator: _separator.value).format(_selectedDate!);

return Scaffold(
appBar: AppBar(title: const Text('showDatePicker with input delegate')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(label),
const SizedBox(height: 24),
DropdownButtonFormField<DateInputFormat>(
initialValue: _formatType,
decoration: const InputDecoration(labelText: 'Date format'),
items: DateInputFormat.values
.map((DateInputFormat format) => DropdownMenuItem(value: format, child: Text(format.patternLabel)))
.toList(),
onChanged: (DateInputFormat? value) {
if (value != null) {
setState(() => _formatType = value);
}
},
),
const SizedBox(height: 16),
DropdownButtonFormField<DateSeparator>(
initialValue: _separator,
decoration: const InputDecoration(labelText: 'Separator'),
items: DateSeparator.values
.map((DateSeparator sep) => DropdownMenuItem(value: sep, child: Text(sep.value)))
.toList(),
onChanged: (DateSeparator? value) {
if (value != null) {
setState(() => _separator = value);
}
},
),

const SizedBox(height: 24),

OutlinedButton(onPressed: _showPicker, child: const Text('Select date')),
],
),
),
);
}
}

enum DateInputFormat {
dayMonthYear(fieldLengths: [2, 2, 4], patternLabel: 'dd/mm/yyyy'),
monthDayYear(fieldLengths: [2, 2, 4], patternLabel: 'mm/dd/yyyy'),
yearMonthDay(fieldLengths: [4, 2, 2], patternLabel: 'yyyy/mm/dd');

const DateInputFormat({required this.fieldLengths, required this.patternLabel});

final List<int> fieldLengths;
final String patternLabel;

String pattern(String separator) => patternLabel.replaceAll('/', separator);
}

enum DateSeparator {
slash('/'),
dash('-'),
dot('.');

const DateSeparator(this.value);
final String value;
}

class DateInputFormatter extends TextInputFormatter {
const DateInputFormatter({required this.formatType, required this.separator});

final DateInputFormat formatType;
final String separator;

String get pattern => formatType.pattern(separator);

String format(DateTime date) {
final String day = date.day.toString().padLeft(2, '0');
final String month = date.month.toString().padLeft(2, '0');
final String year = date.year.toString().padLeft(4, '0');

return switch (formatType) {
DateInputFormat.dayMonthYear => '$day$separator$month$separator$year',
DateInputFormat.monthDayYear => '$month$separator$day$separator$year',
DateInputFormat.yearMonthDay => '$year$separator$month$separator$day',
};
}

DateTime? parse(String? input) {
if (input == null || input.isEmpty) {
return null;
}

final List<String> parts = input.split(separator);
if (parts.length != 3) {
return null;
}

final (String d, String m, String y) = switch (formatType) {
DateInputFormat.dayMonthYear => (parts[0], parts[1], parts[2]),
DateInputFormat.monthDayYear => (parts[1], parts[0], parts[2]),
DateInputFormat.yearMonthDay => (parts[2], parts[1], parts[0]),
};

final int? day = int.tryParse(d);
final int? month = int.tryParse(m);
final int? year = int.tryParse(y);

if (day == null || month == null || year == null) {
return null;
}
if (month < 1 || month > 12 || day < 1) {
return null;
}

final int lastDayOfMonth = DateTime(year, month + 1, 0).day;
if (day > lastDayOfMonth) {
return null;
}

return DateTime(year, month, day);
}

@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final String digits = newValue.text.replaceAll(RegExp(r'\D'), '');
final int maxLength = formatType.fieldLengths.fold<int>(0, (int a, int b) => a + b);

if (digits.length > maxLength) {
return oldValue;
}

final String formatted = _applyMask(digits);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}

String _applyMask(String digits) {
final StringBuffer buffer = StringBuffer();
int offset = 0;

for (int i = 0; i < formatType.fieldLengths.length && offset < digits.length; i++) {
final int len = formatType.fieldLengths[i];
final int end = (offset + len).clamp(0, digits.length);
buffer.write(digits.substring(offset, end));
offset = end;

if (offset < digits.length && i < formatType.fieldLengths.length - 1) {
buffer.write(separator);
}
}
return buffer.toString();
}
}

class ConfigurableDateDelegate extends DateInputCalendarDelegate {
const ConfigurableDateDelegate({required this.formatType, this.separator = DateSeparator.slash});

final DateInputFormat formatType;
final DateSeparator separator;

DateInputFormatter get _formatter => DateInputFormatter(formatType: formatType, separator: separator.value);

@override
List<TextInputFormatter> get inputFormatters => <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
_formatter,
];

@override
String dateHelpText(MaterialLocalizations localizations) => _formatter.pattern;

@override
String formatCompactDate(DateTime date, MaterialLocalizations localizations) => _formatter.format(date);

@override
DateTime? parseCompactDate(String? inputString, MaterialLocalizations localizations) => _formatter.parse(inputString);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui_examples/date_picker/show_date_picker.2.dart' as example;

void main() {
testWidgets('renders selected date text according to formatType and separator (input mode)', (
WidgetTester tester,
) async {
await tester.pumpWidget(const MaterialApp(home: example.DatePickerSample()));

expect(find.text('No date selected'), findsOneWidget);

// Open date picker.
await tester.tap(find.byType(OutlinedButton));
await tester.pumpAndSettle();

final Finder textField = find.byType(TextField);
expect(textField, findsOneWidget);

// Enter date: 30/07/2021.
await tester.enterText(textField, '30072021');
await tester.pump();

await tester.tap(find.text('OK'));
await tester.pumpAndSettle();

expect(find.text('30.07.2021'), findsOneWidget);

// Change format to monthDayYear.
await tester.tap(find.byType(DropdownButtonFormField<example.DateInputFormat>));
await tester.pumpAndSettle();
await tester.tap(find.text('mm/dd/yyyy').last);
await tester.pumpAndSettle();

expect(find.text('07.30.2021'), findsOneWidget);

// Change separator to dash.
await tester.tap(find.byType(DropdownButtonFormField<example.DateSeparator>));
await tester.pumpAndSettle();
await tester.tap(find.text('-').last);
await tester.pumpAndSettle();

expect(find.text('07-30-2021'), findsOneWidget);

// Change format to yearMonthDay.
await tester.tap(find.byType(DropdownButtonFormField<example.DateInputFormat>));
await tester.pumpAndSettle();
await tester.tap(find.text('yyyy/mm/dd').last);
await tester.pumpAndSettle();

expect(find.text('2021-07-30'), findsOneWidget);
});
}
18 changes: 18 additions & 0 deletions packages/material_ui/lib/src/date.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
library;

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import 'date_picker.dart';
import 'input_date_picker_form_field.dart';
import 'material_localizations.dart';

/// Controls the calendar system used in the date picker.
Expand All @@ -34,6 +37,8 @@ import 'material_localizations.dart';
/// See also:
///
/// * [GregorianCalendarDelegate], the default implementation for the Gregorian calendar.
/// * [DateInputCalendarDelegate], an interface for delegates that support
/// customizing date text input and formatting.
/// * [CalendarDatePicker], which uses this delegate to manage calendar-specific behavior.
abstract class CalendarDelegate<T extends DateTime> {
/// Creates a calendar delegate.
Expand Down Expand Up @@ -249,6 +254,19 @@ class GregorianCalendarDelegate extends CalendarDelegate<DateTime> {
}
}

/// A [GregorianCalendarDelegate] that synchronizes calendar operations with
/// text input requirements, such as [inputFormatters].
///
/// Subclasses must provide the appropriate [inputFormatters] to ensure
/// that user entry matches the parsing logic in [parseCompactDate].
abstract class DateInputCalendarDelegate extends GregorianCalendarDelegate {
/// Creates a calendar delegate.
const DateInputCalendarDelegate();

/// The formatters applied to the text field to guide and validate user input.
List<TextInputFormatter> get inputFormatters;
}

/// Utility functions for working with dates.
abstract final class DateUtils {
/// {@template material_ui.date.dateOnly}
Expand Down
25 changes: 25 additions & 0 deletions packages/material_ui/lib/src/date_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ const double _fontSizeToScale = 14.0;
///
/// {@macro material_ui.calendar_date_picker.calendarDelegate}
///
/// Use [DateInputCalendarDelegate] to customize how dates are entered and
/// formatted in [DatePickerEntryMode.input].
///
/// A custom delegate can define specific date input conventions, such as
/// ordering, separators, or formatting rules (for example, `dd.MM.yyyy`), and
/// is responsible for keeping text input parsing and calendar selection
/// synchronized.
///
/// {@tool dartpad}
/// This sample shows how to customize the text input behavior of
/// [showDatePicker] using a [DateInputCalendarDelegate].
///
/// ** See code in examples/api/lib/material/date_picker/show_date_picker.2.dart **
/// {@end-tool}
///
/// The following optional string parameters allow you to override the default
/// text used for various parts of the dialog:
///
Expand Down Expand Up @@ -3485,6 +3500,7 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
keyboardType: widget.keyboardType,
onChanged: _handleStartChanged,
autofocus: widget.autofocus,
inputFormatters: [...?_textInputFormatter],
),
),
const SizedBox(width: 8),
Expand All @@ -3501,9 +3517,18 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
),
keyboardType: widget.keyboardType,
onChanged: _handleEndChanged,
inputFormatters: [...?_textInputFormatter],
),
),
],
);
}

List<TextInputFormatter>? get _textInputFormatter {
return switch (widget.calendarDelegate) {
DateInputCalendarDelegate(:final List<TextInputFormatter>? inputFormatters) =>
inputFormatters,
_ => null,
};
}
}
Loading