import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { omit, untrack } from "solid-js"; import { userRef } from "../../internals/compose-refs"; import { useLabel } from "../../internals/labelable-provider/use-label.ts"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import { useRegisteredLabelId } from "../../internals/use-registered-label-id"; import { type SliderRootState, useSliderRootContext, } from "../slider-root-context.ts"; import { sliderDataAttrs } from "../utils/state-attrs.ts"; export type SliderLabelState = SliderRootState; export interface SliderLabelProps extends Omit, "id"> { /** Tag/component to render. Defaults to `"div"`. */ as?: ValidComponent; } /** * Accessible label automatically associated with the slider root (via * `aria-labelledby`) and thumb inputs (via the labelable provider). Renders * a `
` (or `as` override). * * Registers its id twice — once on the root's label id setter (so the root's * `aria-labelledby` picks it up in the `resolveAriaLabelledBy` chain), and * once into the labelable provider (so nested thumb ``s discover it * through `useAriaLabelledBy`). Matches React's double-registration setup. * * Clicking the label focuses the lone thumb's `` when the * slider is not a range (there is exactly one `input[type="range"]` inside * `controlRef`). For range sliders this is intentionally a no-op — there's no * single correct focus target. Matches React `SliderLabel.tsx:29-49`. * * §1 wave 3: replaces `renderElement` with `dynamic(() => props.as ?? "div")`. */ export function SliderLabel(props: SliderLabelProps): JSX.Element { const context = useSliderRootContext(); // Register with the parent root's label id setter — drives its // `resolveAriaLabelledBy` fallback chain. The override `id` is handled // through `useRegisteredLabelId`, which also generates an id when absent. const rootLabelId = useRegisteredLabelId( untrack(() => context.rootLabelId()), context.setLabelId, ); // Register with the labelable provider so thumb inputs pick this up via // `useAriaLabelledBy`. The `id` returned here is the same id we just // registered with the root — we pass it as an override so both registries // see the same value. const { id } = useLabel({ id: rootLabelId }); function handleClick(event: MouseEvent): void { // Only focus when there's exactly one thumb input inside the control. // Range sliders have multiple inputs and there's no sensible default. const control = context.controlRef.current; if (!control) return; const target = event.target as Element | null; if ( target != null && target.closest("button,input,select,textarea") != null ) { return; } const inputs = control.querySelectorAll( 'input[type="range"]', ); if (inputs.length === 1) { inputs[0]?.focus({ preventScroll: true }); } } // Resolve `class`/`style` here so function values (stateful styling) are // invoked with the slider state instead of being spread to the DOM raw. const userClassStyle = resolveClassStyle(props, () => context.state()); const userProps = omit( props as Record, "as", "id", "class", "style", "attrs", ); const internalSlot = { id, onClick: handleClick, }; let merged: Record = mergeRootProps( internalSlot, sliderDataAttrs(context.state), ); merged = mergeRootProps(merged, userClassStyle); merged = mergeRootProps(merged, userProps); const Tag = dynamic(() => props.as ?? "div"); return ; } export namespace SliderLabel { export type State = SliderLabelState; export type Props = SliderLabelProps; }