import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { omit } from "solid-js"; import { userRef } from "../../internals/compose-refs"; import { isHydrating } from "../../internals/is-hydrating.ts"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import { valueToPercent } from "../../internals/value-to-percent"; import { type SliderRootState, useSliderRootContext, } from "../slider-root-context.ts"; import { sliderDataAttrs } from "../utils/state-attrs.ts"; export interface SliderIndicatorState extends SliderRootState {} export interface SliderIndicatorProps extends BaseUIComponentProps<"div", SliderIndicatorState> { /** Tag/component to render. Defaults to `"div"`. */ as?: ValidComponent; } function getCenteredStyles( vertical: boolean, range: boolean, startPercent: number, endPercent: number, ): JSX.CSSProperties { const startEdge = vertical ? "bottom" : "inset-inline-start"; const mainSide = vertical ? "height" : "width"; const crossSide = vertical ? "width" : "height"; const styles: JSX.CSSProperties = { position: vertical ? "absolute" : "relative", [crossSide]: "inherit", }; if (!range) { styles[startEdge] = 0; styles[mainSide] = `${startPercent}%`; return styles; } styles[startEdge] = `${startPercent}%`; styles[mainSide] = `${endPercent - startPercent}%`; return styles; } function getInsetStyles( vertical: boolean, range: boolean, start: number | undefined, end: number | undefined, renderBeforeHydration: boolean, isHydrating: boolean, ): JSX.CSSProperties { const visibility = start === undefined || (range && end === undefined) ? "hidden" : undefined; const startEdge = vertical ? "bottom" : "inset-inline-start"; const mainSide = vertical ? "height" : "width"; const crossSide = vertical ? "width" : "height"; const styles: JSX.CSSProperties = { visibility: renderBeforeHydration && isHydrating ? "hidden" : visibility, position: vertical ? "absolute" : "relative", [crossSide]: "inherit", "--start-position": `${start ?? 0}%`, }; if (!range) { styles[startEdge] = 0; styles[mainSide] = "var(--start-position)"; return styles; } styles["--relative-size"] = `${(end ?? 0) - (start ?? 0)}%`; styles[startEdge] = "var(--start-position)"; styles[mainSide] = "var(--relative-size)"; return styles; } /** * Visualizes the current value of the slider. Renders a `
` (or `as` * override). * * Center-aligned indicators use pure-CSS percent math (`(value - min) / * (max - min)`). Edge-aligned indicators pull positions from the root's * `indicatorPosition` signal, which is written from ``'s * ResizeObserver — this compensates for the thumb's own width so its edge * (not its center) aligns with the control endpoints. * * §1 wave 3: replaces `renderElement` with `dynamic(() => props.as ?? "div")`. */ export function SliderIndicator(props: SliderIndicatorProps): JSX.Element { const context = useSliderRootContext(); // Resolve `class`/`style` here so function values (stateful styling) are // invoked with the slider state instead of being spread to the DOM raw. // The internal `style` (the indicator position) is merged first below, so // a user-supplied style composes with it rather than replacing it. const userClassStyle = resolveClassStyle(props, () => context.state()); const userProps = omit(props, "as", "class", "style", "attrs"); const internalSlot = { // SSR hook for the deferred prehydration script (see // `slider-control.tsx` for the symmetric attr on the control). // Seed `""` so Solid 2's DOM spread keeps the key in `ownKeys` // per `test/internals/merge-props-undefined-attr.test.tsx`. get "data-quoin-slider-indicator"() { return context.renderBeforeHydration() ? "" : ""; }, get style(): JSX.CSSProperties { const values = context.values(); const min = context.min(); const max = context.max(); const vertical = context.orientation() === "vertical"; const range = values.length > 1; if (context.inset()) { const [start, end] = context.indicatorPosition(); return getInsetStyles( vertical, range, start, end, context.renderBeforeHydration(), isHydrating(), ); } const start = valueToPercent(values[0] ?? min, min, max); const end = valueToPercent(values[values.length - 1] ?? max, min, max); return getCenteredStyles(vertical, range, start, end); }, }; 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 SliderIndicator { export type State = SliderIndicatorState; export type Props = SliderIndicatorProps; }