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
@@ -10,6 +10,11 @@
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.
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
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.
@@ -265,6 +270,30 @@
data-datepicker-picker
>
<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>
<p data-datepicker-title x-text="typing ? @js($inputTitle) : @js($title)">{{ $title }}</p>
@@ -280,7 +309,7 @@
</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>
<div data-datepicker-nav x-show="presentation === 'modal'">
@@ -350,9 +379,14 @@
</tr>
</thead>
<tbody>
<template x-for="(week, row) in weeks" :key="row">
<template x-for="row in rows" :key="row.key">
<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
role="gridcell"
x-bind:tabindex="cell.blank ? null : (cell.focused ? 0 : -1)"
@@ -469,7 +503,8 @@
</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="__('OK')" x-on:click="confirm()" data-datepicker-confirm />
</div>