@@ -62,7 +75,7 @@
Date pickers
- <x-datepicker> — 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.
+ <x-datepicker> — 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; week-start and format set the last two for someone who chose their own.
@foreach ($examples as $title => $code)
diff --git a/tests/Browser/DatepickerTest.php b/tests/Browser/DatepickerTest.php
index 6e70b44d..70b0666c 100644
--- a/tests/Browser/DatepickerTest.php
+++ b/tests/Browser/DatepickerTest.php
@@ -69,6 +69,62 @@ function dateProbe(string $locale = 'en')
->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'
+
+
sunday: {{ $sunday }}
+
iso: {{ $iso }}
+
dotted: {{ $dotted }}
+
span: {{ json_encode($span) }}
+
+
+
+
+
+
+ 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'
+
+
+
+
+ @vite(config('livewire-material.showcase.vite'))
+ @livewireStyles
+
+
+
+ @livewireScripts
+
+
+ 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. */
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}')
->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');
+});
diff --git a/tests/Feature/Components/DatepickerTest.php b/tests/Feature/Components/DatepickerTest.php
index 750d1ee2..e1752b17 100644
--- a/tests/Feature/Components/DatepickerTest.php
+++ b/tests/Feature/Components/DatepickerTest.php
@@ -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]);
});
+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('
');
+
+ expect(datepickerConfig($html))->toMatchArray(['weekStart' => 0, 'format' => 'yyyy-MM-dd', 'locale' => 'de'])
+ ->and($html)->toContain('value="2026-09-13"')
+ ->and(datepickerConfig((string) $this->blade('
')))
+ ->toMatchArray(['weekStart' => 6, 'format' => 'MM/dd/yyyy'])
+ ->and((string) $this->blade('
'))
+ ->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("
")))
+ ->toMatchArray(['weekStart' => null, 'format' => null]);
+ }
+
+ expect(datepickerConfig((string) $this->blade('
')))->toMatchArray(['weekStart' => null, 'format' => null])
+ ->and((string) $this->blade('
'))->toContain('value="13.09.2026"');
+});
+
it('shows the value in the locale\'s numeric format before Alpine starts', function () {
expect((string) $this->blade('
'))
->toContain('value="09/13/2026"')