{{-- A time: M3's time picker, opened from a text field. The field is read-only and opens the picker in a modal `` on a press, Enter, Space or ArrowDown (the clock icon at its end opens it too). The picker is M3's dial: the hour and minute boxes on top, AM and PM beside them on a 12-hour clock, the clock face below. A press or a drag on the dial picks the hour, then it moves on to the minutes; the arrow keys change the value on the focused dial, Home and End go to the ends, Enter confirms, and Escape, Cancel or a press on the scrim close it without a change, handing focus back to the field. The keyboard icon swaps the dial for M3's input variant: two text fields, with the error under a field that holds something impossible. In a landscape window the dial lies on its side, as Compose lays it out. `wire:model` (or `x-model`) holds the time as `H:i`, and null until one is chosen; a value with seconds (`09:30:00`, from a `time` column) is read, and written back as `H:i`. Nothing is written until OK. The draft starts at the bound time, or now. Errors are read from the bag under the `wire:model` name and replace the hint. `format` is `12` or `24`; without it the hour cycle is the locale's (`locale`, the app locale by default) as `Intl.DateTimeFormat` reports it, and the field shows the time as the locale writes it. `step` is the minute step (a tap on the minute dial picks fives, or steps when five is not a multiple of the step; a drag and the arrows move by the step). `min` and `max` (`H:i`, inclusive; a `min` later than `max` spans midnight) grey out and skip what lies outside them and turn typed values outside them into errors — validate on the server as well. `clearable` adds a button that empties the field; `name` posts the value from a hidden input. `label`, `hint`, `icon`, `variant` and `size` are the field's; other attributes (`required`, `disabled`, `placeholder`) reach the field's input. From androidx Compose Material 3 at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326 (Apache-2.0): TimePickerTokens and TimeInputTokens — a surface-container-high dialog with an extra-large corner and elevation 3, a 256dp surface-container-highest dial with body-large numbers, a primary selector with a 48dp handle, a 2dp line and an 8dp centre, 96×80 time selector boxes in display-large (primary-container when selected), 96×72 time fields in display-medium — and TimePicker.kt and TimePickerDialog.kt for the layout, the 24-hour inner ring (12–23, at 69dp; 00–11 outside at 101dp, as Material Components for Android labels them too), the gestures, the move on to minutes and the error texts. The period selector is Compose's current default (`isUpdatedTimepickerToggleEnabled`): two separate shape-morphing toggle buttons in primary-container, not the outlined pair its tokens still describe. The dial's numbers are aria-hidden; the dial is a `slider` whose value text names the hour or minute. The dialog is `wire:ignore`, so a Livewire render never closes an open picker or resets its draft. --}} @props([ 'label' => null, 'hint' => null, 'icon' => null, 'variant' => null, 'size' => 'md', 'format' => null, 'locale' => null, 'min' => null, 'max' => null, 'step' => 1, 'value' => null, 'name' => null, 'clearable' => false, ]) @php $model = $attributes->wire('model')->value() ?: null; $messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; $id = $attributes->get('id') ?? 'timepicker-'.substr(md5($model.'|'.$label.'|'.$name.'|timepicker'), 0, 12); $cycle = in_array((string) $format, ['12', '24'], true) ? (int) $format : null; $locale = str_replace('_', '-', filled($locale) ? $locale : app()->getLocale()); $interval = is_numeric($step) && (int) $step >= 1 && (int) $step <= 60 ? (int) $step : 1; $clock = fn (mixed $time): ?string => is_string($time) && preg_match('/^([01]?\d|2[0-3]):([0-5]\d)/', $time, $parts) === 1 ? sprintf('%02d:%02d', $parts[1], $parts[2]) : null; $earliest = $clock($min); $latest = $clock($max); // Rendered as the bound property already says, so the field is not empty until Alpine starts. $current = $value; if ($model !== null && ($component = \Livewire\Livewire::current()) !== null) { $current = data_get($component, $model); } $current = $clock($current); // The first frame writes the time as the browser will; ICU decides the hour cycle on both sides. $display = ''; if ($current !== null) { $time = \Carbon\CarbonImmutable::createFromFormat('!H:i', $current, 'UTC'); $shown = $cycle; if (class_exists(\IntlDatePatternGenerator::class)) { $generator = new \IntlDatePatternGenerator(str_replace('-', '_', $locale)); $shown ??= preg_match('/[hK]/', preg_replace("/'[^']*'/", '', (string) $generator->getBestPattern('j'))) === 1 ? 12 : 24; $pattern = $generator->getBestPattern($shown === 12 ? 'hmm' : 'Hmm'); $display = (string) (new \IntlDateFormatter(str_replace('-', '_', $locale), \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, 'UTC', null, $pattern))->format($time); } else { $display = $shown === 12 ? $time->format('g:i A') : $time->format('H:i'); } } $config = [ 'locale' => $locale, 'format' => $cycle, 'min' => $earliest, 'max' => $latest, 'step' => $interval, 'strings' => [ 'am' => __('AM'), 'pm' => __('PM'), 'oclock' => __(':hour o\'clock'), 'hours' => __(':hour hours'), 'minutes' => __(':minute minutes'), 'hourError12' => __('Hour must be 1–12'), 'hourError24' => __('Hour must be 0–23'), 'minuteError' => __('Minute must be 0–59'), 'stepError' => __('Minute must be a multiple of :step'), 'between' => __('Choose a time from :min to :max'), 'after' => __('Choose :min or later'), 'before' => __('Choose :max or earlier'), ], ]; $constrained = $earliest !== null || $latest !== null || $interval > 1; // The dial's numbers, placed round the ring from twelve o'clock (TimePicker.kt's CircularLayout). $spot = fn (int $index): string => sprintf('--x: %.4F; --y: %.4F', sin(deg2rad($index * 30)), -cos(deg2rad($index * 30))); $sets = [ 'hour12' => array_map(fn (int $index): array => ['value' => $index ?: 12, 'text' => $index ?: 12, 'index' => $index, 'inner' => false, 'allowed' => "hourAllowed({$index} + (isPm ? 12 : 0))"], range(0, 11)), 'hour24' => [ ...array_map(fn (int $index): array => ['value' => $index, 'text' => $index === 0 ? '00' : $index, 'index' => $index, 'inner' => false, 'allowed' => "hourAllowed({$index})"], range(0, 11)), ...array_map(fn (int $index): array => ['value' => $index + 12, 'text' => $index + 12, 'index' => $index, 'inner' => true, 'allowed' => 'hourAllowed('.($index + 12).')'], range(0, 11)), ], 'minute' => array_map(fn (int $index): array => ['value' => $index * 5, 'text' => $index === 0 ? '00' : $index * 5, 'index' => $index, 'inner' => false, 'allowed' => 'minuteAllowed('.($index * 5).')'], range(0, 11)), ]; $inputAttributes = $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except(['class', 'id', 'wire:key', 'placeholder']); $disabled = (bool) $attributes->get('disabled'); $described = $messages !== [] || filled($hint); @endphp
only(['class', 'wire:key', 'x-model']) }} x-data="materialTimepicker(@if ($model !== null) @entangle($attributes->wire('model')) @else @js($current) @endif, @js($config))" @if ($model === null) x-modelable="value" @endif > @if ($clearable) @endif @if (filled($name)) @endif

{{ __('Select time') }}

@foreach ([false => 'am', true => 'pm'] as $pm => $period) @endforeach
@foreach (['labels', 'ink'] as $layer) @if ($layer === 'ink') @endif @endforeach
@foreach (['hour' => __('Hour'), 'minute' => __('Minute')] as $part => $partLabel) @if ($part === 'minute') @endif

{{ $partLabel }}

@endforeach
@foreach ([false => 'am', true => 'pm'] as $pm => $period) @endforeach