You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/time-range-picker/README.md

13 KiB

TimeRangePicker

TimeRangeField + Popover with per-endpoint sliders. The segmented inputs accept keyboard editing; the popover exposes drag sliders (hour / minute / second) plus an AM/PM toggle for each endpoint (start and end). Picker owns the shared reactive state; TimeRangeField and Popover read the same writableActive refs.

Anatomy

<TimeRangePicker.Provider bind:value bind:open>
	<TimeRangePicker.Label>Meeting window</TimeRangePicker.Label>

	<TimeRangePicker.Input type="start">
		{#snippet children({ segments })}
			{#each segments as segment, i (i)}
				<TimeRangePicker.Segment part={segment.part}>{segment.value}</TimeRangePicker.Segment>
			{/each}
		{/snippet}
	</TimeRangePicker.Input>
	<span aria-hidden="true">→</span>
	<TimeRangePicker.Input type="end">
		{#snippet children({ segments })}
			{#each segments as segment, i (i)}
				<TimeRangePicker.Segment part={segment.part}>{segment.value}</TimeRangePicker.Segment>
			{/each}
		{/snippet}
	</TimeRangePicker.Input>

	<TimeRangePicker.Trigger>🕐</TimeRangePicker.Trigger>

	<TimeRangePicker.Content side="bottom" align="end" sideOffset={8}>
		<!-- START -->
		<TimeRangePicker.DayPeriodToggle type="start" />
		<TimeRangePicker.HourSlider type="start">
			<TimeRangePicker.Range />
			<TimeRangePicker.Thumb index={0} />
		</TimeRangePicker.HourSlider>
		<TimeRangePicker.MinuteSlider type="start">…</TimeRangePicker.MinuteSlider>
		<TimeRangePicker.SecondSlider type="start">…</TimeRangePicker.SecondSlider>

		<!-- END -->
		<TimeRangePicker.DayPeriodToggle type="end" />
		<TimeRangePicker.HourSlider type="end">…</TimeRangePicker.HourSlider>
		<TimeRangePicker.MinuteSlider type="end">…</TimeRangePicker.MinuteSlider>
		<TimeRangePicker.SecondSlider type="end">…</TimeRangePicker.SecondSlider>
	</TimeRangePicker.Content>
</TimeRangePicker.Provider>

The per-endpoint slider / toggle wrappers read their endpoint from the required type: 'start' | 'end' prop and drive the same underlying { start, end } state as the segmented input.

Parts

Part Source Description
Provider range picker Shared value: TimeRange + placeholder + open. Creates Popover + TRF.
Trigger range picker Popover trigger button.
HourSlider range picker Per-endpoint hour slider (type: 'start' | 'end').
MinuteSlider range picker Per-endpoint minute slider.
SecondSlider range picker Per-endpoint second slider.
DayPeriodToggle range picker Per-endpoint AM/PM radio group (rendered only in 12-hour cycle).
Label TimeRangeField Re-export. Single label naming the whole range.
Input TimeRangeField Re-export. One endpoint's segmented input (type: 'start' | 'end').
Segment TimeField Re-export. Resolves to the nearest Input's TimeField ctx.
Content Popover Re-export. Surface mounted via floating layer.
Arrow Popover Re-export.
Close Popover Re-export.
Overlay Popover Re-export.
Anchor Popover Re-export.
Thumb Slider Re-export. Used inside each slider wrapper.
Range Slider Re-export. Used inside each slider wrapper.
Tick Slider Re-export. Optional tick marks.

ARIA

  • Sliders emit role="slider" with per-endpoint aria-label resolved through UIX lang refs (morfo.translations for component-owned labels, langs.ts only for legacy imperative constants).
  • DayPeriodToggle emits role="radiogroup" with two <input type="radio"> items.
  • Input / Segment inherit TimeRangeField / TimeField ARIA.

Data Attributes

Part Attribute Values
Root data-time-range-picker Always present
Trigger data-time-range-picker-trigger Always present
Trigger data-state open | closed
Sliders data-endpoint start | end
DayPeriodToggle data-endpoint start | end
DayPeriodToggle data-time-range-picker-day-period Always present
Input + all data-time-range-field-* Inherited from TimeRangeField
Segment + all data-time-field-segment-* Inherited from TimeField
Content / Arrow / Close / Overlay / Anchor + all data-popover-* Inherited from Popover

Keyboard

  • Input segments: identical to TimeRangeField — arrows cycle, digits auto-advance, Home/End, Tab between segments, Backspace clears, A/P toggles AM/PM.
  • Trigger: Enter/Space opens the popover.
  • Sliders: Arrow keys step by {hour,minute,second}Step, Home/End jump to min/max.
  • DayPeriodToggle: Arrow keys / Space toggle between AM and PM.
  • Popover: Escape closes; focus returns to the trigger (non-modal via FocusScope).

Props

See types.ts for the JSDoc-documented surface. Summary:

Prop Type Notes
value TimeRange Bindable { start, end }.
onValueChange (v) => void Fires when both endpoints resolve.
onStartValueChange / onEndValueChange (v) => void Every endpoint change (even incomplete).
placeholder TimeValue Bindable. Drives format for both inputs.
open boolean Bindable. Popover open state.
onOpenChange / onOpenChangeComplete (open) => void Change callbacks + post-animation.
closeOnRangeComplete boolean Auto-close when both endpoints set. @default false
validate TimeRangeValidator Receives { start, end } — return error or ∅.
minValue / maxValue TimeValue Applied to both endpoints.
disabled / readonly / required boolean Merged with parent Field if present.
startReadonlySegments EditableTimeSegmentPart[] Per-endpoint locked segments on start (A25).
endReadonlySegments EditableTimeSegmentPart[] Per-endpoint locked segments on end (A25).
granularity 'hour' | 'minute' | 'second' Drives segment set for both inputs.
hourCycle 12 | 24 Numeric. Falls back to locale.
hideTimeZone boolean Hide tz segment on ZonedDateTime.
hourStep / minuteStep / secondStep number Slider step sizes. @default 1
locale / dir string / 'ltr' | 'rtl' Inherited from Soma when omitted.
errorMessageId string External error element id (aria-describedby).

Field integration

Wrapping in Field.Provider inherits disabled/readonly/required/invalid into the picker (OR-merged). Labels/helper-text from Field wire normally.

Validation

Range validation runs on the provider when both endpoints are set and fires onInvalid with one of:

Reason Condition
min start < minValue
max end > maxValue or start > maxValue
invalid end < start (natural order)
custom validate({start,end}) returned a message

Comparison with reference libraries

Feature React Aria Ark UI Bits UI Soma
Dedicated time-range picker ✗ ✗ ✗ ✓ Soma innovation
Paired segmented inputs ✓ (in Dateᵣ) ✓ (in Dateᵣ) ✓ (in Dateᵣ) ✓
Per-endpoint popover sliders ✗ ✗ ✗ ✓
closeOnRangeComplete ✗ closeOnComplete (DateRangePicker) ✗ ✓
onStartValueChange / onEndValueChange ✓ (in Dateᵣ) ✓ ✓ ✓
Range-level validate ✓ (in Dateᵣ) ✓ ✓ ✓
Natural order check (end ≥ start) ✓ ✓ ✓ ✓
Per-endpoint readonlySegments ✗ ✗ ✓ ✓ (A25)
hh/mm/ss placeholders ✗ ✗ ✗ ✓ (A28)
beforeinput blocks IME/paste ✗ ✗ ✗ ✓ (A26)
Shared state via single composed provider ✓ ✓ ✓ ✓ (A27)
Parent Field integration via Form via FieldRoot ✗ ✓ (soma Field)
Anatomy — — — 17 parts (6 unique + 11 re-exports)

No mainstream library ships a dedicated time-range picker — they collapse it into a date-range picker via CalendarDateTime. Soma keeps them separate so pure time ranges (office hours, meeting windows) do not force a date.

Example — meeting scheduler

<script lang="ts">
	import { TimeRangePicker } from '$soma/components';
	import { Time } from '$libs/days';
	import type { TimeRange } from '$libs/days';

	let range = $state<TimeRange>({ start: undefined, end: undefined });
	let open = $state(false);
</script>

<TimeRangePicker.Provider
	bind:value={range}
	bind:open
	placeholder={new Time(9, 0, 0)}
	hourCycle={24}
	closeOnRangeComplete
>
	<TimeRangePicker.Label>Availability</TimeRangePicker.Label>
	<TimeRangePicker.Input type="start" name="start">
		{#snippet children({ segments })}
			{#each segments as segment, i (i)}
				<TimeRangePicker.Segment part={segment.part}>{segment.value}</TimeRangePicker.Segment>
			{/each}
		{/snippet}
	</TimeRangePicker.Input>
	<TimeRangePicker.Input type="end" name="end">
		{#snippet children({ segments })}
			{#each segments as segment, i (i)}
				<TimeRangePicker.Segment part={segment.part}>{segment.value}</TimeRangePicker.Segment>
			{/each}
		{/snippet}
	</TimeRangePicker.Input>

	<TimeRangePicker.Trigger>⏰</TimeRangePicker.Trigger>

	<TimeRangePicker.Content side="bottom" align="end">
		<!-- start + end sliders + AM/PM toggles -->
	</TimeRangePicker.Content>
</TimeRangePicker.Provider>

Powered by TurnKey Linux.