@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
620 lines (612 loc) • 19.6 kB
JavaScript
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 };