Forms
Time Picker code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/time-picker.blade.php
@props([
'name' => null,
'value' => null,
'placeholder' => 'Select a time',
'size' => 'base',
'icon' => 'clock',
'minuteStep' => 5,
'hour24' => false,
'invalid' => false,
'disabled' => false,
])
@php
/*
| Time picker. A read-only trigger that opens hour / minute (and AM·PM)
| columns in a popover. The chosen time is submitted as 24-hour `HH:MM` via a
| hidden input bound to `name`, regardless of whether it's displayed in 12-
| or 24-hour format.
|
| Usage:
| <x-ui.time-picker name="start_time" />
| <x-ui.time-picker name="opens_at" value="09:30" :minute-step="15" />
| <x-ui.time-picker name="alarm" value="18:00" :hour24="true" />
*/
$sizes = [
'sm' => 'text-sm py-1.5',
'base' => 'text-sm py-2.5',
'lg' => 'text-base py-3',
];
$ring = $invalid
? 'border-red-400 dark:border-red-500 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-700 focus:border-brand-500 focus:ring-brand-500';
$classes = implode(' ', [
'block w-full rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 shadow-sm pe-3.5 text-left transition focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed',
$icon ? 'ps-10' : 'ps-3.5',
$sizes[$size] ?? $sizes['base'],
$ring,
]);
@endphp
<div
x-data="{
open: false,
hour24: @js((bool) $hour24),
minuteStep: @js(max(1, (int) $minuteStep)),
disabled: @js((bool) $disabled),
hour: null,
minute: null,
init() {
const initial = @js($value);
if (initial && /^\d{1,2}:\d{2}$/.test(initial)) {
const [h, m] = initial.split(':').map(Number);
this.hour = h;
this.minute = m;
}
},
get value() {
if (this.hour === null || this.minute === null) { return ''; }
return String(this.hour).padStart(2, '0') + ':' + String(this.minute).padStart(2, '0');
},
get label() {
if (this.value === '') { return ''; }
if (this.hour24) { return this.value; }
const period = this.hour < 12 ? 'AM' : 'PM';
let h = this.hour % 12;
if (h === 0) { h = 12; }
return h + ':' + String(this.minute).padStart(2, '0') + ' ' + period;
},
get hours() {
if (this.hour24) { return Array.from({ length: 24 }, (_, i) => i); }
return Array.from({ length: 12 }, (_, i) => i === 0 ? 12 : i);
},
get minutes() {
const out = [];
for (let m = 0; m < 60; m += this.minuteStep) { out.push(m); }
return out;
},
get period() { return this.hour === null ? 'AM' : (this.hour < 12 ? 'AM' : 'PM'); },
displayHour(h) {
if (this.hour24) { return String(h).padStart(2, '0'); }
return String(h);
},
isHour(h) {
if (this.hour === null) { return false; }
if (this.hour24) { return this.hour === h; }
let current = this.hour % 12;
if (current === 0) { current = 12; }
return current === h;
},
setHour(h) {
if (this.hour24) {
this.hour = h;
} else {
const pm = this.period === 'PM';
let base = h % 12;
this.hour = pm ? base + 12 : base;
}
if (this.minute === null) { this.minute = 0; }
},
setMinute(m) {
this.minute = m;
if (this.hour === null) { this.hour = this.hour24 ? 0 : 12; }
},
setPeriod(p) {
if (this.hour === null) { this.hour = p === 'PM' ? 12 : 0; return; }
const isPm = this.hour >= 12;
if (p === 'PM' && ! isPm) { this.hour += 12; }
if (p === 'AM' && isPm) { this.hour -= 12; }
},
}"
@click.outside="open = false"
@keydown.escape.window="open = false"
class="relative"
>
@if ($name)
<input type="hidden" name="{{ $name }}" :value="value">
@endif
@if ($icon)
<span class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3 text-gray-400 dark:text-gray-500">
<x-ui.icon :name="$icon" size="base" />
</span>
@endif
<button
type="button"
@disabled($disabled)
@if ($invalid) aria-invalid="true" @endif
@click="open = ! open"
class="{{ $classes }}"
>
<span x-show="label" x-text="label"></span>
<span x-show="! label" class="text-gray-400 dark:text-gray-500">{{ $placeholder }}</span>
</button>
<div
x-show="open" x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="absolute z-50 mt-2 flex w-56 rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-2 shadow-lg ring-1 ring-black/5"
>
<div class="flex-1 max-h-56 overflow-auto pe-1 space-y-0.5" role="listbox" aria-label="Hour">
<template x-for="h in hours" :key="h">
<button
type="button"
@click="setHour(h)"
:class="isHour(h) ? 'bg-brand-600 text-white font-semibold' : 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'"
class="flex w-full items-center justify-center rounded-lg py-1.5 text-sm tabular-nums transition cursor-pointer"
x-text="displayHour(h)"
></button>
</template>
</div>
<div class="mx-1 w-px bg-gray-200 dark:bg-gray-800"></div>
<div class="flex-1 max-h-56 overflow-auto pe-1 space-y-0.5" role="listbox" aria-label="Minute">
<template x-for="m in minutes" :key="m">
<button
type="button"
@click="setMinute(m)"
:class="minute === m ? 'bg-brand-600 text-white font-semibold' : 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'"
class="flex w-full items-center justify-center rounded-lg py-1.5 text-sm tabular-nums transition cursor-pointer"
x-text="String(m).padStart(2, '0')"
></button>
</template>
</div>
<template x-if="! hour24">
<div class="ms-1 flex flex-col gap-0.5 ps-1 border-s border-gray-200 dark:border-gray-800">
<template x-for="p in ['AM', 'PM']" :key="p">
<button
type="button"
@click="setPeriod(p)"
:class="period === p ? 'bg-brand-600 text-white font-semibold' : 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'"
class="rounded-lg px-2.5 py-1.5 text-sm font-medium transition cursor-pointer"
x-text="p"
></button>
</template>
</div>
</template>
</div>
</div>