Add a clear button to the date picker
`clearable` empties a date, or both ends of a range, closes an open picker and hands focus back to the field, as the time picker's does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
eca6b8fb05
commit
23218431bf
@@ -523,7 +523,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
|
||||
```blade
|
||||
<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" />
|
||||
<x-datepicker label="Trip" range wire:model="trip" hint="Start and end" clearable />
|
||||
```
|
||||
|
||||
| Prop | Default | |
|
||||
@@ -534,6 +534,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
|
||||
| `label`, `hint`, `icon`, `variant`, `size` | | the field's |
|
||||
| `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 |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -366,6 +366,18 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
/** The clear button: no date (no start and no end), closed, and focus back in the field. */
|
||||
clear() {
|
||||
if (this.open) {
|
||||
this.cancel(false)
|
||||
}
|
||||
|
||||
this.fieldError = ''
|
||||
this.text = ''
|
||||
this.write(null)
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
|
||||
fieldProblem(value) {
|
||||
if (!config.range) {
|
||||
return this.problem(value)
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
with, and the errors for `period`, `period.start` and `period.end` all belong to this field.
|
||||
`min` and `max` (`Y-m-d` or a date object) disable the days outside them and keep the
|
||||
keyboard inside them. `label`, `hint`, `icon`, `variant` (`outlined`, `filled`) and `size`
|
||||
are the field's; `name` adds hidden inputs carrying `Y-m-d` for a plain form post, and every
|
||||
are the field's; `clearable` adds a button that empties it (a date, or both ends of a range)
|
||||
once it holds one; `name` adds hidden inputs carrying `Y-m-d` for a plain form post, and every
|
||||
other attribute (`required`, `disabled`, `readonly`) reaches the text field. Errors under
|
||||
the `wire:model` name replace the hint, and so does a typed date that cannot be read.
|
||||
|
||||
@@ -48,6 +49,7 @@
|
||||
'min' => null,
|
||||
'max' => null,
|
||||
'value' => null,
|
||||
'clearable' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -179,6 +181,19 @@
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
@if ($clearable)
|
||||
<button
|
||||
type="button"
|
||||
class="field-trailing field-clear field-button"
|
||||
aria-label="{{ __('Clear') }}"
|
||||
x-on:click="clear()"
|
||||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||||
data-field-clear
|
||||
>
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="field-trailing field-button"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'docked' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ expires: '{{ now()->addWeek()->format('Y-m-d') }}', starts: null }">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Expires on" x-model="expires" hint="Type a date or pick one" />
|
||||
<x-datepicker label="Expires on" clearable x-model="expires" hint="Type a date or pick one" />
|
||||
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(expires)"></code></p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ class DateProbe extends Component
|
||||
<p>trip: <span id="trip">{{ json_encode($trip) }}</span></p>
|
||||
<p>renders: <span id="renders">{{ $renders }}</span></p>
|
||||
|
||||
<x-datepicker id="expires-field" label="Expires" wire:model.live="expires" min="2026-09-10" max="2026-10-20" />
|
||||
<x-datepicker id="expires-field" label="Expires" wire:model.live="expires" min="2026-09-10" max="2026-10-20" clearable />
|
||||
<x-datepicker id="birthday-field" label="Birthday" mode="modal" wire:model.live="birthday" />
|
||||
<x-datepicker id="delivery-field" label="Delivery" mode="input" wire:model.live="delivery" min="2026-01-01" />
|
||||
<x-datepicker id="trip-field" label="Trip" range wire:model.live="trip" />
|
||||
<x-datepicker id="trip-field" label="Trip" range wire:model.live="trip" clearable />
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
@@ -296,3 +296,18 @@ it('opens a docked picker as a modal one on a compact window', function () {
|
||||
->assertScript("document.querySelector('#expires-field-picker').matches(':modal')")
|
||||
->assertScript("getComputedStyle(document.querySelector('#expires-field-picker [data-datepicker-header]')).display !== 'none'");
|
||||
});
|
||||
|
||||
it('empties a date, or both ends of a range, with its clear button', function () {
|
||||
$page = dateProbe()
|
||||
->assertScript("getComputedStyle(document.querySelector('#expires-field').closest('.field').querySelector('[data-field-clear]')).display !== 'none'");
|
||||
|
||||
$page->click('[data-datepicker]:has(#expires-field) [data-field-clear]')
|
||||
->assertScript("document.querySelector('#expires').textContent === ''")
|
||||
->assertValue('#expires-field', '')
|
||||
->assertScript("document.activeElement.id === 'expires-field'")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-datepicker]:has(#expires-field) [data-field-clear]')).display === 'none'");
|
||||
|
||||
$page->click('[data-datepicker]:has(#trip-field) [data-field-clear]')
|
||||
->assertSeeIn('#trip', '{"start":null,"end":null}')
|
||||
->assertValue('#trip-field', '');
|
||||
});
|
||||
|
||||
@@ -163,3 +163,12 @@ it('replaces the hint with the errors for the property and its start and end', f
|
||||
->toContain('data-datepicker-error')
|
||||
->and(substr_count($html, 'data-invalid=""'))->toBe(2);
|
||||
});
|
||||
|
||||
it('offers a clear button on request', function () {
|
||||
expect((string) $this->blade('<x-datepicker label="Expires" clearable />'))
|
||||
->toContain('data-field-clear')
|
||||
->toContain('x-on:click="clear()"')
|
||||
->toContain('aria-label="Clear"')
|
||||
->and((string) $this->blade('<x-datepicker label="Expires" />'))
|
||||
->not->toContain('data-field-clear');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user