# 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 ```svelte Meeting window {#snippet children({ segments })} {#each segments as segment, i (i)} {segment.value} {/each} {/snippet} {#snippet children({ segments })} {#each segments as segment, i (i)} {segment.value} {/each} {/snippet} πŸ• … … … … … ``` 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 `` 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 ```svelte Availability {#snippet children({ segments })} {#each segments as segment, i (i)} {segment.value} {/each} {/snippet} {#snippet children({ segments })} {#each segments as segment, i (i)} {segment.value} {/each} {/snippet} ⏰ ```