Draw the timepicker without Tailwind

Plan step 36 (inputs group, last of the "not started" pair): <x-timepicker>
renders data-md-timepicker-* throughout (data-md-mode, data-md-cycle,
data-md-view, data-md-inner, data-md-dragging, data-md-disabled,
data-md-error and data-md-value replace their unprefixed hooks), and
timepicker.css moves into material.components, importing field.css,
icon.css and button.css. The dialog's Tailwind utility class (position,
the scrim, the spatial-fast pop and its @starting-style) becomes plain
declarations under data-md-timepicker-dialog, matching the datepicker's
modal dialog. The landscape rule stays keyed on orientation and viewport
height, in px (560/364/346), never a breakpoint, as M3 asks.

The hour and minute boxes and the period buttons swap their Tailwind
`state-layer focus-ring` utilities for `md-state-layer md-focus-ring`:
same declarations, so nothing else in the stylesheet changes for them.
The dial swaps in `md-focus-ring` for its own hand-rolled, byte-identical
outline-on-:focus-visible rule, which timepicker.css no longer carries.
The period selector was already the radio-in-a-list M3's labelling table
asks for (role="radio" in a role="radiogroup", moved by the arrow keys),
and Shift+M/Y already reached the date picker's dropdowns — both
predate this rewrite.

field.css's data-md-field-button note is tidied now that every component
that selects it (input, password, datepicker, timepicker) is rewritten.

Browser tests written but not run (the inputs group's Chromium run
follows this stream): the period radios moving with the arrow keys, and
the dial lying on its side below the landscape height threshold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 19:19:34 +02:00
co-authored by Claude Opus 5
parent 72d9dbfc89
commit d46b07c13c
9 changed files with 340 additions and 254 deletions
+50 -50
View File
@@ -143,7 +143,7 @@
x-data="materialTimepicker(@if ($model !== null) @entangle($attributes->wire('model')) @else @js($current) @endif, @js($config))"
@if ($model === null) x-modelable="value" @endif
>
<x-livewire-material::field :$id :$label :$hint :$messages :$icon :$size :$variant data-timepicker-field>
<x-livewire-material::field :$id :$label :$hint :$messages :$icon :$size :$variant data-md-timepicker-field>
<input
{{ $inputAttributes }}
id="{{ $id }}"
@@ -189,7 +189,7 @@
data-md-field-trailing
data-md-field-button
aria-label="{{ __('Choose time') }}"
data-timepicker-open
data-md-timepicker-open
@disabled($disabled)
>
<x-livewire-material::icon name="schedule" :size="$fieldIconSize" />
@@ -206,33 +206,32 @@
wire:ignore
x-ref="dialog"
aria-labelledby="{{ $id }}-title"
data-timepicker-dialog
data-md-timepicker-dialog
x-on:close="closed()"
x-on:pointerdown="scrimPressed = $event.target === $el"
x-on:click.self="if (scrimPressed) cancel()"
class="m-auto overflow-visible bg-transparent p-0 text-on-surface backdrop:bg-scrim/32 opacity-100 scale-100 starting:opacity-0 starting:scale-95 transition-[opacity,scale] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast"
>
<div data-timepicker-surface x-bind:data-mode="mode" data-mode="dial">
<h2 id="{{ $id }}-title" data-timepicker-title x-text="mode === 'dial' ? @js(__('Select time')) : @js(__('Enter time'))">{{ __('Select time') }}</h2>
<div data-md-timepicker-surface x-bind:data-md-mode="mode" data-md-mode="dial">
<h2 id="{{ $id }}-title" data-md-timepicker-title x-text="mode === 'dial' ? @js(__('Select time')) : @js(__('Enter time'))">{{ __('Select time') }}</h2>
<div data-timepicker-picker>
<div data-timepicker-display x-bind:data-cycle="is24 ? '24' : '12'">
<div data-timepicker-numbers>
<div data-md-timepicker-picker>
<div data-md-timepicker-display x-bind:data-md-cycle="is24 ? '24' : '12'">
<div data-md-timepicker-numbers>
<button
type="button"
data-timepicker-box="hour"
class="state-layer focus-ring"
data-md-timepicker-box="hour"
class="md-state-layer md-focus-ring"
aria-pressed="true"
x-bind:aria-pressed="(view === 'hour').toString()"
x-bind:aria-label="@js(__('Select hour')) + ', ' + hourLabel"
x-on:click="choose('hour')"
x-text="hourLabel"
></button>
<span data-timepicker-separator aria-hidden="true">:</span>
<span data-md-timepicker-separator aria-hidden="true">:</span>
<button
type="button"
data-timepicker-box="minute"
class="state-layer focus-ring"
data-md-timepicker-box="minute"
class="md-state-layer md-focus-ring"
aria-pressed="false"
x-bind:aria-pressed="(view === 'minute').toString()"
x-bind:aria-label="@js(__('Select minutes')) + ', ' + minuteLabel"
@@ -242,7 +241,7 @@
</div>
<div
data-timepicker-period
data-md-timepicker-period
role="radiogroup"
aria-label="{{ __('Select AM or PM') }}"
x-show="! is24"
@@ -254,8 +253,8 @@
@foreach ([false => 'am', true => 'pm'] as $pm => $period)
<button
type="button"
data-timepicker-period-option="{{ $period }}"
class="state-layer focus-ring"
data-md-timepicker-period-option="{{ $period }}"
class="md-state-layer md-focus-ring"
role="radio"
aria-checked="false"
x-bind:aria-checked="({{ $pm ? '' : '! ' }}isPm).toString()"
@@ -270,7 +269,8 @@
<div
x-ref="dial"
data-timepicker-dial
class="md-focus-ring"
data-md-timepicker-dial
role="slider"
tabindex="0"
aria-valuemin="0"
@@ -278,31 +278,31 @@
x-bind:aria-valuemax="view === 'hour' ? 23 : 59"
x-bind:aria-valuenow="view === 'hour' ? hour : minute"
x-bind:aria-valuetext="valueText"
x-bind:data-view="view"
x-bind:data-cycle="is24 ? '24' : '12'"
x-bind:data-inner="inner ? '' : null"
x-bind:data-dragging="dragging ? '' : null"
x-bind:data-md-view="view"
x-bind:data-md-cycle="is24 ? '24' : '12'"
x-bind:data-md-inner="inner ? '' : null"
x-bind:data-md-dragging="dragging ? '' : null"
x-bind:style="{ '--timepicker-angle': angle + 'deg' }"
x-on:pointerdown="press($event)"
x-on:keydown="key($event)"
>
@foreach (['labels', 'ink'] as $layer)
@if ($layer === 'ink')
<div data-timepicker-selector aria-hidden="true">
<span data-timepicker-track></span>
<span data-timepicker-centre></span>
<span data-timepicker-handle></span>
<div data-md-timepicker-selector aria-hidden="true">
<span data-md-timepicker-track></span>
<span data-md-timepicker-centre></span>
<span data-md-timepicker-handle></span>
</div>
@endif
<div data-timepicker-{{ $layer }} aria-hidden="true">
<div data-md-timepicker-{{ $layer }} aria-hidden="true">
@foreach ($sets as $set => $labels)
<div data-timepicker-set="{{ $set }}">
<div data-md-timepicker-set="{{ $set }}">
@foreach ($labels as $spotLabel)
<span
@if ($layer === 'labels') data-timepicker-label="{{ $set }}" data-value="{{ $spotLabel['value'] }}" @endif
@if ($spotLabel['inner']) data-inner @endif
@if ($constrained && $layer === 'labels') x-bind:data-disabled="{{ $spotLabel['allowed'] }} ? null : ''" @endif
@if ($layer === 'labels') data-md-timepicker-label="{{ $set }}" data-md-value="{{ $spotLabel['value'] }}" @endif
@if ($spotLabel['inner']) data-md-inner @endif
@if ($constrained && $layer === 'labels') x-bind:data-md-disabled="{{ $spotLabel['allowed'] }} ? null : ''" @endif
style="{{ $spot($spotLabel['index']) }}"
>{{ $spotLabel['text'] }}</span>
@endforeach
@@ -313,21 +313,21 @@
</div>
</div>
<div data-timepicker-typing>
<div data-timepicker-inputs>
<div data-md-timepicker-typing>
<div data-md-timepicker-inputs>
@foreach (['hour' => __('Hour'), 'minute' => __('Minute')] as $part => $partLabel)
@if ($part === 'minute')
<span data-timepicker-separator aria-hidden="true">:</span>
<span data-md-timepicker-separator aria-hidden="true">:</span>
@endif
<div data-timepicker-column>
<div data-md-timepicker-column>
<input
id="{{ $id }}-{{ $part }}"
type="text"
inputmode="numeric"
maxlength="2"
autocomplete="off"
data-timepicker-input="{{ $part }}"
data-md-timepicker-input="{{ $part }}"
x-ref="{{ $part }}Input"
aria-label="{{ $partLabel }}"
aria-describedby="{{ $id }}-{{ $part }}-support"
@@ -339,16 +339,16 @@
/>
<p
id="{{ $id }}-{{ $part }}-support"
data-timepicker-support
data-md-timepicker-support
aria-live="polite"
x-bind:data-error="{{ $part }}Error ? '' : null"
x-bind:data-md-error="{{ $part }}Error ? '' : null"
x-text="{{ $part }}Error ?? @js($partLabel)"
>{{ $partLabel }}</p>
</div>
@endforeach
<div
data-timepicker-period
data-md-timepicker-period
role="radiogroup"
aria-label="{{ __('Select AM or PM') }}"
x-show="! is24"
@@ -360,8 +360,8 @@
@foreach ([false => 'am', true => 'pm'] as $pm => $period)
<button
type="button"
data-timepicker-period-option="{{ $period }}"
class="state-layer focus-ring"
data-md-timepicker-period-option="{{ $period }}"
class="md-state-layer md-focus-ring"
role="radio"
aria-checked="false"
x-bind:aria-checked="({{ $pm ? '' : '! ' }}isPm).toString()"
@@ -374,19 +374,19 @@
</div>
</div>
<p data-timepicker-range-error role="alert" x-show="rangeError" x-text="rangeError"></p>
<p data-md-timepicker-range-error role="alert" x-show="rangeError" x-text="rangeError"></p>
</div>
<div data-timepicker-actions>
<span data-timepicker-when="dial">
<x-livewire-material::button icon="keyboard" :tooltip="__('Switch to text input mode')" x-on:click="toggleMode()" data-timepicker-mode="input" />
<div data-md-timepicker-actions>
<span data-md-timepicker-when="dial">
<x-livewire-material::button icon="keyboard" :tooltip="__('Switch to text input mode')" x-on:click="toggleMode()" data-md-timepicker-mode="input" />
</span>
<span data-timepicker-when="input">
<x-livewire-material::button icon="schedule" :tooltip="__('Switch to clock mode')" x-on:click="toggleMode()" data-timepicker-mode="dial" />
<span data-md-timepicker-when="input">
<x-livewire-material::button icon="schedule" :tooltip="__('Switch to clock mode')" x-on:click="toggleMode()" data-md-timepicker-mode="dial" />
</span>
<span data-timepicker-spacer></span>
<x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-timepicker-cancel />
<x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-timepicker-confirm />
<span data-md-timepicker-spacer></span>
<x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-md-timepicker-cancel />
<x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-md-timepicker-confirm />
</div>
</div>
</dialog>