Open a range picker full screen on a compact window

M3 § Date Pickers, Anatomy describes a 14-element full-screen range picker for
compact breakpoints; `<x-datepicker range>` always used the 360px modal dialog,
which on a phone is cramped for a two-month scroll (plan step 24, audit
docs/audits/m3-alignment/inputs.md § Missing). A third presentation, "full",
joins docked and modal: the same dialog grown to the screen, an app bar with a
close button and Save, the supporting text and the range as the headline over a
divider, the weekday labels held at the top, and the months in one vertically
scrolling list, each under its own label. The grid now draws from one `rows`
getter, which is this month's weeks everywhere else, so the docked and
single-date pickers render exactly as before. The list is a window of months
grown by the scroll and by the keyboard, since nothing here is lazy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:36:49 +02:00
co-authored by Claude Fable 5.1
parent 790ef93c6a
commit 2662040c24
6 changed files with 277 additions and 20 deletions
@@ -622,7 +622,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
| Prop | Default | | | Prop | Default | |
|---|---|---| |---|---|---|
| `mode` | `docked` | `docked`: type a date (in the locale's numeric format) or pick one from a calendar under the field, which opens as a dialog on a compact window (below `medium`), as M3 asks; `modal`: the field opens a calendar dialog; `input`: the dialog opens on a text field. Both dialogs switch between calendar and typing | | `mode` | `docked` | `docked`: type a date (in the locale's numeric format) or pick one from a calendar under the field, which opens as a dialog on a compact window (below `medium`), as M3 asks; `modal`: the field opens a calendar dialog; `input`: the dialog opens on a text field. Both dialogs switch between calendar and typing |
| `range` | `false` | binds one array property, `['start' => 'Y-m-d', 'end' => 'Y-m-d']` (either may be null); errors for `trip`, `trip.start` and `trip.end` show on the field | | `range` | `false` | binds one array property, `['start' => 'Y-m-d', 'end' => 'Y-m-d']` (either may be null); errors for `trip`, `trip.start` and `trip.end` show on the field. On a compact window (below `medium`) it opens as M3's full-screen range picker: an app bar with a close button and **Save**, the range as the headline, and the months in one scrolling list instead of stepped one at a time |
| `min`, `max` | `null` | `Y-m-d` or a date; days outside are disabled and the keyboard stays inside | | `min`, `max` | `null` | `Y-m-d` or a date; days outside are disabled and the keyboard stays inside |
| `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` |
+94 -7
View File
@@ -1,10 +1,12 @@
/* /*
* M3's date pickers — docked, modal and modal input (resources/views/components/datepicker.blade.php). * M3's date pickers — docked, modal and modal input (resources/views/components/datepicker.blade.php).
* *
* One `<dialog>` wears both: docked it is a popover hung under the field by CSS anchor positioning * One `<dialog>` wears all three: docked it is a popover hung under the field by CSS anchor
* (large corner), modal it is opened with `showModal()` over a scrim (extra-large corner, with M3's * positioning (large corner), modal it is opened with `showModal()` over a scrim (extra-large
* header: a title, the chosen date as the headline, and the switch between calendar and typing). * corner, with M3's header: a title, the chosen date as the headline, and the switch between
* Both are 360px of surface-container-high at elevation 3. * calendar and typing), and full — a `range` picker on a compact window — is that modal dialog
* grown to the whole screen, M3's full-screen range picker. The first two are 360px of
* surface-container-high at elevation 3; the third keeps the colour and drops the corner.
* *
* Values from DatePickerModalTokens and DateInputModalTokens (androidx Compose Material 3, commit * Values from DatePickerModalTokens and DateInputModalTokens (androidx Compose Material 3, commit
* 27cf9a7d5788aa0f5f2d8b6699ce279560daf326) and the layout of DatePicker.kt, DateRangePicker.kt and * 27cf9a7d5788aa0f5f2d8b6699ce279560daf326) and the layout of DatePicker.kt, DateRangePicker.kt and
@@ -13,15 +15,19 @@
* *
* [data-datepicker] the root: the field, its support line, the dialog * [data-datepicker] the root: the field, its support line, the dialog
* [data-datepicker-support] the hint, a server error, or what is wrong with the typed date * [data-datepicker-support] the hint, a server error, or what is wrong with the typed date
* [data-datepicker-picker] the <dialog>; data-presentation="docked" | "modal" * [data-datepicker-picker] the <dialog>; data-presentation="docked" | "modal" | "full"
* [data-datepicker-surface] * [data-datepicker-surface]
* [data-datepicker-header] modal only: title, headline, switch * [data-datepicker-header] modal only: title, headline, switch
* [data-datepicker-calendar] * [data-datepicker-full] full only: [data-datepicker-app-bar] (close, switch, Save)
* and [data-datepicker-full-headline] (title, headline)
* [data-datepicker-calendar] the scroller, full screen
* [data-datepicker-nav] month and year: one menu button and arrows (modal), * [data-datepicker-nav] month and year: one menu button and arrows (modal),
* or a month and a year stepper (docked, data-docked) * or a month and a year stepper (docked, data-docked)
* [data-datepicker-grid] the weekdays and six weeks of [data-datepicker-day] cells: * [data-datepicker-grid] the weekdays and six weeks of [data-datepicker-day] cells:
* data-today, data-selected, data-start, data-end, * data-today, data-selected, data-start, data-end,
* data-between, data-outside, data-blank, aria-disabled * data-between, data-outside, data-blank, aria-disabled
* full screen, every month of the window under a
* [data-datepicker-month-label] row
* [data-datepicker-years] the modal year grid * [data-datepicker-years] the modal year grid
* [data-datepicker-menu] a docked month or year list of [data-datepicker-option]s * [data-datepicker-menu] a docked month or year list of [data-datepicker-option]s
* [data-datepicker-entry] the text field(s) of the modal input * [data-datepicker-entry] the text field(s) of the modal input
@@ -128,6 +134,87 @@
border-radius: var(--md-sys-shape-corner-xl); border-radius: var(--md-sys-shape-corner-xl);
} }
/* ---- The full-screen range picker (compact) ------------------------------------------------- */
/* M3 § Date Pickers: a range picker at a compact breakpoint is a full-screen dialog, "covering
* the whole screen for readability and touch-target size", with a close (×) icon button and a
* Save confirmation instead of Cancel and OK. Its anatomy is the modal picker's parts in a
* different order: an app bar, the supporting text and the range as the headline, a divider, the
* day-of-week labels, and a vertically scrolling list of months under them, each with its label.
* Only the months scroll; everything above them stays where it is. */
[data-datepicker-picker][data-presentation="full"] {
inset: 0;
width: 100vw;
max-width: none;
height: 100dvh;
max-height: none;
margin: 0;
}
[data-datepicker-picker][data-presentation="full"] [data-datepicker-surface] {
height: 100%;
max-height: none;
padding-block: var(--material-safe-top, env(safe-area-inset-top)) var(--material-safe-bottom, env(safe-area-inset-bottom));
overflow: hidden;
border-radius: 0;
box-shadow: none;
}
[data-datepicker-full] {
flex: none;
border-bottom: 1px solid var(--md-sys-color-outline-variant);
color: var(--md-sys-color-on-surface-variant);
}
[data-datepicker-app-bar] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
height: 4rem;
padding-inline: 0.25rem 0.75rem;
}
[data-datepicker-app-bar-actions] {
display: flex;
align-items: center;
gap: 0.25rem;
}
[data-datepicker-full-headline] {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0 1.5rem 1rem;
}
/* The calendar is the only part that scrolls, with the weekday header held at its top. */
[data-datepicker-picker][data-presentation="full"] [data-datepicker-calendar] {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
[data-datepicker-picker][data-presentation="full"] [data-datepicker-grid] thead th {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--md-sys-color-surface-container-high);
}
[data-datepicker-picker][data-presentation="full"] [data-datepicker-day] {
scroll-margin-block: 3rem;
}
[data-datepicker-month-label] {
padding: 1.25rem 0 0.5rem;
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-title-sm);
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
text-align: start;
}
/* ---- The modal header ---------------------------------------------------------------------- */ /* ---- The modal header ---------------------------------------------------------------------- */
[data-datepicker-header] { [data-datepicker-header] {
+106 -7
View File
@@ -2,12 +2,15 @@
* `materialDatepicker`: the behaviour of `<x-datepicker>` — M3's docked, modal and modal-input * `materialDatepicker`: the behaviour of `<x-datepicker>` — M3's docked, modal and modal-input
* date pickers on one `<dialog>`. * date pickers on one `<dialog>`.
* *
* The dialog is shown two ways. Docked, it is a `popover="manual"` placed under the field by CSS * The dialog is shown three ways. Docked, it is a `popover="manual"` placed under the field by CSS
* anchor positioning; modal (the `modal` and `input` modes, and a docked picker on a compact * anchor positioning; modal (the `modal` and `input` modes, and a docked picker on a compact
* window — below `medium`, 600px, where M3 puts the picker in a dialog rather than under the * window — below `medium`, 600px, where M3 puts the picker in a dialog rather than under the
* field) it is opened with `showModal()`. Everything a person sees in it is drawn by Alpine from * field) it is opened with `showModal()`; full, a `range` picker on a compact window, is that same
* the state below, and the dialog is `wire:ignore`, so a Livewire render never touches it while * modal dialog grown to the whole screen — M3's full-screen range picker, with an app bar carrying
* it is open. * a close button and **Save**, the range as the headline, and a list of months scrolled through
* rather than stepped (§ Date Pickers, Anatomy). Everything a person sees in it is drawn by Alpine
* from the state below, and the dialog is `wire:ignore`, so a Livewire render never touches it
* while it is open.
* *
* 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
@@ -33,6 +36,17 @@ const WEEK_STARTS_SATURDAY = 'AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY'.spli
const YEARS_FROM = 1900 const YEARS_FROM = 1900
const YEARS_TO = 2100 const YEARS_TO = 2100
/**
* The full-screen range picker scrolls through months rather than stepping between them, so it
* draws a window of them: this many either side of the month it opens on, grown by the same number
* whenever the scroll or the keyboard reaches an end. Compose's own list is lazy; this one is not,
* so the window stays small enough for the DOM it costs.
*/
const MONTH_WINDOW = 6
/** How near an end of the month list a scroll has to come before the window grows. */
const MONTH_EDGE = 400
function pad(number, length = 2) { function pad(number, length = 2) {
return String(number).padStart(length, '0') return String(number).padStart(length, '0')
} }
@@ -181,6 +195,9 @@ document.addEventListener('alpine:init', () => {
entryError: '', entryError: '',
today: localToday(), today: localToday(),
compact: false, compact: false,
// The first and last month the full-screen range picker draws, as `yyyy-MM-01`.
monthsFrom: null,
monthsTo: null,
refocus: true, refocus: true,
// Set in init() from the config. Declared here, or Alpine writes them to the outermost // Set in init() from the config. Declared here, or Alpine writes them to the outermost
// x-data scope, where every picker inside the same page scope would share the last one's. // x-data scope, where every picker inside the same page scope would share the last one's.
@@ -436,11 +453,21 @@ document.addEventListener('alpine:init', () => {
const value = this.current() const value = this.current()
this.today = localToday() this.today = localToday()
this.presentation = config.mode === 'docked' && !this.compact ? 'docked' : 'modal' // M3 gives a range picker its own full-screen dialog at compact; everything else is the
// docked popover at medium and up, and the modal dialog otherwise.
this.presentation = config.range && this.compact ? 'full' : config.mode === 'docked' && !this.compact ? 'docked' : 'modal'
this.typing = config.mode === 'input' this.typing = config.mode === 'input'
this.view = 'days' this.view = 'days'
this.draft = value this.draft = value
this.monthsFrom = null
this.monthsTo = null
this.moveTo(this.start() ?? this.today, false) this.moveTo(this.start() ?? this.today, false)
if (this.presentation === 'full') {
this.monthsFrom = this.clampMonth(addMonths(this.shown, -MONTH_WINDOW))
this.monthsTo = this.clampMonth(addMonths(this.shown, MONTH_WINDOW))
}
this.entryError = '' this.entryError = ''
this.fillEntry() this.fillEntry()
this.refocus = true this.refocus = true
@@ -566,12 +593,61 @@ document.addEventListener('alpine:init', () => {
moveTo(value, focus = true) { moveTo(value, focus = true) {
this.focused = this.clamp(value) this.focused = this.clamp(value)
this.shown = this.focused.slice(0, 8) + '01' this.shown = this.focused.slice(0, 8) + '01'
this.coverMonth(this.shown)
if (focus) { if (focus) {
this.focusDay() this.focusDay()
} }
}, },
// ---- The full-screen range picker's list of months --------------------------------------
/** A month inside the years the picker allows, and inside `min` and `max`. */
clampMonth(month) {
const floor = this.min ? this.min.slice(0, 8) + '01' : `${pad(this.yearsFrom, 4)}-01-01`
const ceiling = this.max ? this.max.slice(0, 8) + '01' : `${pad(this.yearsTo, 4)}-12-01`
return month < floor ? floor : month > ceiling ? ceiling : month
},
/** Keeps a month the keyboard walked to inside the window the list draws. */
coverMonth(month) {
if (this.monthsFrom === null) {
return
}
if (month < this.monthsFrom) {
this.monthsFrom = this.clampMonth(month)
} else if (month > this.monthsTo) {
this.monthsTo = this.clampMonth(month)
}
},
/**
* Grows the window when the scroll comes near either end. Adding months above moves
* everything down, so the scroll is put back by however much the list grew.
*/
extendMonths(event) {
if (this.presentation !== 'full' || this.monthsFrom === null) {
return
}
const list = event.currentTarget
if (list.scrollTop < MONTH_EDGE) {
const from = this.clampMonth(addMonths(this.monthsFrom, -MONTH_WINDOW))
if (from !== this.monthsFrom) {
const before = list.scrollHeight
this.monthsFrom = from
this.$nextTick(() => (list.scrollTop += list.scrollHeight - before))
}
} else if (list.scrollHeight - list.scrollTop - list.clientHeight < MONTH_EDGE) {
this.monthsTo = this.clampMonth(addMonths(this.monthsTo, MONTH_WINDOW))
}
},
/** The single date, or a range's start. */ /** The single date, or a range's start. */
start() { start() {
const value = this.current() const value = this.current()
@@ -805,8 +881,9 @@ document.addEventListener('alpine:init', () => {
}) })
}, },
get weeks() { /** Six weeks of cells for one month, `yyyy-MM-01`. */
const [year, month] = parts(this.shown) weeksOf(shownMonth) {
const [year, month] = parts(shownMonth)
const first = utc(year, month, 1) const first = utc(year, month, 1)
const offset = (first.getUTCDay() - this.firstDay + 7) % 7 const offset = (first.getUTCDay() - this.firstDay + 7) % 7
const range = config.range ? (this.draft ?? { start: null, end: null }) : null const range = config.range ? (this.draft ?? { start: null, end: null }) : null
@@ -836,6 +913,28 @@ document.addEventListener('alpine:init', () => {
})) }))
}, },
/**
* The rows the grid draws: this month's six weeks, or — full screen — every month of the
* window, each headed by its name, which is M3's vertically scrolling range picker. A
* month's empty trailing weeks are dropped there, so one month follows the next.
*/
get rows() {
const weeks = (month) => this.weeksOf(month).map((cells, row) => ({ key: `${month}-${row}`, cells }))
if (this.presentation !== 'full' || this.monthsFrom === null) {
return weeks(this.shown)
}
const rows = []
for (let month = this.monthsFrom; month <= this.monthsTo; month = addMonths(month, 1)) {
rows.push({ key: `label-${month}`, label: this.formats.monthYear.format(utc(...parts(month))) })
rows.push(...weeks(month).filter((row) => row.cells.some((cell) => !cell.blank)))
}
return rows
},
get years() { get years() {
const shown = parts(this.shown)[0] const shown = parts(this.shown)[0]
const current = parts(this.today)[0] const current = parts(this.today)[0]
@@ -10,6 +10,11 @@
calendar in a dialog, with a pencil to switch to typing. calendar in a dialog, with a pencil to switch to typing.
- `input`: the same dialog, opened on its text field, with a calendar icon to switch back. - `input`: the same dialog, opened on its text field, with a calendar icon to switch back.
A `range` picker on a compact window (below `medium`) is M3's full-screen range picker instead
of the 360px dialog: an app bar with a close button and **Save**, the range as the headline
under the supporting text, and the months in one vertically scrolling list rather than stepped
one at a time. Nothing changes for a single date, or for a docked picker at `medium` and up.
The calendar and the dialog's text field pick a draft; OK (or Enter on a day) makes it the The calendar and the dialog's text field pick a draft; OK (or Enter on a day) makes it the
value, Cancel or Escape leaves the value alone and returns focus to the field. What is typed value, Cancel or Escape leaves the value alone and returns focus to the field. What is typed
into the docked field is the value as soon as it is a whole, allowed date. into the docked field is the value as soon as it is a whole, allowed date.
@@ -265,6 +270,30 @@
data-datepicker-picker data-datepicker-picker
> >
<div tabindex="-1" data-datepicker-surface> <div tabindex="-1" data-datepicker-surface>
{{-- M3's full-screen range picker (compact): an app bar with a close button and Save,
then the supporting text and the range as the headline, over a divider. --}}
<div data-datepicker-full x-cloak x-show="presentation === 'full'">
<div data-datepicker-app-bar>
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="cancel()" data-datepicker-close />
<span data-datepicker-app-bar-actions>
<span x-show="! typing">
<x-livewire-material::button icon="edit" :tooltip="__('Switch to text input mode')" x-on:click="toggleTyping()" data-datepicker-switch />
</span>
<span x-show="typing">
<x-livewire-material::button icon="date_range" :tooltip="__('Switch to calendar input mode')" x-on:click="toggleTyping()" data-datepicker-switch />
</span>
<x-livewire-material::button :label="__('Save')" x-on:click="confirm()" data-datepicker-save />
</span>
</div>
<div data-datepicker-full-headline>
<p data-datepicker-title x-text="typing ? @js($inputTitle) : @js($title)">{{ $title }}</p>
<p data-datepicker-headline aria-live="polite" x-text="headline"></p>
</div>
</div>
<div data-datepicker-header x-show="presentation === 'modal'" @if ($range) data-range @endif> <div data-datepicker-header x-show="presentation === 'modal'" @if ($range) data-range @endif>
<p data-datepicker-title x-text="typing ? @js($inputTitle) : @js($title)">{{ $title }}</p> <p data-datepicker-title x-text="typing ? @js($inputTitle) : @js($title)">{{ $title }}</p>
@@ -280,7 +309,7 @@
</div> </div>
</div> </div>
<div x-show="! typing" data-datepicker-calendar> <div x-show="! typing" x-on:scroll="extendMonths($event)" data-datepicker-calendar>
<span id="{{ $id }}-month" class="sr-only" aria-live="polite" x-text="monthYear"></span> <span id="{{ $id }}-month" class="sr-only" aria-live="polite" x-text="monthYear"></span>
<div data-datepicker-nav x-show="presentation === 'modal'"> <div data-datepicker-nav x-show="presentation === 'modal'">
@@ -350,9 +379,14 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template x-for="(week, row) in weeks" :key="row"> <template x-for="row in rows" :key="row.key">
<tr> <tr>
<template x-for="(cell, column) in week" :key="column"> {{-- Full screen: a month's name heads its weeks, M3's month label. --}}
<template x-if="row.label">
<th colspan="7" scope="colgroup" x-text="row.label" data-datepicker-month-label></th>
</template>
<template x-for="(cell, column) in (row.cells ?? [])" :key="column">
<td <td
role="gridcell" role="gridcell"
x-bind:tabindex="cell.blank ? null : (cell.focused ? 0 : -1)" x-bind:tabindex="cell.blank ? null : (cell.focused ? 0 : -1)"
@@ -469,7 +503,8 @@
</div> </div>
</div> </div>
<div x-show="view === 'days' || typing || presentation === 'modal'" data-datepicker-actions> {{-- The full-screen picker confirms from its app bar, so it draws no action row. --}}
<div x-show="presentation !== 'full' && (view === 'days' || typing || presentation === 'modal')" data-datepicker-actions>
<x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-datepicker-cancel /> <x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-datepicker-cancel />
<x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-datepicker-confirm /> <x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-datepicker-confirm />
</div> </div>
@@ -27,6 +27,7 @@
</div> </div>
BLADE, BLADE,
'Range' => <<<'BLADE' 'Range' => <<<'BLADE'
{{-- Narrow the window below 600px: a range picker becomes M3's full-screen one, with a close button, Save, and the months scrolled through. --}}
<div class="grid w-full gap-6 medium:grid-cols-2" x-data="{ trip: { start: '{{ now()->addDays(3)->format('Y-m-d') }}', end: '{{ now()->addDays(9)->format('Y-m-d') }}' }, leave: { start: null, end: null } }"> <div class="grid w-full gap-6 medium:grid-cols-2" x-data="{ trip: { start: '{{ now()->addDays(3)->format('Y-m-d') }}', end: '{{ now()->addDays(9)->format('Y-m-d') }}' }, leave: { start: null, end: null } }">
<div class="grid content-start gap-4"> <div class="grid content-start gap-4">
<x-datepicker label="Trip" range x-model="trip" /> <x-datepicker label="Trip" range x-model="trip" />
@@ -34,7 +35,7 @@
</div> </div>
<div class="grid content-start gap-4"> <div class="grid content-start gap-4">
<x-datepicker label="Leave" range mode="modal" x-model="leave" /> <x-datepicker label="Leave" range mode="modal" x-model="leave" hint="Full screen on a compact window" />
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(leave)"></code></p> <p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(leave)"></code></p>
</div> </div>
</div> </div>
@@ -54,6 +54,41 @@ it('makes the field a read-only trigger for the modal and modal input pickers',
->and(datepickerConfig($html)['mode'])->toBe($mode); ->and(datepickerConfig($html)['mode'])->toBe($mode);
})->with(['modal', 'input']); })->with(['modal', 'input']);
it('gives a range picker M3\'s full-screen presentation, with an app bar and Save', function () {
$html = (string) $this->blade('<x-datepicker id="trip" label="Trip" range mode="modal" />');
expect($html)
->toContain('data-datepicker-full')
->toContain('x-show="presentation === \'full\'"')
->toContain('data-datepicker-app-bar')
->toContain('data-datepicker-close')
->toContain('data-datepicker-save')
->toContain('Save')
->toContain('aria-label="Select dates"')
// The months come from one list of rows, each headed by its label.
->toContain('x-for="row in rows"')
->toContain('data-datepicker-month-label')
// The action row is the modal's; the full-screen picker confirms from its app bar.
->toContain('x-show="presentation !== \'full\' && (view === \'days\' || typing || presentation === \'modal\')"')
->toContain('x-on:scroll="extendMonths($event)"');
expect(datepickerConfig($html))->toMatchArray(['range' => true, 'mode' => 'modal']);
});
it('leaves the docked and single-date pickers as they were', function () {
// Only the presentation is new: a single date never reaches it, whatever the window.
expect(file_get_contents(__DIR__.'/../../../resources/js/datepicker.js'))
->toContain("this.presentation = config.range && this.compact ? 'full' : config.mode === 'docked' && !this.compact ? 'docked' : 'modal'");
$html = (string) $this->blade('<x-datepicker id="expires" label="Expires on" />');
expect($html)
->toContain('data-datepicker-nav data-docked')
->toContain('x-show="presentation === \'docked\'"')
->toContain('data-datepicker-menu-button="months"')
->toContain('x-show="presentation === \'modal\'"');
});
it('names each dropdown so Shift+M and Shift+Y can reach it', function () { it('names each dropdown so Shift+M and Shift+Y can reach it', function () {
$html = (string) $this->blade('<x-datepicker label="Birthday" />'); $html = (string) $this->blade('<x-datepicker label="Birthday" />');