UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

620 lines (612 loc) 19.6 kB
import { createSize } from '../chunk/B2CSS4CB.js'; import { COLOR_INTL_TRANSLATIONS, parseColor } from '../chunk/MQHWXXI2.js'; import { useLocale } from '../chunk/XHJPQEZP.js'; import { FORM_CONTROL_FIELD_PROP_NAMES, createFormControlField } from '../chunk/HYP2U57X.js'; import { FormControlLabel } from '../chunk/7ZHN3PYD.js'; export { FormControlLabel as Label } from '../chunk/7ZHN3PYD.js'; import { createFormResetListener } from '../chunk/ANN3A2QM.js'; import { FormControlErrorMessage } from '../chunk/ICNSTULC.js'; export { FormControlErrorMessage as ErrorMessage } from '../chunk/ICNSTULC.js'; import { FormControlDescription, useFormControlContext, FORM_CONTROL_PROP_NAMES, createFormControl, FormControlContext } from '../chunk/YKGT7A57.js'; export { FormControlDescription as Description } from '../chunk/YKGT7A57.js'; import { createControllableSignal } from '../chunk/BLN63FDC.js'; import { Polymorphic } from '../chunk/6Y7B2NEO.js'; import { spread, mergeProps, createComponent, template } from 'solid-js/web'; import { mergeDefaultProps, visuallyHiddenStyles, createGenerateId, access, mergeRefs, callHandler } from '@kobalte/utils'; import { createContext, useContext, splitProps, createSignal, createUniqueId, createMemo } from 'solid-js'; import { combineStyle } from '@solid-primitives/props'; var ColorWheelContext = createContext(); function useColorWheelContext() { const context = useContext(ColorWheelContext); if (context === void 0) { throw new Error("[kobalte]: `useColorWheelContext` must be used within a `ColorWheel` component"); } return context; } // src/color-wheel/color-wheel-input.tsx var _tmpl$ = /* @__PURE__ */ template(`<input type="range">`); function ColorWheelInput(props) { const formControlContext = useFormControlContext(); const context = useColorWheelContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("input") }, props); const [local, formControlFieldProps, others] = splitProps(mergedProps, ["style", "onChange"], FORM_CONTROL_FIELD_PROP_NAMES); const { fieldProps } = createFormControlField(formControlFieldProps); const onChange = (e) => { callHandler(e, local.onChange); const target = e.target; context.state.setHue(Number.parseFloat(target.value)); target.value = String(context.state.hue()) ?? ""; }; return (() => { const _el$ = _tmpl$(); _el$.addEventListener("change", onChange); spread(_el$, mergeProps({ get id() { return fieldProps.id(); }, get name() { return formControlContext.name(); }, get tabIndex() { return context.state.isDisabled() ? void 0 : -1; }, get min() { return context.state.minValue(); }, get max() { return context.state.maxValue(); }, get step() { return context.state.step(); }, get value() { return context.state.hue(); }, get required() { return formControlContext.isRequired(); }, get disabled() { return formControlContext.isDisabled(); }, get readonly() { return formControlContext.isReadOnly(); }, get style() { return combineStyle({ ...visuallyHiddenStyles }, local.style); }, get ["aria-valuetext"]() { return context.getThumbValueLabel(); }, get ["aria-label"]() { return fieldProps.ariaLabel(); }, get ["aria-labelledby"]() { return fieldProps.ariaLabelledBy(); }, get ["aria-describedby"]() { return fieldProps.ariaDescribedBy(); }, get ["aria-invalid"]() { return formControlContext.validationState() === "invalid" || void 0; }, get ["aria-required"]() { return formControlContext.isRequired() || void 0; }, get ["aria-disabled"]() { return formControlContext.isDisabled() || void 0; }, get ["aria-readonly"]() { return formControlContext.isReadOnly() || void 0; } }, () => formControlContext.dataset(), others), false, false); return _el$; })(); } // src/color-wheel/utils.ts function roundToStep(value, step) { return Math.round(value / step) * step; } function mod(n, m) { return (n % m + m) % m; } function roundDown(v) { const r = Math.floor(v); if (r === v) { return v - 1; } return r; } function degToRad(deg) { return deg * Math.PI / 180; } function radToDeg(rad) { return rad * 180 / Math.PI; } function angleToCartesian(angle, radius) { const rad = degToRad(360 - angle + 90); const x = Math.sin(rad) * radius; const y = Math.cos(rad) * radius; return { x, y }; } function cartesianToAngle(x, y, radius) { const deg = radToDeg(Math.atan2(y / radius, x / radius)); return (deg + 360) % 360; } // src/color-wheel/create-color-wheel-state.ts function createColorWheelState(props) { const mergedProps = mergeDefaultProps( { isDisabled: () => false }, props ); const defaultValue = createMemo(() => { return mergedProps.defaultValue() ?? parseColor("hsl(0, 100%, 50%)"); }); const [value, setValue] = createControllableSignal({ value: mergedProps.value, defaultValue, onChange: (value2) => mergedProps.onChange?.(value2) }); const color = createMemo(() => { const colorSpace = value().getColorSpace(); return colorSpace === "hsl" || colorSpace === "hsb" ? value() : value().toFormat("hsl"); }); const channelRange = () => color().getChannelRange("hue"); const step = () => channelRange().step; const pageSize = () => channelRange().pageSize; const maxValue = () => channelRange().maxValue; const minValue = () => channelRange().minValue; const [isDragging, setIsDragging] = createSignal(false); const resetValue = () => { setValue(defaultValue()); }; const hue = () => color().getChannelValue("hue"); const setHue = (value2) => { let newValue = value2 > 360 ? 0 : value2; newValue = roundToStep(mod(newValue, 360), step()); if (hue() !== newValue) { setValue(color().withChannelValue("hue", newValue)); } }; const increment = (stepSize = 1) => { const newStepSize = Math.max(stepSize, step()); let newValue = hue() + newStepSize; if (newValue >= maxValue()) { newValue = minValue(); } setHue(roundToStep(mod(newValue, 360), newStepSize)); }; const decrement = (stepSize = 1) => { const newStepSize = Math.max(stepSize, step()); if (hue() === 0) { setHue(roundDown(360 / newStepSize) * newStepSize); } else { setHue(roundToStep(mod(hue() - newStepSize, 360), newStepSize)); } }; const getThumbPosition = () => angleToCartesian(hue(), mergedProps.thumbRadius()); const setThumbValue = (x, y, radius) => { if (mergedProps.isDisabled()) return; setHue(cartesianToAngle(x, y, radius)); }; const updateDragging = (dragging) => { if (mergedProps.isDisabled()) return; const wasDragging = isDragging(); setIsDragging(dragging); if (wasDragging && !isDragging()) { mergedProps.onChangeEnd?.(color()); } }; return { value: color, setValue, hue, setHue, step, pageSize, maxValue, minValue, increment, decrement, getThumbPosition, setThumbValue, isDragging, setIsDragging: updateDragging, resetValue, isDisabled: mergedProps.isDisabled }; } // src/color-wheel/color-wheel-root.tsx function ColorWheelRoot(props) { const [ref, setRef] = createSignal(); const defaultId = `colorwheel-${createUniqueId()}`; const mergedProps = mergeDefaultProps({ id: defaultId, getValueLabel: (param) => param.formatChannelValue("hue"), translations: COLOR_INTL_TRANSLATIONS, disabled: false, thickness: 30 }, props); const [local, formControlProps, others] = splitProps(mergedProps, ["ref", "value", "defaultValue", "thickness", "onChange", "onChangeEnd", "getValueLabel", "translations", "disabled"], FORM_CONTROL_PROP_NAMES); const { formControlContext } = createFormControl(formControlProps); const { direction } = useLocale(); const [trackRef, setTrackRef] = createSignal(); const [thumbRef, setThumbRef] = createSignal(); const size = createSize(trackRef); const outerRadius = createMemo(() => { if (size.width() === 0) return void 0; return size.width() / 2; }); const thumbRadius = () => (139.75 - local.thickness / 100 * 70) * outerRadius() / 140; const state = createColorWheelState({ value: () => local.value, defaultValue: () => local.defaultValue, thumbRadius, onChange: local.onChange, onChangeEnd: local.onChangeEnd, isDisabled: () => formControlContext.isDisabled() ?? false }); createFormResetListener(ref, () => state.resetValue()); const isLTR = () => direction() === "ltr"; let currentPosition = null; const onDragStart = (value) => { state.setIsDragging(true); state.setThumbValue(value[0], value[1], Math.sqrt(value[0] * value[0] + value[1] * value[1])); currentPosition = null; }; const onDrag = ({ deltaX, deltaY }) => { if (currentPosition === null) { currentPosition = state.getThumbPosition(); } currentPosition.x += deltaX; currentPosition.y += deltaY; state.setThumbValue(currentPosition.x, currentPosition.y, thumbRadius()); local.onChange?.(state.value()); }; const onDragEnd = () => { state.setIsDragging(false); thumbRef()?.focus(); }; const getThumbValueLabel = () => `${state.value().formatChannelValue("hue")}, ${context.state.value().getHueName(local.translations)}`; const onHomeKeyDown = (event) => { if (!formControlContext.isDisabled()) { event.preventDefault(); event.stopPropagation(); state.setHue(state.minValue()); } }; const onEndKeyDown = (event) => { if (!formControlContext.isDisabled()) { event.preventDefault(); event.stopPropagation(); state.setHue(state.maxValue()); } }; const onStepKeyDown = (event) => { if (!formControlContext.isDisabled()) { switch (event.key) { case "Left": case "ArrowLeft": event.preventDefault(); event.stopPropagation(); if (!isLTR()) { state.increment(event.shiftKey ? state.pageSize() : state.step()); } else { state.decrement(event.shiftKey ? state.pageSize() : state.step()); } break; case "Down": case "ArrowDown": event.preventDefault(); event.stopPropagation(); state.decrement(event.shiftKey ? state.pageSize() : state.step()); break; case "Up": case "ArrowUp": event.preventDefault(); event.stopPropagation(); state.increment(event.shiftKey ? state.pageSize() : state.step()); break; case "Right": case "ArrowRight": event.preventDefault(); event.stopPropagation(); if (!isLTR()) { state.decrement(event.shiftKey ? state.pageSize() : state.step()); } else { state.increment(event.shiftKey ? state.pageSize() : state.step()); } break; case "Home": onHomeKeyDown(event); break; case "End": onEndKeyDown(event); break; case "PageUp": event.preventDefault(); event.stopPropagation(); state.increment(state.pageSize()); break; case "PageDown": event.preventDefault(); event.stopPropagation(); state.decrement(state.pageSize()); break; } } }; const context = { state, outerRadius, thickness: () => local.thickness, onDragStart, onDrag, onDragEnd, getThumbValueLabel, getValueLabel: local.getValueLabel, onStepKeyDown, trackRef, setTrackRef, thumbRef, setThumbRef, generateId: createGenerateId(() => access(formControlProps.id)) }; return createComponent(FormControlContext.Provider, { value: formControlContext, get children() { return createComponent(ColorWheelContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs(setRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, role: "group", get id() { return access(formControlProps.id); } }, () => formControlContext.dataset(), others)); } }); } }); } function ColorWheelThumb(props) { const context = useColorWheelContext(); const formControlContext = useFormControlContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("thumb") }, props); const [local, formControlFieldProps, others] = splitProps(mergedProps, ["style", "onKeyDown", "onPointerDown", "onPointerMove", "onPointerUp"], FORM_CONTROL_FIELD_PROP_NAMES); const { fieldProps } = createFormControlField(formControlFieldProps); const onKeyDown = (e) => { callHandler(e, local.onKeyDown); context.onStepKeyDown(e); }; const [sRect, setRect] = createSignal(); const getValueFromPointer = (pointerPosition) => { const rect = sRect() || context.trackRef().getBoundingClientRect(); setRect(rect); return [pointerPosition.x - rect.left - rect.width / 2, pointerPosition.y - rect.top - rect.height / 2]; }; let startPosition = { x: 0, y: 0 }; const onPointerDown = (e) => { callHandler(e, local.onPointerDown); const target = e.currentTarget; e.preventDefault(); e.stopPropagation(); target.setPointerCapture(e.pointerId); target.focus(); const value = getValueFromPointer({ x: e.clientX, y: e.clientY }); startPosition = { x: e.clientX, y: e.clientY }; context.onDragStart?.(value); }; const onPointerMove = (e) => { e.stopPropagation(); callHandler(e, local.onPointerMove); const target = e.currentTarget; if (target.hasPointerCapture(e.pointerId)) { const delta = { deltaX: e.clientX - startPosition.x, deltaY: e.clientY - startPosition.y }; context.onDrag?.(delta); startPosition = { x: e.clientX, y: e.clientY }; } }; const onPointerUp = (e) => { e.stopPropagation(); callHandler(e, local.onPointerUp); const target = e.currentTarget; if (target.hasPointerCapture(e.pointerId)) { target.releasePointerCapture(e.pointerId); context.onDragEnd?.(); } }; return createComponent(Polymorphic, mergeProps({ as: "span", ref(r$) { const _ref$ = mergeRefs(context.setThumbRef, props.ref); typeof _ref$ === "function" && _ref$(r$); }, role: "slider", get id() { return fieldProps.id(); }, get tabIndex() { return context.state.isDisabled() ? void 0 : 0; }, get style() { return combineStyle({ position: "absolute", left: `${context.outerRadius() + context.state.getThumbPosition().x}px`, top: `${context.outerRadius() + context.state.getThumbPosition().y}px`, transform: "translate(-50%, -50%)", "forced-color-adjust": "none", "touch-action": "none", opacity: context.outerRadius() ? 1 : 0, transition: "opacity .1s linear", "--kb-color-current": context.state.value().toString() }, local.style); }, get ["aria-valuetext"]() { return context.getThumbValueLabel(); }, get ["aria-valuemin"]() { return context.state.minValue(); }, get ["aria-valuenow"]() { return context.state.hue(); }, get ["aria-valuemax"]() { return context.state.maxValue(); }, get ["aria-label"]() { return fieldProps.ariaLabel(); }, get ["aria-labelledby"]() { return fieldProps.ariaLabelledBy(); }, get ["aria-describedby"]() { return fieldProps.ariaDescribedBy(); }, onKeyDown, onPointerDown, onPointerMove, onPointerUp }, () => formControlContext.dataset(), others)); } function ColorWheelTrack(props) { const context = useColorWheelContext(); const formControlContext = useFormControlContext(); const [local, others] = splitProps(props, ["style", "onPointerDown", "onPointerMove", "onPointerUp"]); const [sRect, setRect] = createSignal(); const getValueFromPointer = (pointerPosition) => { const rect = sRect() || context.trackRef().getBoundingClientRect(); setRect(rect); return [pointerPosition.x - rect.left - rect.width / 2, pointerPosition.y - rect.top - rect.height / 2]; }; let startPosition = { x: 0, y: 0 }; const onPointerDown = (e) => { callHandler(e, local.onPointerDown); const target = e.target; target.setPointerCapture(e.pointerId); e.preventDefault(); const value = getValueFromPointer({ x: e.clientX, y: e.clientY }); startPosition = { x: e.clientX, y: e.clientY }; context.onDragStart?.(value); }; const onPointerMove = (e) => { callHandler(e, local.onPointerMove); const target = e.target; if (target.hasPointerCapture(e.pointerId)) { context.onDrag?.({ deltaX: e.clientX - startPosition.x, deltaY: e.clientY - startPosition.y }); startPosition = { x: e.clientX, y: e.clientY }; } }; const onPointerUp = (e) => { callHandler(e, local.onPointerUp); const target = e.target; if (target.hasPointerCapture(e.pointerId)) { target.releasePointerCapture(e.pointerId); setRect(void 0); context.onDragEnd?.(); } }; const backgroundStyle = ` conic-gradient( from 90deg, ${[...Array(13).keys()].map((i) => `hsl(${i * 30} 100% 50%)`).join(",")} ) `; return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs(context.setTrackRef, props.ref); typeof _ref$ === "function" && _ref$(r$); }, get style() { return combineStyle({ "touch-action": "none", "forced-color-adjust": "none", background: backgroundStyle, "clip-path": "circle(50%)", mask: `radial-gradient(#0000 ${70 - context.thickness() / 100 * 70}%, #000 ${70.5 - context.thickness() / 100 * 70}%)` }, local.style); }, onPointerDown, onPointerMove, onPointerUp }, () => formControlContext.dataset(), others)); } function ColorWheelValueLabel(props) { const context = useColorWheelContext(); const formControlContext = useFormControlContext(); return createComponent(Polymorphic, mergeProps({ as: "div" }, () => formControlContext.dataset(), props, { get children() { return context.getValueLabel(context.state.value()); } })); } // src/color-wheel/index.tsx var ColorWheel = Object.assign(ColorWheelRoot, { Description: FormControlDescription, ErrorMessage: FormControlErrorMessage, Input: ColorWheelInput, Label: FormControlLabel, Thumb: ColorWheelThumb, Track: ColorWheelTrack, ValueLabel: ColorWheelValueLabel }); export { ColorWheel, ColorWheelInput as Input, ColorWheelRoot as Root, ColorWheelThumb as Thumb, ColorWheelTrack as Track, ColorWheelValueLabel as ValueLabel, useColorWheelContext };