import { createMemo, createSignal, mergeProps, splitProps, type JSX, type Component, type FlowComponent, type ParentComponent, Show, createEffect, on, onMount, type Accessor, type ComponentProps, For, } from "solid-js"; import { assignRef, type Ref, type RefCallback } from "@material-solid/utils/refs"; import { assignInlineVars } from "@vanilla-extract/dynamic"; import { createPresence } from "@solid-primitives/presence"; import { Focus } from "../focus"; import { asAccessor, type MaybeAccessor } from "@solid-primitives/utils"; import { createStaticStore } from "@solid-primitives/static-store"; import { containerStyle, focusStyle, handleIndicatorStyle, handleStyle, inputStyle, labelStyle, labelTextStyle, segmentFraction, segmentShapeStyle, segmentStyle, stopsPolygon, stopsStyle, stopStyle, } from "./slider.css"; import { createElementSize, createResizeObserver } from "@solid-primitives/resize-observer"; import { createContextProvider } from "@solid-primitives/context"; import { createStore } from "solid-js/store"; import { ReactiveSet } from "@solid-primitives/set"; type SliderState = { pressed: boolean; } type SliderStateProviderProps = { state: SliderState; } const createSliderState = () => { return createStore({ pressed: false, }); } const [ SliderStateProvider, useSliderState ] = createContextProvider< SliderState, SliderStateProviderProps >(props => props.state); const normalize = (value: number, min: number, max: number) => { return (value - min) / (max - min); }; export type SliderElement = {}; export type SliderProps = { ref?: Ref; /** * @default 0 */ from?: number; /** * @default 1 */ to?: number; steps?: number; center?: number; value: number; valueFormatter?: (value: number) => number; labelFormatter?: (value: number) => string; onChanged?: (value: number) => void; }; export const Slider: Component = (props) => { const mergedProps = mergeProps({ from: 0, to: 1 }, props); const [local, others] = splitProps(mergedProps, [ "ref", "from", "to", "value", "valueFormatter", "labelFormatter", "onChanged", "steps", ]); assignRef(local.ref, {}); let ref!: HTMLElement; let inputRef!: HTMLInputElement; const [state, setState] = createSliderState(); const setPressed = (value: boolean) => { setState("pressed", value); showLabel(value); } const [labelVisible, setLabelVisible] = createSignal(false); const [labelTimeout, setLabelTimeout] = createSignal(); const fraction = createMemo(() => normalize(local.value, local.from, local.to) ); const value = createMemo(() => { return local.valueFormatter?.(local.value) ?? local.value; }); const label = createMemo(() => { return `${local.labelFormatter?.(value()) ?? value()}`; }); const showLabel = (show: boolean = true) => { const timeout = labelTimeout(); if(timeout !== undefined) clearTimeout(timeout); if(show) setLabelVisible(true); setLabelTimeout( setTimeout( () => { if(!state.pressed) setLabelVisible(false); }, 400, ) as unknown as number, // TODO: fix NodeJS.Timeout error ); } const onChange = (value: number) => { local.onChanged?.(value); showLabel(); } const [data, setData] = createStore<{ segments: { inactive: HTMLElement[]; active: HTMLElement[]; }; polygons: { inactive: string; active: string; }; }>({ segments: { inactive: [], active: [], }, polygons: { inactive: "", active: "", }, }); const getPolygon = (segments: HTMLElement[]): string => { const containerRect = ref.getBoundingClientRect(); return segments.reduce( (polygon, segment) => { const segmentRect = segment.children.item(0)!.getBoundingClientRect(); const left = segmentRect.left - containerRect.left; const right = segmentRect.right - containerRect.left; const next = [ `${left}px 0%`, `${left}px 100%`, `${right}px 100%`, `${right}px 0%`, ]; polygon.push(...next); return polygon; }, [], ).join(","); } createResizeObserver( () => data.segments.inactive, (rect, element, entry) => { setData( "polygons", "inactive", getPolygon(data.segments.inactive), ); }, ); createResizeObserver( () => data.segments.active, (rect, element, entry) => { setData( "polygons", "active", getPolygon(data.segments.active), ); }, ); return (
setPressed(true)} onPointerUp={() => setPressed(false)} onPointerCancel={() => setPressed(false)} onFocusIn={() => { if(inputRef.matches(":focus-visible")) setPressed(true); }} onFocusOut={() => setPressed(false)} onChange={event => onChange(event.currentTarget.valueAsNumber)} onInput={event => onChange(event.currentTarget.valueAsNumber)} type="range" min={local.from} max={local.to} step={local.steps ?? "any"} aria-valuemin={local.from} aria-valuemax={local.to} aria-valuetext={`${local.value}`} aria-label="Slider" /> setData("segments", "inactive", 0, element)} active edge="start" fraction={fraction()} /> setData("segments", "active", 0, element)} edge="end" fraction={1 - fraction()} />
); }; type SliderStopsProps = { active?: boolean; polygon: string; } const SliderStops: Component = (props) => { const state = useSliderState()!; const mergedProps = mergeProps( { active: false }, props, ); const [local, others] = splitProps( mergedProps, ["active", "polygon"], ); return (
{ (item, index) => (
) }
); } type SliderSegmentProps = & { fraction: number; edge?: "start" | "end"; active?: boolean; stop?: boolean; } & JSX.HTMLAttributes; const SliderSegment: Component = (props) => { const mergedProps = mergeProps( { active: false, stop: false }, props, ); const [local, others] = splitProps( mergedProps, [ "ref", "fraction", "edge", "active", "stop", ], ); const state = useSliderState()!; return (
} class={segmentStyle} style={ assignInlineVars({ [segmentFraction]: `${props.fraction}`, }) } {...others}>
) } type SliderHandleProps = { for: MaybeAccessor; label: JSX.Element; labelVisible?: boolean; } const SliderHandle: Component = (props) => { const state = useSliderState()!; return (
); }