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
@@ -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'
|
||||
<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. */
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -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('<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 () {
|
||||
expect((string) $this->blade('<x-datepicker label="Date" value="2026-09-13" name="expires" />'))
|
||||
->toContain('value="09/13/2026"')
|
||||
|
||||
Reference in New Issue
Block a user