Skip to main content

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>