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
|
```blade
|
||||||
<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" />
|
<x-datepicker label="Trip" range wire:model="trip" hint="Start and end" clearable />
|
||||||
```
|
```
|
||||||
|
|
||||||
| Prop | Default | |
|
| 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 |
|
| `label`, `hint`, `icon`, `variant`, `size` | | the field's |
|
||||||
| `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 |
|
||||||
|
|
||||||
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()`. 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) {
|
fieldProblem(value) {
|
||||||
if (!config.range) {
|
if (!config.range) {
|
||||||
return this.problem(value)
|
return this.problem(value)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
with, and the errors for `period`, `period.start` and `period.end` all belong to this field.
|
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
|
`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`
|
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
|
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.
|
the `wire:model` name replace the hint, and so does a typed date that cannot be read.
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
'min' => null,
|
'min' => null,
|
||||||
'max' => null,
|
'max' => null,
|
||||||
'value' => null,
|
'value' => null,
|
||||||
|
'clearable' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
@@ -179,6 +181,19 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<x-slot:trailing>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="field-trailing field-button"
|
class="field-trailing field-button"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
'docked' => <<<'BLADE'
|
'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 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">
|
<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>
|
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(expires)"></code></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ class DateProbe extends Component
|
|||||||
<p>trip: <span id="trip">{{ json_encode($trip) }}</span></p>
|
<p>trip: <span id="trip">{{ json_encode($trip) }}</span></p>
|
||||||
<p>renders: <span id="renders">{{ $renders }}</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="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="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>
|
</div>
|
||||||
BLADE;
|
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("document.querySelector('#expires-field-picker').matches(':modal')")
|
||||||
->assertScript("getComputedStyle(document.querySelector('#expires-field-picker [data-datepicker-header]')).display !== 'none'");
|
->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')
|
->toContain('data-datepicker-error')
|
||||||
->and(substr_count($html, 'data-invalid=""'))->toBe(2);
|
->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