Let the date picker take a first day of the week and a format
An application that lets each person choose when their week starts and how dates are written needs the picker to follow that choice rather than the locale. <x-datepicker> takes week-start, 0 (Sunday) to 6 (Saturday), and format, dd, MM and yyyy each once around one delimiter (dd.MM.yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy-MM-dd). They replace what Intl derives for firstDayOfWeek() and inputFormat(), so the field, the server-rendered value, the typed-date reader (year first too), the calendar's columns and weekday header, Home and End, the dialog's text fields and ranges all follow them. Month and weekday names stay the locale's, and wire:model still stores Y-m-d. Values that are neither are ignored, and without the props nothing changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
This commit is contained in:
co-authored by
Claude Opus 5
parent
fdb13313f7
commit
fb29169d44
@@ -592,6 +592,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
|
||||
<x-datepicker label="Expires on" wire:model.live="expiresOn" :min="now()" :max="now()->addMonth()" />
|
||||
<x-datepicker label="Birthday" mode="modal" wire:model="birthday" :max="now()" />
|
||||
<x-datepicker label="Trip" range wire:model="trip" hint="Start and end" clearable />
|
||||
<x-datepicker label="Race day" wire:model="raceDay" :week-start="$user->week_start" :format="$user->date_format" />
|
||||
```
|
||||
|
||||
| Prop | Default | |
|
||||
@@ -603,8 +604,10 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
|
||||
| `value` | `null` | the initial value without `wire:model` |
|
||||
| `name` | | adds hidden inputs with `Y-m-d` for a plain form post (`name[start]`, `name[end]` for a range) |
|
||||
| `clearable` | `false` | a button that empties the field (both ends of a range) once it holds a date |
|
||||
| `week-start` | `null` | the first day of the week, `0` (Sunday) to `6` (Saturday), instead of the locale's: the calendar's columns, weekday header and Home/End follow it. Anything else is ignored |
|
||||
| `format` | `null` | the typed and displayed format instead of the locale's: `dd`, `MM` and `yyyy`, each once, around one delimiter (`.`, `/`, `-`) — `dd.MM.yyyy`, `dd/MM/yyyy`, `MM/dd/yyyy`, `yyyy-MM-dd`. The field, the dialog's text fields, a range and the error message follow it; `wire:model` still stores `Y-m-d`. Anything else is ignored |
|
||||
|
||||
Picking in the calendar is a draft; OK or Enter on a day keeps it, Cancel or Escape does not. A typed date is the value once it is whole and allowed; otherwise the field says why. Month and weekday names, the week's first day and the typed format follow `app()->getLocale()`. Keyboard: arrows, Home/End (week), PageUp/PageDown (month; with Shift, year), Space, Enter, Escape. `min` and `max` are read when the picker starts: when they change on the server, give the component a `wire:key` that changes with them. `required`, `disabled` and `readonly` reach the text field.
|
||||
Picking in the calendar is a draft; OK or Enter on a day keeps it, Cancel or Escape does not. A typed date is the value once it is whole and allowed; otherwise the field says why. Month and weekday names, the week's first day and the typed format follow `app()->getLocale()` (the last two unless `week-start` and `format` say otherwise — for a per-person setting). Keyboard: arrows, Home/End (week), PageUp/PageDown (month; with Shift, year), Space, Enter, Escape. `min` and `max` are read when the picker starts: when they change on the server, give the component a `wire:key` that changes with them. `required`, `disabled` and `readonly` reach the text field.
|
||||
|
||||
### `<x-timepicker>`
|
||||
|
||||
|
||||
+24
-10
@@ -11,7 +11,9 @@
|
||||
* Dates are ISO strings (`2026-09-13`) throughout, computed in UTC so no time zone or daylight
|
||||
* saving change can move a day; only "today" is read in the browser's own zone. Month and weekday
|
||||
* names, the week's first day and the typed format come from `Intl` for the locale the server
|
||||
* passes (the application's).
|
||||
* passes (the application's), unless the component names the first day (`weekStart`, 0 for Sunday
|
||||
* to 6) or the format (`format`, such as `dd.MM.yyyy`); everything that reads `firstDay` and
|
||||
* `format` below then follows those.
|
||||
*
|
||||
* The keyboard is WAI-ARIA's date picker dialog: arrows move a day or a week, Home and End go to
|
||||
* the start and end of the week, PageUp and PageDown a month (with Shift a year), Space selects,
|
||||
@@ -82,8 +84,12 @@ function localToday() {
|
||||
return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
||||
}
|
||||
|
||||
/** 0 for Sunday … 6 for Saturday. */
|
||||
function firstDayOfWeek(locale) {
|
||||
/** 0 for Sunday … 6 for Saturday: `weekStart` when it is one of those, otherwise the locale's. */
|
||||
function firstDayOfWeek(locale, weekStart = null) {
|
||||
if (Number.isInteger(weekStart) && weekStart >= 0 && weekStart <= 6) {
|
||||
return weekStart
|
||||
}
|
||||
|
||||
try {
|
||||
const tag = new Intl.Locale(locale)
|
||||
const info = typeof tag.getWeekInfo === 'function' ? tag.getWeekInfo() : tag.weekInfo
|
||||
@@ -111,9 +117,16 @@ function firstDayOfWeek(locale) {
|
||||
/**
|
||||
* The typed format: the locale's short numeric date, reduced to `dd`, `MM` and `yyyy` and one
|
||||
* delimiter — androidx's `datePatternAsInputFormat`, fed from `formatToParts` because `Intl` has
|
||||
* no pattern to give. `de` gives `dd.MM.yyyy`, `en-US` `MM/dd/yyyy`, `ja` `yyyy/MM/dd`.
|
||||
* no pattern to give. `de` gives `dd.MM.yyyy`, `en-US` `MM/dd/yyyy`, `ja` `yyyy/MM/dd`. A `chosen`
|
||||
* pattern of the same shape (each unit once, one delimiter) replaces the locale's.
|
||||
*/
|
||||
function inputFormat(locale) {
|
||||
function inputFormat(locale, chosen = null) {
|
||||
const units = /^(dd|MM|yyyy)([/\-.])(dd|MM|yyyy)\2(dd|MM|yyyy)$/.exec(typeof chosen === 'string' ? chosen : '')
|
||||
|
||||
if (units && new Set([units[1], units[3], units[4]]).size === 3) {
|
||||
return formatOf(chosen)
|
||||
}
|
||||
|
||||
let pattern = ''
|
||||
|
||||
try {
|
||||
@@ -131,10 +144,11 @@ function inputFormat(locale) {
|
||||
pattern = ''
|
||||
}
|
||||
|
||||
if (!/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[/\-.][dMy]+[/\-.][dMy]+$/.test(pattern)) {
|
||||
pattern = 'yyyy-MM-dd'
|
||||
}
|
||||
return formatOf(/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[/\-.][dMy]+[/\-.][dMy]+$/.test(pattern) ? pattern : 'yyyy-MM-dd')
|
||||
}
|
||||
|
||||
/** A pattern, the placeholder it shows (`DD.MM.YYYY`) and the order its units are typed in (`dMy`). */
|
||||
function formatOf(pattern) {
|
||||
return {
|
||||
pattern,
|
||||
placeholder: pattern.toUpperCase(),
|
||||
@@ -169,8 +183,8 @@ document.addEventListener('alpine:init', () => {
|
||||
const locale = config.locale || document.documentElement.lang || 'en'
|
||||
const format = (options) => new Intl.DateTimeFormat(locale, { timeZone: 'UTC', ...options })
|
||||
|
||||
this.firstDay = firstDayOfWeek(locale)
|
||||
this.format = inputFormat(locale)
|
||||
this.firstDay = firstDayOfWeek(locale, config.weekStart ?? null)
|
||||
this.format = inputFormat(locale, config.format ?? null)
|
||||
this.numbers = new Intl.NumberFormat(locale, { useGrouping: false })
|
||||
this.formats = {
|
||||
monthYear: format({ year: 'numeric', month: 'long' }),
|
||||
|
||||
@@ -27,8 +27,14 @@
|
||||
the `wire:model` name replace the hint, and so does a typed date that cannot be read.
|
||||
|
||||
Month and weekday names, the first day of the week and the typed format come from `Intl` for
|
||||
`app()->getLocale()` (resources/js/datepicker.js). Replaces ReStride's flatpickr picker: its
|
||||
`config` becomes `min`, `max`, `range` and `mode`.
|
||||
`app()->getLocale()` (resources/js/datepicker.js). An application that lets each person choose
|
||||
overrides the last two: `week-start` is the first day of the week, 0 (Sunday) to 6 (Saturday),
|
||||
and `format` the typed and displayed format, `dd`, `MM` and `yyyy` in any order around one
|
||||
delimiter (`.`, `/` or `-`: `dd.MM.yyyy`, `MM/dd/yyyy`, `yyyy-MM-dd`). The field, the typed-date
|
||||
reader, the calendar's columns and weekday header, Home and End, and the dialog's text fields
|
||||
all follow them; the names stay the locale's, and `wire:model` still stores `Y-m-d`. A value
|
||||
that is neither (`week-start="7"`, `format="d.M.yy"`) is ignored, as `null` is. Replaces
|
||||
ReStride's flatpickr picker: its `config` becomes `min`, `max`, `range` and `mode`.
|
||||
|
||||
M3's date pickers (DatePickerModalTokens and DateInputModalTokens from androidx Compose
|
||||
Material 3, androidx commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, with the layout of
|
||||
@@ -50,6 +56,8 @@
|
||||
'max' => null,
|
||||
'value' => null,
|
||||
'clearable' => false,
|
||||
'weekStart' => null,
|
||||
'format' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -63,6 +71,10 @@
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
|
||||
: [];
|
||||
$locale = str_replace('_', '-', app()->getLocale());
|
||||
$weekStart = (is_int($weekStart) || is_string($weekStart)) && preg_match('/^[0-6]\z/', (string) $weekStart) === 1 ? (int) $weekStart : null;
|
||||
$format = is_string($format) && preg_match('/^(dd|MM|yyyy)([.\/-])(dd|MM|yyyy)\2(dd|MM|yyyy)\z/', $format, $units) === 1 && count(array_unique([$units[1], $units[3], $units[4]])) === 3
|
||||
? $format
|
||||
: null;
|
||||
|
||||
$toIso = function (mixed $date): ?string {
|
||||
if ($date instanceof \DateTimeInterface) {
|
||||
@@ -85,9 +97,9 @@
|
||||
? ['start' => $toIso(data_get($current, 'start')), 'end' => $toIso(data_get($current, 'end'))]
|
||||
: $toIso($current);
|
||||
|
||||
// The typed format as resources/js/datepicker.js derives it, from ICU's short date when PHP has intl.
|
||||
$pattern = 'yyyy-MM-dd';
|
||||
if (class_exists(\IntlDateFormatter::class)) {
|
||||
// The typed format: `format`, or as resources/js/datepicker.js derives it, from ICU's short date when PHP has intl.
|
||||
$pattern = $format ?? 'yyyy-MM-dd';
|
||||
if ($format === null && class_exists(\IntlDateFormatter::class)) {
|
||||
$short = (string) (new \IntlDateFormatter(str_replace('-', '_', $locale), \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE))->getPattern();
|
||||
$candidate = rtrim(str_replace('My', 'M/y', (string) preg_replace(['/[^dMy\/\-.]/', '/d{1,2}/', '/M{1,2}/', '/y{1,4}/'], ['', 'dd', 'MM', 'yyyy'], $short)), '.');
|
||||
if (preg_match('/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[\/\-.][dMy]+[\/\-.][dMy]+$/', $candidate) === 1) {
|
||||
@@ -110,6 +122,8 @@
|
||||
'range' => (bool) $range,
|
||||
'min' => $toIso($min),
|
||||
'max' => $toIso($max),
|
||||
'weekStart' => $weekStart,
|
||||
'format' => $format,
|
||||
'disabled' => (bool) $attributes->get('disabled'),
|
||||
'readonly' => (bool) $attributes->get('readonly'),
|
||||
'strings' => [
|
||||
|
||||
@@ -39,6 +39,19 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'First day of the week and format' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ race: '{{ now()->addMonth()->format('Y-m-d') }}', block: { start: null, end: null } }">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Race day" week-start="0" format="yyyy-MM-dd" x-model="race" hint="Weeks start on Sunday; typed year first" />
|
||||
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(race)"></code></p>
|
||||
</div>
|
||||
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Training block" range mode="modal" week-start="1" format="dd.MM.yyyy" x-model="block" />
|
||||
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(block)"></code></p>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Limits, errors and states' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2">
|
||||
<div class="grid content-start gap-4">
|
||||
@@ -62,7 +75,7 @@
|
||||
<h2 class="type-headline-md">Date pickers</h2>
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-datepicker></code> — docked, modal and modal input; single dates and ranges. Month and weekday names and the first day of the week follow the application's locale.
|
||||
<code><x-datepicker></code> — docked, modal and modal input; single dates and ranges. Month and weekday names, the first day of the week and the typed format follow the application's locale; <code>week-start</code> and <code>format</code> set the last two for someone who chose their own.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
|
||||
Reference in New Issue
Block a user