UNPKG

@kobalte/core

Version:

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

777 lines (764 loc) 24.8 kB
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 };