@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
724 lines (716 loc) • 24.9 kB
JavaScript
import { parseColor, COLOR_INTL_TRANSLATIONS } from '../chunk/MQHWXXI2.js';
import { linearScale } from '../chunk/YSW4UOAR.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 { createComponent, mergeProps, spread, memo, template } from 'solid-js/web';
import { mergeRefs, mergeDefaultProps, createGenerateId, access, callHandler, visuallyHiddenStyles, clamp, snapValueToStep } from '@kobalte/utils';
import { combineStyle } from '@solid-primitives/props';
import { createContext, useContext, splitProps, createSignal, createMemo, createUniqueId } from 'solid-js';
var ColorAreaContext = createContext();
function useColorAreaContext() {
const context = useContext(ColorAreaContext);
if (context === void 0) {
throw new Error("[kobalte]: `useColorAreaContext` must be used within a `ColorArea` component");
}
return context;
}
// src/color-area/color-area-background.tsx
function ColorAreaBackground(props) {
const context = useColorAreaContext();
const formControlContext = useFormControlContext();
const [local, others] = splitProps(props, ["style", "onPointerDown", "onPointerMove", "onPointerUp"]);
const {
direction
} = useLocale();
const [sRect, setRect] = createSignal();
const getValueFromPointer = (pointerPosition) => {
const rect = sRect() || context.backgroundRef().getBoundingClientRect();
const xInput = [0, rect.width];
const xOutput = [context.state.xMinValue(), context.state.xMaxValue()];
const yInput = [0, rect.height];
const yOutput = [context.state.yMinValue(), context.state.yMaxValue()];
const xValue = linearScale(xInput, xOutput);
const yValue = linearScale(yInput, yOutput);
setRect(rect);
return [xValue(pointerPosition.x - rect.left), yValue(pointerPosition.y - rect.top)];
};
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 backgroundStyles = createMemo(() => {
const end = direction() === "ltr" ? "right" : "left";
const zValue = context.state.value().getChannelValue(context.state.channels().zChannel);
switch (context.state.value().getColorSpace()) {
case "rgb": {
const rgb = parseColor("rgb(0, 0, 0)");
return {
background: [`linear-gradient(to ${end}, ${rgb.withChannelValue(context.state.channels().xChannel, 0)}, ${rgb.withChannelValue(context.state.channels().xChannel, 255)})`, `linear-gradient(to top, ${rgb.withChannelValue(context.state.channels().yChannel, 0)}, ${rgb.withChannelValue(context.state.channels().yChannel, 255)})`, rgb.withChannelValue(context.state.channels().zChannel, zValue)].join(","),
"background-blend-mode": "screen"
};
}
case "hsl": {
const value = parseColor("hsl(0, 100%, 50%)").withChannelValue(context.state.channels().zChannel, zValue);
const bg = context.state.value().getColorChannels().filter((c) => c !== context.state.channels().zChannel).map((c) => `linear-gradient(to ${c === context.state.channels().xChannel ? end : "top"}, ${hslChannels[c](value)})`).reverse();
if (context.state.channels().zChannel === "hue") {
bg.push(value.toString("css"));
}
return {
background: bg.join(", ")
};
}
case "hsb": {
const value = parseColor("hsb(0, 100%, 100%)").withChannelValue(context.state.channels().zChannel, zValue);
const bg = context.state.value().getColorChannels().filter((c) => c !== context.state.channels().zChannel).map((c) => `linear-gradient(to ${c === context.state.channels().xChannel ? end : "top"}, ${hsbChannels[c](value)})`).reverse();
if (context.state.channels().zChannel === "hue") {
bg.push(value.toString("css"));
}
return {
background: bg.join(", ")
};
}
}
});
return createComponent(Polymorphic, mergeProps({
as: "div",
ref(r$) {
const _ref$ = mergeRefs(context.setBackgroundRef, props.ref);
typeof _ref$ === "function" && _ref$(r$);
},
get style() {
return combineStyle({
"touch-action": "none",
"forced-color-adjust": "none",
...backgroundStyles()
}, local.style);
},
onPointerDown,
onPointerMove,
onPointerUp
}, () => formControlContext.dataset(), others));
}
var hue = (color) => [0, 60, 120, 180, 240, 300, 360].map((hue2) => color.withChannelValue("hue", hue2).toString("css")).join(", ");
var saturation = (color) => `${color.withChannelValue("saturation", 0)}, transparent`;
var hslChannels = {
hue,
saturation,
lightness: () => "black, transparent, white"
};
var hsbChannels = {
hue,
saturation,
brightness: () => "black, transparent"
};
var _tmpl$ = /* @__PURE__ */ template(`<input type="range">`);
function ColorAreaHiddenInputBase(props) {
const formControlContext = useFormControlContext();
const context = useColorAreaContext();
const mergedProps = mergeDefaultProps({
id: context.generateId("input"),
orientation: "horizontal"
}, props);
const [local, formControlFieldProps, others] = splitProps(mergedProps, ["style", "orientation", "onChange", "onFocus", "onBlur"], FORM_CONTROL_FIELD_PROP_NAMES);
const {
fieldProps
} = createFormControlField(formControlFieldProps);
const isVertical = () => local.orientation === "vertical";
const ariaLabel = () => {
return [fieldProps.ariaLabel(), context.translations().colorPicker].filter(Boolean).join(", ");
};
const onChange = (e) => {
callHandler(e, local.onChange);
const target = e.target;
isVertical() ? context.state.setYValue(Number.parseFloat(target.value)) : context.state.setXValue(Number.parseFloat(target.value));
target.value = String(isVertical() ? context.state.yValue() : context.state.xValue()) ?? "";
};
const valueText = createMemo(() => {
const channel = isVertical() ? context.state.channels().yChannel : context.state.channels().xChannel;
return `${context.state.value().getChannelName(channel, COLOR_INTL_TRANSLATIONS)} ${context.state.value().formatChannelValue(channel)}, ${context.state.value().getColorName(COLOR_INTL_TRANSLATIONS)}`;
});
return (() => {
const _el$ = _tmpl$();
_el$.addEventListener("change", onChange);
spread(_el$, mergeProps({
get id() {
return fieldProps.id();
},
get name() {
return (isVertical() ? context.yName() : context.xName()) || formControlContext.name();
},
get tabIndex() {
return context.state.isDisabled() ? void 0 : -1;
},
get min() {
return memo(() => !!isVertical())() ? context.state.yMinValue() : context.state.xMinValue();
},
get max() {
return memo(() => !!isVertical())() ? context.state.yMaxValue() : context.state.xMaxValue();
},
get step() {
return memo(() => !!isVertical())() ? context.state.yStep() : context.state.xStep();
},
get value() {
return memo(() => !!isVertical())() ? context.state.yValue() : context.state.xValue();
},
get required() {
return formControlContext.isRequired();
},
get disabled() {
return formControlContext.isDisabled();
},
get readonly() {
return formControlContext.isReadOnly();
},
get style() {
return combineStyle({
...visuallyHiddenStyles
}, local.style);
},
get ["aria-roledescription"]() {
return context.translations().twoDimensionalSlider;
},
get ["aria-valuetext"]() {
return valueText();
},
get ["aria-orientation"]() {
return local.orientation;
},
get ["aria-label"]() {
return 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;
},
get ["data-orientation"]() {
return local.orientation;
}
}, () => formControlContext.dataset(), others), false, false);
return _el$;
})();
}
// src/color-area/color-area-hidden-input-x.tsx
function ColorAreaHiddenInputX(props) {
return createComponent(ColorAreaHiddenInputBase, props);
}
function ColorAreaHiddenInputY(props) {
return createComponent(ColorAreaHiddenInputBase, mergeProps({
orientation: "vertical"
}, props));
}
// src/color-area/color-area.intl.ts
var COLOR_AREA_INTL_TRANSLATIONS = {
colorPicker: "Color picker",
twoDimensionalSlider: "2D slider"
};
function createColorAreaState(props) {
const mergedProps = mergeDefaultProps(
{
isDisabled: () => false,
defaultValue: () => parseColor("hsl(0, 100%, 50%)")
},
props
);
const [value, setValue] = createControllableSignal({
value: mergedProps.value,
defaultValue: mergedProps.defaultValue,
onChange: (value2) => mergedProps.onChange?.(value2)
});
const color = createMemo(() => {
return mergedProps.colorSpace?.() ? value().toFormat(mergedProps.colorSpace()) : value();
});
const channels = createMemo(() => {
return color().getColorSpaceAxes({
xChannel: mergedProps.xChannel?.(),
yChannel: mergedProps.yChannel?.()
});
});
const xChannelRange = () => color().getChannelRange(channels().xChannel);
const yChannelRange = () => color().getChannelRange(channels().yChannel);
const xStep = () => xChannelRange().step;
const yStep = () => yChannelRange().step;
const xPageSize = () => xChannelRange().pageSize;
const yPageSize = () => yChannelRange().pageSize;
const xMaxValue = () => xChannelRange().maxValue;
const xMinValue = () => xChannelRange().minValue;
const yMaxValue = () => yChannelRange().maxValue;
const yMinValue = () => yChannelRange().minValue;
const [isDragging, setIsDragging] = createSignal(false);
const initialValue = color();
const resetValue = () => {
setValue(initialValue);
};
const xValue = () => color().getChannelValue(channels().xChannel);
const yValue = () => color().getChannelValue(channels().yChannel);
const setXValue = (value2) => {
if (value2 === xValue())
return;
setValue(color().withChannelValue(channels().xChannel, value2));
};
const setYValue = (value2) => {
if (value2 === yValue())
return;
setValue(color().withChannelValue(channels().yChannel, value2));
};
const incrementX = (stepSize = 1) => {
setXValue(
xValue() + stepSize > xMaxValue() ? xMaxValue() : snapValueToStep(
xValue() + stepSize,
xMinValue(),
xMaxValue(),
xStep()
)
);
};
const incrementY = (stepSize = 1) => {
setYValue(
yValue() + stepSize > yMaxValue() ? yMaxValue() : snapValueToStep(
yValue() + stepSize,
yMinValue(),
yMaxValue(),
yStep()
)
);
};
const decrementX = (stepSize = 1) => {
setXValue(
snapValueToStep(xValue() - stepSize, xMinValue(), xMaxValue(), xStep())
);
};
const decrementY = (stepSize = 1) => {
setYValue(
snapValueToStep(yValue() - stepSize, yMinValue(), yMaxValue(), yStep())
);
};
const getThumbPosition = () => {
const x = (xValue() - xMinValue()) / (xMaxValue() - xMinValue());
const y = (yMaxValue() - (yValue() - yMinValue())) / (yMaxValue() - yMinValue());
return { x, y };
};
const getValuePercent = () => {
const x = (xValue() - xMinValue()) / xMaxValue() - xMinValue();
const y = (yValue() - yMinValue()) / yMaxValue() - yMinValue();
return { x, y };
};
const updateValue = (value2) => {
if (mergedProps.isDisabled())
return;
const xSnappedValue = snapValueToStep(
value2.x,
xMinValue(),
xMaxValue(),
xStep()
);
const ySnappedValue = snapValueToStep(
value2.y,
yMinValue(),
yMaxValue(),
yStep()
);
if (xSnappedValue === xValue() && ySnappedValue === yValue())
return;
setValue(
color().withChannelValue(channels().xChannel, xSnappedValue).withChannelValue(channels().yChannel, ySnappedValue)
);
};
const setThumbPercent = (value2) => {
updateValue(getPercentValues(value2.x, value2.y));
};
const getRoundedValues = (value2) => {
const x = Math.round((value2.x - xMinValue()) / xStep()) * xStep() + xMinValue();
const y = Math.round((value2.y - yMinValue()) / yStep()) * yStep() + yMinValue();
return { x, y };
};
const getPercentValues = (xPercent, yPercent) => {
const x = xPercent * (xMaxValue() - xMinValue()) + xMinValue();
const y = yPercent * (yMaxValue() - yMinValue()) + yMinValue();
const roundedValues = getRoundedValues({ x, y });
return {
x: clamp(roundedValues.x, xMinValue(), xMaxValue()),
y: clamp(roundedValues.y, yMinValue(), yMaxValue())
};
};
const updateDragging = (dragging) => {
if (mergedProps.isDisabled())
return;
const wasDragging = isDragging();
setIsDragging(dragging);
if (wasDragging && !isDragging()) {
mergedProps.onChangeEnd?.(color());
}
};
return {
value: color,
xValue,
yValue,
xStep,
yStep,
xPageSize,
yPageSize,
xMaxValue,
yMaxValue,
xMinValue,
yMinValue,
setValue,
setXValue,
setYValue,
incrementX,
decrementX,
incrementY,
decrementY,
getThumbPosition,
isDragging,
setIsDragging: updateDragging,
channels,
resetValue,
setThumbPercent,
setThumbValue: updateValue,
getThumbPercent: getValuePercent,
isDisabled: mergedProps.isDisabled
};
}
// src/color-area/color-area-root.tsx
function ColorAreaRoot(props) {
let ref;
const defaultId = `colorarea-${createUniqueId()}`;
const mergedProps = mergeDefaultProps({
id: defaultId,
translations: COLOR_AREA_INTL_TRANSLATIONS,
disabled: false,
defaultValue: parseColor("hsl(0, 100%, 50%)")
}, props);
const [local, formControlProps, others] = splitProps(mergedProps, ["ref", "value", "defaultValue", "colorSpace", "xChannel", "yChannel", "onChange", "onChangeEnd", "translations", "xName", "yName", "disabled"], FORM_CONTROL_PROP_NAMES);
const {
formControlContext
} = createFormControl(formControlProps);
const {
direction
} = useLocale();
const [backgroundRef, setBackgroundRef] = createSignal();
const [thumbRef, setThumbRef] = createSignal();
const state = createColorAreaState({
value: () => local.value,
defaultValue: () => local.defaultValue,
onChange: local.onChange,
onChangeEnd: local.onChangeEnd,
colorSpace: () => local.colorSpace,
xChannel: () => local.xChannel,
yChannel: () => local.yChannel,
isDisabled: () => formControlContext.isDisabled() ?? false
});
createFormResetListener(() => ref, () => state.resetValue());
const isLTR = () => direction() === "ltr";
let currentPosition = null;
const onDragStart = (value) => {
state.setIsDragging(true);
state.setThumbValue({
x: value[0],
y: context.state.yMaxValue() - value[1]
});
currentPosition = null;
};
const onDrag = ({
deltaX,
deltaY
}) => {
const {
width,
height
} = backgroundRef().getBoundingClientRect();
if (currentPosition === null) {
currentPosition = {
x: state.getThumbPercent().x * width,
y: state.getThumbPercent().y * height
};
}
currentPosition.x += deltaX;
currentPosition.y += -deltaY;
const xPercent = clamp(currentPosition.x / width, 0, 1);
const yPercent = clamp(currentPosition.y / height, 0, 1);
state.setThumbPercent({
x: xPercent,
y: yPercent
});
local.onChange?.(state.value());
};
const onDragEnd = () => {
state.setIsDragging(false);
thumbRef()?.focus();
};
const getDisplayColor = () => {
return state.value().withChannelValue("alpha", 1);
};
const onHomeKeyDown = (event) => {
if (!formControlContext.isDisabled()) {
event.preventDefault();
event.stopPropagation();
if (!isLTR()) {
state.incrementX(state.xPageSize());
} else {
state.decrementX(state.xPageSize());
}
}
};
const onEndKeyDown = (event) => {
if (!formControlContext.isDisabled()) {
event.preventDefault();
event.stopPropagation();
if (!isLTR()) {
state.decrementX(state.xPageSize());
} else {
state.incrementX(state.xPageSize());
}
}
};
const onStepKeyDown = (event) => {
if (!formControlContext.isDisabled()) {
switch (event.key) {
case "Left":
case "ArrowLeft":
event.preventDefault();
event.stopPropagation();
if (!isLTR()) {
state.incrementX(event.shiftKey ? state.xPageSize() : state.xStep());
} else {
state.decrementX(event.shiftKey ? state.xPageSize() : state.xStep());
}
break;
case "Down":
case "ArrowDown":
event.preventDefault();
event.stopPropagation();
state.decrementY(event.shiftKey ? state.yPageSize() : state.yStep());
break;
case "Up":
case "ArrowUp":
event.preventDefault();
event.stopPropagation();
state.incrementY(event.shiftKey ? state.yPageSize() : state.yStep());
break;
case "Right":
case "ArrowRight":
event.preventDefault();
event.stopPropagation();
if (!isLTR()) {
state.decrementX(event.shiftKey ? state.xPageSize() : state.xStep());
} else {
state.incrementX(event.shiftKey ? state.xPageSize() : state.xStep());
}
break;
case "Home":
onHomeKeyDown(event);
break;
case "End":
onEndKeyDown(event);
break;
case "PageUp":
event.preventDefault();
event.stopPropagation();
state.incrementY(state.yPageSize());
break;
case "PageDown":
event.preventDefault();
event.stopPropagation();
state.decrementY(state.yPageSize());
break;
}
}
};
const context = {
state,
xName: () => local.xName,
yName: () => local.yName,
onDragStart,
onDrag,
onDragEnd,
translations: () => local.translations,
getDisplayColor,
onStepKeyDown,
backgroundRef,
setBackgroundRef,
thumbRef,
setThumbRef,
generateId: createGenerateId(() => access(formControlProps.id))
};
return createComponent(FormControlContext.Provider, {
value: formControlContext,
get children() {
return createComponent(ColorAreaContext.Provider, {
value: context,
get children() {
return createComponent(Polymorphic, mergeProps({
as: "div",
ref(r$) {
const _ref$ = mergeRefs((el) => ref = el, local.ref);
typeof _ref$ === "function" && _ref$(r$);
},
role: "group",
get id() {
return access(formControlProps.id);
}
}, () => formControlContext.dataset(), others));
}
});
}
});
}
function ColorAreaThumb(props) {
const context = useColorAreaContext();
const formControlContext = useFormControlContext();
const [local, others] = splitProps(props, ["style", "aria-label", "onKeyDown", "onPointerDown", "onPointerMove", "onPointerUp"]);
const ariaLabel = () => {
const xChannel = context.state.channels().xChannel;
const yChannel = context.state.channels().yChannel;
const xChannelName = `${context.state.value().getChannelName(xChannel, COLOR_INTL_TRANSLATIONS)} ${context.state.value().formatChannelValue(xChannel)}`;
const yChannelName = `${context.state.value().getChannelName(yChannel, COLOR_INTL_TRANSLATIONS)} ${context.state.value().formatChannelValue(yChannel)}`;
const colorName = context.state.value().getColorName(COLOR_INTL_TRANSLATIONS);
return local["aria-label"] ?? [xChannelName, yChannelName, colorName].join(", ");
};
const onKeyDown = (e) => {
callHandler(e, local.onKeyDown);
context.onStepKeyDown(e);
};
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();
startPosition = {
x: e.clientX,
y: e.clientY
};
context.onDragStart?.([context.state.xValue(), context.state.yMaxValue() - context.state.yValue()]);
};
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: "presentation",
get tabIndex() {
return context.state.isDisabled() ? void 0 : 0;
},
get style() {
return combineStyle({
position: "absolute",
left: `${context.state.getThumbPosition().x * 100}%`,
top: `${context.state.getThumbPosition().y * 100}%`,
transform: "translate(-50%, -50%)",
"forced-color-adjust": "none",
"touch-action": "none",
"--kb-color-current": context.state.value().toString()
}, local.style);
},
get ["aria-label"]() {
return ariaLabel();
},
onKeyDown,
onPointerDown,
onPointerMove,
onPointerUp
}, () => formControlContext.dataset(), others));
}
// src/color-area/index.tsx
var ColorArea = Object.assign(ColorAreaRoot, {
Description: FormControlDescription,
ErrorMessage: FormControlErrorMessage,
Label: FormControlLabel,
Background: ColorAreaBackground,
Thumb: ColorAreaThumb,
HiddenInputX: ColorAreaHiddenInputX,
HiddenInputY: ColorAreaHiddenInputY
});
export { ColorAreaBackground as Background, ColorArea, ColorAreaHiddenInputX as HiddenInputX, ColorAreaHiddenInputY as HiddenInputY, ColorAreaRoot as Root, ColorAreaThumb as Thumb, useColorAreaContext };