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:
Andreas Reinhold / reini
2026-09-13 18:18:39 +02:00
co-authored by Claude Opus 5
parent fdb13313f7
commit fb29169d44
6 changed files with 222 additions and 17 deletions
@@ -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="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="Birthday" mode="modal" wire:model="birthday" :max="now()" />
<x-datepicker label="Trip" range wire:model="trip" hint="Start and end" clearable /> <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 | | | 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` | | `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) | | `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 | | `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>` ### `<x-timepicker>`
+24 -10
View File
@@ -11,7 +11,9 @@
* Dates are ISO strings (`2026-09-13`) throughout, computed in UTC so no time zone or daylight * 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 * 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 * 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 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, * 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())}` return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
} }
/** 0 for Sunday … 6 for Saturday. */ /** 0 for Sunday … 6 for Saturday: `weekStart` when it is one of those, otherwise the locale's. */
function firstDayOfWeek(locale) { function firstDayOfWeek(locale, weekStart = null) {
if (Number.isInteger(weekStart) && weekStart >= 0 && weekStart <= 6) {
return weekStart
}
try { try {
const tag = new Intl.Locale(locale) const tag = new Intl.Locale(locale)
const info = typeof tag.getWeekInfo === 'function' ? tag.getWeekInfo() : tag.weekInfo 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 * 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 * 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 = '' let pattern = ''
try { try {
@@ -131,10 +144,11 @@ function inputFormat(locale) {
pattern = '' pattern = ''
} }
if (!/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[/\-.][dMy]+[/\-.][dMy]+$/.test(pattern)) { return formatOf(/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[/\-.][dMy]+[/\-.][dMy]+$/.test(pattern) ? pattern : 'yyyy-MM-dd')
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 { return {
pattern, pattern,
placeholder: pattern.toUpperCase(), placeholder: pattern.toUpperCase(),
@@ -169,8 +183,8 @@ document.addEventListener('alpine:init', () => {
const locale = config.locale || document.documentElement.lang || 'en' const locale = config.locale || document.documentElement.lang || 'en'
const format = (options) => new Intl.DateTimeFormat(locale, { timeZone: 'UTC', ...options }) const format = (options) => new Intl.DateTimeFormat(locale, { timeZone: 'UTC', ...options })
this.firstDay = firstDayOfWeek(locale) this.firstDay = firstDayOfWeek(locale, config.weekStart ?? null)
this.format = inputFormat(locale) this.format = inputFormat(locale, config.format ?? null)
this.numbers = new Intl.NumberFormat(locale, { useGrouping: false }) this.numbers = new Intl.NumberFormat(locale, { useGrouping: false })
this.formats = { this.formats = {
monthYear: format({ year: 'numeric', month: 'long' }), 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. 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 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 `app()->getLocale()` (resources/js/datepicker.js). An application that lets each person choose
`config` becomes `min`, `max`, `range` and `mode`. 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 M3's date pickers (DatePickerModalTokens and DateInputModalTokens from androidx Compose
Material 3, androidx commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, with the layout of Material 3, androidx commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, with the layout of
@@ -50,6 +56,8 @@
'max' => null, 'max' => null,
'value' => null, 'value' => null,
'clearable' => false, 'clearable' => false,
'weekStart' => null,
'format' => null,
]) ])
@php @php
@@ -63,6 +71,10 @@
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')]))) ? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
: []; : [];
$locale = str_replace('_', '-', app()->getLocale()); $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 { $toIso = function (mixed $date): ?string {
if ($date instanceof \DateTimeInterface) { if ($date instanceof \DateTimeInterface) {
@@ -85,9 +97,9 @@
? ['start' => $toIso(data_get($current, 'start')), 'end' => $toIso(data_get($current, 'end'))] ? ['start' => $toIso(data_get($current, 'start')), 'end' => $toIso(data_get($current, 'end'))]
: $toIso($current); : $toIso($current);
// The typed format as resources/js/datepicker.js derives it, from ICU's short date when PHP has intl. // The typed format: `format`, or as resources/js/datepicker.js derives it, from ICU's short date when PHP has intl.
$pattern = 'yyyy-MM-dd'; $pattern = $format ?? 'yyyy-MM-dd';
if (class_exists(\IntlDateFormatter::class)) { if ($format === null && class_exists(\IntlDateFormatter::class)) {
$short = (string) (new \IntlDateFormatter(str_replace('-', '_', $locale), \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE))->getPattern(); $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)), '.'); $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) { if (preg_match('/^(?=.*dd)(?=.*MM)(?=.*yyyy)[dMy]+[\/\-.][dMy]+[\/\-.][dMy]+$/', $candidate) === 1) {
@@ -110,6 +122,8 @@
'range' => (bool) $range, 'range' => (bool) $range,
'min' => $toIso($min), 'min' => $toIso($min),
'max' => $toIso($max), 'max' => $toIso($max),
'weekStart' => $weekStart,
'format' => $format,
'disabled' => (bool) $attributes->get('disabled'), 'disabled' => (bool) $attributes->get('disabled'),
'readonly' => (bool) $attributes->get('readonly'), 'readonly' => (bool) $attributes->get('readonly'),
'strings' => [ 'strings' => [
@@ -39,6 +39,19 @@
</div> </div>
</div> </div>
BLADE, 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' 'Limits, errors and states' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2"> <div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid content-start gap-4"> <div class="grid content-start gap-4">
@@ -62,7 +75,7 @@
<h2 class="type-headline-md">Date pickers</h2> <h2 class="type-headline-md">Date pickers</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant"> <p class="max-w-3xl type-body-md text-on-surface-variant">
<code>&lt;x-datepicker&gt;</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>&lt;x-datepicker&gt;</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> </p>
@foreach ($examples as $title => $code) @foreach ($examples as $title => $code)
+140
View File
@@ -69,6 +69,62 @@ function dateProbe(string $locale = 'en')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
} }
class DateFormatProbe extends Component
{
public ?string $sunday = '2026-09-13';
public ?string $iso = '2026-09-13';
public ?string $dotted = '2026-09-13';
/** @var array{start: ?string, end: ?string} */
public array $span = ['start' => '2026-09-13', 'end' => '2026-09-15'];
public function render(): string
{
return <<<'BLADE'
<div class="grid max-w-md gap-6 p-4">
<p>sunday: <span id="sunday">{{ $sunday }}</span></p>
<p>iso: <span id="iso">{{ $iso }}</span></p>
<p>dotted: <span id="dotted">{{ $dotted }}</span></p>
<p>span: <span id="span">{{ json_encode($span) }}</span></p>
<x-datepicker id="sunday-field" label="Sunday first" wire:model.live="sunday" week-start="0" />
<x-datepicker id="iso-field" label="ISO" wire:model.live="iso" format="yyyy-MM-dd" />
<x-datepicker id="dotted-field" label="Dotted" mode="input" wire:model.live="dotted" format="dd.MM.yyyy" />
<x-datepicker id="span-field" label="Span" range mode="modal" wire:model.live="span" format="dd/MM/yyyy" week-start="6" />
</div>
BLADE;
}
}
function dateFormatProbe(string $locale = 'en')
{
Livewire::component('date-format-probe', DateFormatProbe::class);
Route::middleware('web')->get('/date-format-probe/{locale}', function (string $locale) {
app()->setLocale($locale);
return Blade::render(<<<'BLADE'
<!DOCTYPE html>
<html>
<head>
<x-theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body class="bg-surface">
<livewire:date-format-probe />
@livewireScripts
</body>
</html>
BLADE);
});
return visit("/date-format-probe/{$locale}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
/** A picker's day cell, by its ISO date. */ /** A picker's day cell, by its ISO date. */
function day(string $picker, string $date): string function day(string $picker, string $date): string
{ {
@@ -311,3 +367,87 @@ it('empties a date, or both ends of a range, with its clear button', function ()
->assertSeeIn('#trip', '{"start":null,"end":null}') ->assertSeeIn('#trip', '{"start":null,"end":null}')
->assertValue('#trip-field', ''); ->assertValue('#trip-field', '');
}); });
it('starts the week on the day week-start names, whatever the locale says', function () {
$page = dateFormatProbe('de')
->assertValue('#sunday-field', '13.09.2026')
->click('[aria-controls="sunday-field-picker"][data-datepicker-toggle]')
->assertScript("document.querySelector('#sunday-field-picker thead th').getAttribute('abbr') === 'Sonntag'")
->assertScript("document.querySelector('#sunday-field-picker tbody td').dataset.value === '2026-08-30'")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-19'));
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Enter')
->assertSeeIn('#sunday', '2026-09-13')
->assertValue('#sunday-field', '13.09.2026');
});
it('shows and reads a year-first format and still binds Y-m-d', function () {
$page = dateFormatProbe()
->assertValue('#iso-field', '2026-09-13')
->assertAttribute('#iso-field', 'placeholder', 'YYYY-MM-DD')
->type('#iso-field', '2026-10-01')
->assertSeeIn('#iso', '2026-10-01');
$page->type('#iso-field', '10/02/2026')
->keys('#iso-field', 'Enter')
->assertSee('Date does not match expected pattern: YYYY-MM-DD')
->assertSeeIn('#iso', '2026-10-01');
$page->click('[aria-controls="iso-field-picker"][data-datepicker-toggle]')
->assertScript(focusedDay('2026-10-01'));
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-10-02'));
$page->keys(':focus', 'Enter')
->assertSeeIn('#iso', '2026-10-02')
->assertValue('#iso-field', '2026-10-02');
});
it('reads the dialog\'s text field in the given format, not the locale\'s', function () {
$dialog = "document.querySelector('#dotted-field-picker')";
$page = dateFormatProbe()
->assertValue('#dotted-field', '13.09.2026')
->click('#dotted-field')
->assertScript("{$dialog}.matches(':modal')")
->assertScript("document.activeElement.id === 'dotted-field-entry'")
->assertValue('#dotted-field-entry', '13.09.2026')
->assertAttribute('#dotted-field-entry', 'placeholder', 'DD.MM.YYYY');
$page->type('#dotted-field-entry', '2026-10-01')
->keys('#dotted-field-entry', 'Enter')
->assertSeeIn('#dotted-field-entry-support', 'Date does not match expected pattern: DD.MM.YYYY')
->assertScript("{$dialog}.open");
$page->type('#dotted-field-entry', '1.10.2026')
->assertSeeIn('#dotted-field-picker [data-datepicker-headline]', 'Oct 1, 2026')
->keys('#dotted-field-entry', 'Enter')
->assertSeeIn('#dotted', '2026-10-01')
->assertScript("! {$dialog}.open")
->assertValue('#dotted-field', '01.10.2026');
});
it('lays out and shows a range in the given first day and format', function () {
$dialog = "document.querySelector('#span-field-picker')";
$page = dateFormatProbe()->assertValue('#span-field', '13/09/2026 15/09/2026');
$page->script("document.querySelector('#span-field').focus()");
$page->keys('#span-field', 'Enter')
->assertScript("{$dialog}.matches(':modal')")
->assertScript("document.querySelector('#span-field-picker thead th').getAttribute('abbr') === 'Saturday'")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-12'));
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-18'));
$page->click(day('span-field', '2026-09-20'))
->click(day('span-field', '2026-09-24'))
->click('#span-field-picker [data-datepicker-confirm]')
->assertSeeIn('#span', '{"start":"2026-09-20","end":"2026-09-24"}')
->assertValue('#span-field', '20/09/2026 24/09/2026');
});
@@ -72,6 +72,27 @@ it('hands min, max and the application locale to the picker as Y-m-d', function
->toMatchArray(['min' => null, 'max' => null]); ->toMatchArray(['min' => null, 'max' => null]);
}); });
it('hands a chosen first day of the week and format to the picker, and ignores values that are neither', function () {
app()->setLocale('de');
$html = (string) $this->blade('<x-datepicker label="Date" week-start="0" format="yyyy-MM-dd" value="2026-09-13" />');
expect(datepickerConfig($html))->toMatchArray(['weekStart' => 0, 'format' => 'yyyy-MM-dd', 'locale' => 'de'])
->and($html)->toContain('value="2026-09-13"')
->and(datepickerConfig((string) $this->blade('<x-datepicker label="Date" :week-start="6" format="MM/dd/yyyy" />')))
->toMatchArray(['weekStart' => 6, 'format' => 'MM/dd/yyyy'])
->and((string) $this->blade('<x-datepicker label="Trip" range format="dd/MM/yyyy" :value="[\'start\' => \'2026-09-13\', \'end\' => \'2026-09-20\']" />'))
->toContain('value="13/09/2026 20/09/2026"');
foreach (['week-start="7"', 'week-start="-1"', 'week-start="monday"', 'week-start', 'format="d.M.yy"', 'format="dd.MM/yyyy"', 'format="dd.dd.yyyy"', 'format="yyyy-mm-dd"'] as $attribute) {
expect(datepickerConfig((string) $this->blade("<x-datepicker label=\"Date\" {$attribute} />")))
->toMatchArray(['weekStart' => null, 'format' => null]);
}
expect(datepickerConfig((string) $this->blade('<x-datepicker label="Date" />')))->toMatchArray(['weekStart' => null, 'format' => null])
->and((string) $this->blade('<x-datepicker label="Date" format="dd/MM/y" value="2026-09-13" />'))->toContain('value="13.09.2026"');
});
it('shows the value in the locale\'s numeric format before Alpine starts', function () { it('shows the value in the locale\'s numeric format before Alpine starts', function () {
expect((string) $this->blade('<x-datepicker label="Date" value="2026-09-13" name="expires" />')) expect((string) $this->blade('<x-datepicker label="Date" value="2026-09-13" name="expires" />'))
->toContain('value="09/13/2026"') ->toContain('value="09/13/2026"')