@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
777 lines (764 loc) • 24.8 kB
JSX
import {
COLOR_INTL_TRANSLATIONS,
parseColor
} from "../chunk/KNLKPWUU.jsx";
import {
linearScale
} from "../chunk/GIRHAWVP.jsx";
import {
useLocale
} from "../chunk/LR7LBJN3.jsx";
import {
FORM_CONTROL_FIELD_PROP_NAMES,
createFormControlField
} from "../chunk/NGHEENNE.jsx";
import {
FormControlLabel
} from "../chunk/FOXVCQFV.jsx";
import {
createFormResetListener
} from "../chunk/QJIB6BDF.jsx";
import {
FormControlErrorMessage
} from "../chunk/ZZYKR3VO.jsx";
import {
FORM_CONTROL_PROP_NAMES,
FormControlContext,
FormControlDescription,
createFormControl,
useFormControlContext
} from "../chunk/XUUROM4M.jsx";
import "../chunk/JNCCF6MP.jsx";
import {
createControllableSignal
} from "../chunk/FN6EICGO.jsx";
import "../chunk/OYES4GOP.jsx";
import {
Polymorphic
} from "../chunk/FLVHQV4A.jsx";
import "../chunk/5WXHJDCZ.jsx";
// src/color-area/color-area-background.tsx
import { callHandler, mergeRefs } from "@kobalte/utils";
import { combineStyle } from "@solid-primitives/props";
import {
createMemo,
createSignal,
splitProps
} from "solid-js";
// src/color-area/color-area-context.tsx
import { createContext, useContext } 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 <Polymorphic
as="div"
ref={mergeRefs(context.setBackgroundRef, props.ref)}
style={combineStyle(
{
"touch-action": "none",
"forced-color-adjust": "none",
...backgroundStyles()
},
local.style
)}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={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"
};
// src/color-area/color-area-hidden-input-base.tsx
import {
callHandler as callHandler2,
mergeDefaultProps,
visuallyHiddenStyles
} from "@kobalte/utils";
import { combineStyle as combineStyle2 } from "@solid-primitives/props";
import {
createMemo as createMemo2,
splitProps as splitProps2
} from "solid-js";
function ColorAreaHiddenInputBase(props) {
const formControlContext = useFormControlContext();
const context = useColorAreaContext();
const mergedProps = mergeDefaultProps(
{
id: context.generateId("input"),
orientation: "horizontal"
},
props
);
const [local, formControlFieldProps, others] = splitProps2(
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) => {
callHandler2(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 = createMemo2(() => {
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 <input
type="range"
id={fieldProps.id()}
name={(isVertical() ? context.yName() : context.xName()) || formControlContext.name()}
tabIndex={context.state.isDisabled() ? void 0 : -1}
min={isVertical() ? context.state.yMinValue() : context.state.xMinValue()}
max={isVertical() ? context.state.yMaxValue() : context.state.xMaxValue()}
step={isVertical() ? context.state.yStep() : context.state.xStep()}
value={isVertical() ? context.state.yValue() : context.state.xValue()}
required={formControlContext.isRequired()}
disabled={formControlContext.isDisabled()}
readonly={formControlContext.isReadOnly()}
style={combineStyle2({ ...visuallyHiddenStyles }, local.style)}
aria-roledescription={context.translations().twoDimensionalSlider}
aria-valuetext={valueText()}
aria-orientation={local.orientation}
aria-label={ariaLabel()}
aria-labelledby={fieldProps.ariaLabelledBy()}
aria-describedby={fieldProps.ariaDescribedBy()}
aria-invalid={formControlContext.validationState() === "invalid" || void 0}
aria-required={formControlContext.isRequired() || void 0}
aria-disabled={formControlContext.isDisabled() || void 0}
aria-readonly={formControlContext.isReadOnly() || void 0}
data-orientation={local.orientation}
onChange={onChange}
{...formControlContext.dataset()}
{...others}
/>;
}
// src/color-area/color-area-hidden-input-x.tsx
function ColorAreaHiddenInputX(props) {
return <ColorAreaHiddenInputBase {...props} />;
}
// src/color-area/color-area-hidden-input-y.tsx
function ColorAreaHiddenInputY(props) {
return <ColorAreaHiddenInputBase orientation="vertical" {...props} />;
}
// src/color-area/color-area-root.tsx
import {
access,
clamp as clamp2,
createGenerateId,
mergeDefaultProps as mergeDefaultProps3,
mergeRefs as mergeRefs2
} from "@kobalte/utils";
import {
createSignal as createSignal3,
createUniqueId,
splitProps as splitProps3
} from "solid-js";
// src/color-area/color-area.intl.ts
var COLOR_AREA_INTL_TRANSLATIONS = {
colorPicker: "Color picker",
twoDimensionalSlider: "2D slider"
};
// src/color-area/create-color-area-state.ts
import { clamp, mergeDefaultProps as mergeDefaultProps2, snapValueToStep } from "@kobalte/utils";
import { createMemo as createMemo3, createSignal as createSignal2 } from "solid-js";
function createColorAreaState(props) {
const mergedProps = mergeDefaultProps2(
{
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 = createMemo3(() => {
return mergedProps.colorSpace?.() ? value().toFormat(mergedProps.colorSpace()) : value();
});
const channels = createMemo3(() => {
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] = createSignal2(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 = mergeDefaultProps3(
{
id: defaultId,
translations: COLOR_AREA_INTL_TRANSLATIONS,
disabled: false,
defaultValue: parseColor("hsl(0, 100%, 50%)")
},
props
);
const [local, formControlProps, others] = splitProps3(
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] = createSignal3();
const [thumbRef, setThumbRef] = createSignal3();
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 = clamp2(currentPosition.x / width, 0, 1);
const yPercent = clamp2(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 <FormControlContext.Provider value={formControlContext}><ColorAreaContext.Provider value={context}><Polymorphic
as="div"
ref={mergeRefs2((el) => ref = el, local.ref)}
role="group"
id={access(formControlProps.id)}
{...formControlContext.dataset()}
{...others}
/></ColorAreaContext.Provider></FormControlContext.Provider>;
}
// src/color-area/color-area-thumb.tsx
import { callHandler as callHandler3, mergeRefs as mergeRefs3 } from "@kobalte/utils";
import { combineStyle as combineStyle3 } from "@solid-primitives/props";
import { splitProps as splitProps4 } from "solid-js";
function ColorAreaThumb(props) {
const context = useColorAreaContext();
const formControlContext = useFormControlContext();
const [local, others] = splitProps4(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) => {
callHandler3(e, local.onKeyDown);
context.onStepKeyDown(e);
};
let startPosition = { x: 0, y: 0 };
const onPointerDown = (e) => {
callHandler3(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();
callHandler3(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();
callHandler3(e, local.onPointerUp);
const target = e.currentTarget;
if (target.hasPointerCapture(e.pointerId)) {
target.releasePointerCapture(e.pointerId);
context.onDragEnd?.();
}
};
return <Polymorphic
as="span"
ref={mergeRefs3(context.setThumbRef, props.ref)}
role="presentation"
tabIndex={context.state.isDisabled() ? void 0 : 0}
style={combineStyle3(
{
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
)}
aria-label={ariaLabel()}
onKeyDown={onKeyDown}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={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,
FormControlDescription as Description,
FormControlErrorMessage as ErrorMessage,
ColorAreaHiddenInputX as HiddenInputX,
ColorAreaHiddenInputY as HiddenInputY,
FormControlLabel as Label,
ColorAreaRoot as Root,
ColorAreaThumb as Thumb,
useColorAreaContext
};