generated from boboko/starter
122 lines
5.2 KiB
PHP
122 lines
5.2 KiB
PHP
@props([
|
||
'min',
|
||
'max',
|
||
'minValue' => null,
|
||
'maxValue' => null,
|
||
'step' => 1,
|
||
'name' => null,
|
||
'legend' => 'Range',
|
||
'minLabel' => 'Minimum',
|
||
'maxLabel' => 'Maximum',
|
||
'prefix' => '',
|
||
'suffix' => '',
|
||
'separator' => ' – ',
|
||
])
|
||
|
||
{{-- The default slot sits on the readout row, to the right of the min–max
|
||
text — use it for a "clear"/"reset" control when the slider is filtered.
|
||
Left empty otherwise. --}}
|
||
|
||
@php
|
||
$minValue ??= $min;
|
||
$maxValue ??= $max;
|
||
// Stable when a name is given so focus can survive a re-render; random
|
||
// otherwise, just to keep the label/input association unique on the page.
|
||
$uid = 'rs-' . ($name ?: uniqid());
|
||
@endphp
|
||
|
||
|
||
|
||
<div
|
||
data-controller="range-slider"
|
||
data-range-slider-min-value="{{ $min }}"
|
||
data-range-slider-max-value="{{ $max }}"
|
||
data-range-slider-step-value="{{ $step }}"
|
||
data-range-slider-prefix-value="{{ $prefix }}"
|
||
data-range-slider-suffix-value="{{ $suffix }}"
|
||
data-range-slider-separator-value="{{ $separator }}"
|
||
{{ $attributes->merge(['class' => 'flex flex-col gap-4']) }}
|
||
>
|
||
<fieldset class="m-0 min-w-0 border-0 p-0">
|
||
<legend class="sr-only">{{ $legend }}</legend>
|
||
|
||
{{-- Real controls — keyboard, assistive tech, form values, no-JS
|
||
fallback. The controller adds `sr-only` to each on connect. --}}
|
||
<label data-range-slider-target="field" class="flex items-center gap-2 text-sm">
|
||
<span>{{ $minLabel }}</span>
|
||
<input
|
||
type="range"
|
||
id="{{ $uid }}-min"
|
||
min="{{ $min }}"
|
||
max="{{ $max }}"
|
||
step="{{ $step }}"
|
||
value="{{ $minValue }}"
|
||
@if($name) name="{{ $name }}_min" @endif
|
||
data-range-slider-target="minInput"
|
||
data-action="input->range-slider#onInput change->range-slider#onChange focus->range-slider#syncFocus blur->range-slider#syncFocus"
|
||
>
|
||
</label>
|
||
|
||
<label data-range-slider-target="field" class="flex items-center gap-2 text-sm">
|
||
<span>{{ $maxLabel }}</span>
|
||
<input
|
||
type="range"
|
||
id="{{ $uid }}-max"
|
||
min="{{ $min }}"
|
||
max="{{ $max }}"
|
||
step="{{ $step }}"
|
||
value="{{ $maxValue }}"
|
||
@if($name) name="{{ $name }}_max" @endif
|
||
data-range-slider-target="maxInput"
|
||
data-action="input->range-slider#onInput change->range-slider#onChange focus->range-slider#syncFocus blur->range-slider#syncFocus"
|
||
>
|
||
</label>
|
||
|
||
{{-- Presentational track — pointer control + visual state only; the
|
||
real controls above own accessibility. --min / --max start at
|
||
the extremes so first paint has no jump. --}}
|
||
<div
|
||
data-range-slider-target="track"
|
||
data-action="pointerdown->range-slider#trackPointerDown"
|
||
aria-hidden="true"
|
||
class="relative h-8 touch-none select-none [--min:0%] [--max:100%]"
|
||
>
|
||
<span class="pointer-events-none absolute inset-x-0 top-1/2 h-1.25 -translate-y-1/2 bg-neutral-500"></span>
|
||
<span class="pointer-events-none absolute top-1/2 left-[var(--min)] right-[calc(100%_-_var(--max))] h-1.25 -translate-y-1/2 bg-black"></span>
|
||
|
||
<button
|
||
type="button"
|
||
tabindex="-1"
|
||
data-range-slider-target="minThumb"
|
||
data-action="pointerdown->range-slider#thumbPointerDown"
|
||
class="absolute top-1/2 left-[var(--min)] grid h-8 w-6 -translate-x-1/4 -translate-y-1/2 cursor-grab place-items-center text-black active:cursor-grabbing"
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 32" fill="none" aria-hidden="true" class="h-[32px] w-[22px]">
|
||
<path d="M17 4 L6 16 L17 28" stroke="currentColor" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" />
|
||
</svg>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
tabindex="-1"
|
||
data-range-slider-target="maxThumb"
|
||
data-action="pointerdown->range-slider#thumbPointerDown"
|
||
class="absolute top-1/2 left-[var(--max)] grid h-8 w-6 -translate-x-1/2 -translate-y-1/2 cursor-grab place-items-center text-black active:cursor-grabbing"
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 32" fill="none" aria-hidden="true" class="h-[32px] w-[22px]">
|
||
<path d="M5 4 L16 16 L5 28" stroke="currentColor" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<div class="flex items-center justify-between gap-4">
|
||
{{-- Server-rendered so it's right on first paint and without JS; the
|
||
controller rewrites it as the values change. --}}
|
||
<span data-range-slider-target="output" aria-hidden="true" class="text-sm tabular-nums"
|
||
>{{ $prefix }}{{ $minValue }}{{ $suffix }}{{ $separator }}{{ $prefix }}{{ $maxValue }}{{ $suffix }}</span>
|
||
|
||
{{ $slot }}
|
||
</div>
|
||
</div>
|