@allxsmith/bestax-bulma 5.54.1 → 5.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/Command.js +22 -22
- package/dist/esm/components/Command.js.map +1 -1
- package/dist/esm/components/DataTable.js +215 -0
- package/dist/esm/components/DataTable.js.map +1 -0
- package/dist/esm/components/useDataTable.js +482 -0
- package/dist/esm/components/useDataTable.js.map +1 -0
- package/dist/esm/elements/ColorSwatch.js +49 -0
- package/dist/esm/elements/ColorSwatch.js.map +1 -0
- package/dist/esm/elements/RingProgress.js +3 -3
- package/dist/esm/elements/Td.js +4 -2
- package/dist/esm/elements/Td.js.map +1 -1
- package/dist/esm/elements/Th.js +13 -5
- package/dist/esm/elements/Th.js.map +1 -1
- package/dist/esm/elements/Tr.js.map +1 -1
- package/dist/esm/form/ColorPicker.js +540 -0
- package/dist/esm/form/ColorPicker.js.map +1 -0
- package/dist/esm/form/Combobox.js +18 -18
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/Dropzone.js +524 -0
- package/dist/esm/form/Dropzone.js.map +1 -0
- package/dist/esm/form/_colorInternals/colorMath.js +292 -0
- package/dist/esm/form/_colorInternals/colorMath.js.map +1 -0
- package/dist/esm/form/_colorInternals/pickerState.js +160 -0
- package/dist/esm/form/_colorInternals/pickerState.js.map +1 -0
- package/dist/esm/helpers/statusRegion.js +3 -3
- package/dist/esm/helpers/statusRegion.js.map +1 -1
- package/dist/esm/helpers/useFocusTrap.js +2 -2
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +2986 -796
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/DataTable.d.ts +120 -0
- package/dist/types/components/useDataTable.d.ts +445 -0
- package/dist/types/elements/ColorSwatch.d.ts +54 -0
- package/dist/types/elements/Td.d.ts +9 -1
- package/dist/types/elements/Th.d.ts +14 -2
- package/dist/types/elements/Tr.d.ts +10 -1
- package/dist/types/form/ColorPicker.d.ts +266 -0
- package/dist/types/form/Dropzone.d.ts +138 -0
- package/dist/types/form/_colorInternals/colorMath.d.ts +63 -0
- package/dist/types/form/_colorInternals/pickerState.d.ts +62 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types-cjs/components/DataTable.d.ts +120 -0
- package/dist/types-cjs/components/useDataTable.d.ts +445 -0
- package/dist/types-cjs/elements/ColorSwatch.d.ts +54 -0
- package/dist/types-cjs/elements/Td.d.ts +9 -1
- package/dist/types-cjs/elements/Th.d.ts +14 -2
- package/dist/types-cjs/elements/Tr.d.ts +10 -1
- package/dist/types-cjs/form/ColorPicker.d.ts +266 -0
- package/dist/types-cjs/form/Dropzone.d.ts +138 -0
- package/dist/types-cjs/form/_colorInternals/colorMath.d.ts +63 -0
- package/dist/types-cjs/form/_colorInternals/pickerState.d.ts +62 -0
- package/dist/types-cjs/index.d.ts +4 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/components/_datatable.scss +152 -0
- package/src/scss/components/_index.scss +1 -0
- package/src/scss/elements/_colorswatch.scss +100 -0
- package/src/scss/elements/_index.scss +1 -0
- package/src/scss/form/_checkbox.scss +53 -0
- package/src/scss/form/_colorpicker.scss +354 -0
- package/src/scss/form/_dropzone.scss +233 -0
- package/src/scss/form/_index.scss +2 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { forwardRef, useRef, useState, useMemo, useCallback, useEffect } from 'react';
|
|
4
|
+
import { usePrefixedClassNames, classNames, prefixedClassNames } from '../helpers/classNames.js';
|
|
5
|
+
import { useClassPrefix } from '../helpers/Config.js';
|
|
6
|
+
import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
|
|
7
|
+
import { useIsHydrated } from '../helpers/useIsHydrated.js';
|
|
8
|
+
import { useClientLayoutEffect } from '../helpers/useClientLayoutEffect.js';
|
|
9
|
+
import { withSubComponents } from '../helpers/withSubComponents.js';
|
|
10
|
+
import { ColorSwatch } from '../elements/ColorSwatch.js';
|
|
11
|
+
import { parseColor, formatColor, sameColor, prefersDarkInk, hsvaToRgba } from './_colorInternals/colorMath.js';
|
|
12
|
+
import { usePicker, standalone, useColorPicker, ColorPickerContext, keepingChannels, noop } from './_colorInternals/pickerState.js';
|
|
13
|
+
import { Field } from './Field.js';
|
|
14
|
+
import { Control } from './Control.js';
|
|
15
|
+
import { useFieldsetDisabled, useInsideField, useInsideControl, rendersOwnField, rendersOwnControl } from './FormContext.js';
|
|
16
|
+
import { useAutoLabelledBy } from './useAutoLabelId.js';
|
|
17
|
+
|
|
18
|
+
// The theme colors `swatches="bulma"` shows, read off the page.
|
|
19
|
+
const BULMA_SWATCHES = [
|
|
20
|
+
{ color: 'var(--bulma-primary)', label: 'Primary' },
|
|
21
|
+
{ color: 'var(--bulma-link)', label: 'Link' },
|
|
22
|
+
{ color: 'var(--bulma-info)', label: 'Info' },
|
|
23
|
+
{ color: 'var(--bulma-success)', label: 'Success' },
|
|
24
|
+
{ color: 'var(--bulma-warning)', label: 'Warning' },
|
|
25
|
+
{ color: 'var(--bulma-danger)', label: 'Danger' },
|
|
26
|
+
];
|
|
27
|
+
const clamp$2 = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
28
|
+
/** Whether the element is laid out right to left. */
|
|
29
|
+
const isRtl = (element) => !!element && getComputedStyle(element).direction === 'rtl';
|
|
30
|
+
/** Where a pointer is across an element, from 0 to 1 each way. */
|
|
31
|
+
const pointIn = (element, event) => {
|
|
32
|
+
const rect = element.getBoundingClientRect();
|
|
33
|
+
const across = rect.width > 0 ? clamp$2((event.clientX - rect.left) / rect.width, 0, 1) : 0;
|
|
34
|
+
const down = rect.height > 0 ? clamp$2((event.clientY - rect.top) / rect.height, 0, 1) : 0;
|
|
35
|
+
return { x: isRtl(element) ? 1 - across : across, y: down };
|
|
36
|
+
};
|
|
37
|
+
/** The color's red, green and blue, rounded, as a CSS `rgb()` takes them. */
|
|
38
|
+
const rgbChannels = (color) => {
|
|
39
|
+
const { r, g, b } = hsvaToRgba(color);
|
|
40
|
+
return `${Math.round(r)} ${Math.round(g)} ${Math.round(b)}`;
|
|
41
|
+
};
|
|
42
|
+
/** Whether a press should start a drag: any touch or pen, a mouse's main button. */
|
|
43
|
+
const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
|
|
44
|
+
/** Hands a node to a forwarded ref and to one of the part's own. */
|
|
45
|
+
function setRefs(node, own, forwarded) {
|
|
46
|
+
own.current = node;
|
|
47
|
+
if (typeof forwarded === 'function')
|
|
48
|
+
forwarded(node);
|
|
49
|
+
else if (forwarded)
|
|
50
|
+
forwarded.current = node;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The saturation and brightness square, operated by pointer or by keyboard as one 2D slider.
|
|
54
|
+
*
|
|
55
|
+
* @function
|
|
56
|
+
* @param {ColorPickerAreaProps} props - Props for ColorPicker.Area.
|
|
57
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the square's `<div>`.
|
|
58
|
+
* @returns {JSX.Element | null} The square, or nothing outside a ColorPicker.
|
|
59
|
+
*/
|
|
60
|
+
const ColorPickerArea = forwardRef(({ className, style, ...props }, ref) => {
|
|
61
|
+
const picker = usePicker('Area');
|
|
62
|
+
const live = picker ?? standalone;
|
|
63
|
+
const { color, change, end, disabled, labels, touch, percent } = live;
|
|
64
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
65
|
+
const rootRef = useRef(null);
|
|
66
|
+
const xRef = useRef(null);
|
|
67
|
+
const yRef = useRef(null);
|
|
68
|
+
// The axis whose input holds the tab stop: the one focused or moved last.
|
|
69
|
+
const [axis, setAxis] = useState('x');
|
|
70
|
+
// Once a key has moved the square, both inputs are exposed until focus
|
|
71
|
+
// leaves it, so the one that moved can be read.
|
|
72
|
+
const [keyed, setKeyed] = useState(false);
|
|
73
|
+
const draggingRef = useRef(false);
|
|
74
|
+
const areaClass = usePrefixedClassNames('color-area', {
|
|
75
|
+
'is-disabled': disabled,
|
|
76
|
+
});
|
|
77
|
+
const thumbClass = usePrefixedClassNames('color-area-thumb');
|
|
78
|
+
const inputClass = usePrefixedClassNames('color-area-input');
|
|
79
|
+
const s = Math.round(color.s);
|
|
80
|
+
const v = Math.round(color.v);
|
|
81
|
+
const saturationText = `${labels.saturation} ${percent(s)}`;
|
|
82
|
+
const brightnessText = `${labels.brightness} ${percent(v)}`;
|
|
83
|
+
const focusAxis = (which) => (which === 'x' ? xRef : yRef).current?.focus({ preventScroll: true });
|
|
84
|
+
// The axis a key moved to, focused once the render that shows its input
|
|
85
|
+
// is on the page, so it's no longer aria-hidden as it takes focus.
|
|
86
|
+
const focusNextRef = useRef(null);
|
|
87
|
+
useClientLayoutEffect(() => {
|
|
88
|
+
if (!focusNextRef.current)
|
|
89
|
+
return;
|
|
90
|
+
focusAxis(focusNextRef.current);
|
|
91
|
+
focusNextRef.current = null;
|
|
92
|
+
});
|
|
93
|
+
const moveTo = (event) => {
|
|
94
|
+
const { x, y } = pointIn(event.currentTarget, event);
|
|
95
|
+
change({ ...color, s: x * 100, v: (1 - y) * 100 });
|
|
96
|
+
};
|
|
97
|
+
const onPointerDown = (event) => {
|
|
98
|
+
if (disabled || !startsDrag(event))
|
|
99
|
+
return;
|
|
100
|
+
// No text selection, and focus goes where the square puts it.
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
103
|
+
draggingRef.current = true;
|
|
104
|
+
moveTo(event);
|
|
105
|
+
focusAxis(axis);
|
|
106
|
+
};
|
|
107
|
+
const onPointerMove = (event) => {
|
|
108
|
+
if (draggingRef.current)
|
|
109
|
+
moveTo(event);
|
|
110
|
+
};
|
|
111
|
+
const endDrag = (event) => {
|
|
112
|
+
if (!draggingRef.current)
|
|
113
|
+
return;
|
|
114
|
+
draggingRef.current = false;
|
|
115
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
116
|
+
end();
|
|
117
|
+
};
|
|
118
|
+
const onKeyDown = (event) => {
|
|
119
|
+
const by = event.shiftKey ? 10 : 1;
|
|
120
|
+
// The horizontal arrows follow the screen: right to left, saturation
|
|
121
|
+
// grows to the left.
|
|
122
|
+
const right = isRtl(rootRef.current) ? -by : by;
|
|
123
|
+
const moves = {
|
|
124
|
+
ArrowRight: [right, 0],
|
|
125
|
+
ArrowLeft: [-right, 0],
|
|
126
|
+
ArrowUp: [0, by],
|
|
127
|
+
ArrowDown: [0, -by],
|
|
128
|
+
PageUp: [0, 10],
|
|
129
|
+
PageDown: [0, -10],
|
|
130
|
+
Home: [-10, 0],
|
|
131
|
+
End: [10, 0],
|
|
132
|
+
};
|
|
133
|
+
const move = moves[event.key];
|
|
134
|
+
if (!move)
|
|
135
|
+
return;
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
const [dx, dy] = move;
|
|
138
|
+
change({
|
|
139
|
+
...color,
|
|
140
|
+
s: clamp$2(s + dx, 0, 100),
|
|
141
|
+
v: clamp$2(v + dy, 0, 100),
|
|
142
|
+
}, true);
|
|
143
|
+
const moved = dx !== 0 ? 'x' : 'y';
|
|
144
|
+
setAxis(moved);
|
|
145
|
+
setKeyed(true);
|
|
146
|
+
if (moved !== axis)
|
|
147
|
+
focusNextRef.current = moved;
|
|
148
|
+
};
|
|
149
|
+
// A touch screen reader, or a browser's own stepping, sets an input's
|
|
150
|
+
// value without a key the square handled.
|
|
151
|
+
const onInput = (which) => (event) => {
|
|
152
|
+
const value = Number(event.target.value);
|
|
153
|
+
change(which === 'x' ? { ...color, s: value } : { ...color, v: value }, true);
|
|
154
|
+
};
|
|
155
|
+
const onBlur = (event) => {
|
|
156
|
+
const next = event.relatedTarget;
|
|
157
|
+
if (next !== xRef.current && next !== yRef.current)
|
|
158
|
+
setKeyed(false);
|
|
159
|
+
};
|
|
160
|
+
// What both inputs share; each adds its axis, and the handlers.
|
|
161
|
+
const shared = {
|
|
162
|
+
type: 'range',
|
|
163
|
+
className: inputClass,
|
|
164
|
+
min: 0,
|
|
165
|
+
max: 100,
|
|
166
|
+
step: 1,
|
|
167
|
+
disabled,
|
|
168
|
+
'aria-label': labels.area,
|
|
169
|
+
'aria-roledescription': labels.areaRoleDescription,
|
|
170
|
+
};
|
|
171
|
+
// The one not holding the tab stop is hidden until a key moves the
|
|
172
|
+
// square, so a screen reader lists one control; both show on touch.
|
|
173
|
+
const tabIndexOf = (which) => (touch || axis === which ? 0 : -1);
|
|
174
|
+
const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
|
|
175
|
+
if (!picker)
|
|
176
|
+
return null;
|
|
177
|
+
return (jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
|
|
178
|
+
...style,
|
|
179
|
+
'--color-picker-hue': Math.round(color.h),
|
|
180
|
+
'--color-picker-rgb': rgbChannels(color),
|
|
181
|
+
'--color-area-x': `${color.s}%`,
|
|
182
|
+
'--color-area-y': `${100 - color.v}%`,
|
|
183
|
+
}, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, ...rest, children: jsxs("div", { className: thumbClass, role: "presentation", children: [jsx("input", { ref: xRef, ...shared, value: s, "aria-valuetext": `${saturationText}, ${brightnessText}`, "aria-orientation": "horizontal", tabIndex: tabIndexOf('x'), "aria-hidden": hiddenOf('x'), onChange: onInput('x'), onKeyDown: onKeyDown, onFocus: () => setAxis('x'), onBlur: onBlur }), jsx("input", { ref: yRef, ...shared, value: v, "aria-valuetext": `${brightnessText}, ${saturationText}`, "aria-orientation": "vertical", tabIndex: tabIndexOf('y'), "aria-hidden": hiddenOf('y'), onChange: onInput('y'), onKeyDown: onKeyDown, onFocus: () => setAxis('y'), onBlur: onBlur })] }) }));
|
|
184
|
+
});
|
|
185
|
+
ColorPickerArea.displayName = 'ColorPicker.Area';
|
|
186
|
+
/** The hue or the alpha slider: a hidden native range under a drawn track. */
|
|
187
|
+
const ChannelSlider = forwardRef(({ channel, className, style, ...props }, ref) => {
|
|
188
|
+
const picker = usePicker(channel === 'hue' ? 'HueSlider' : 'AlphaSlider');
|
|
189
|
+
const live = picker ?? standalone;
|
|
190
|
+
const { color, change, end, disabled, labels, percent, degrees } = live;
|
|
191
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
192
|
+
const rootRef = useRef(null);
|
|
193
|
+
const draggingRef = useRef(false);
|
|
194
|
+
const isHue = channel === 'hue';
|
|
195
|
+
const max = isHue ? 360 : 100;
|
|
196
|
+
const value = Math.round(isHue ? color.h : color.a * 100);
|
|
197
|
+
const withValue = (next) => isHue ? { ...color, h: next } : { ...color, a: next / 100 };
|
|
198
|
+
const sliderClass = usePrefixedClassNames('color-slider', {
|
|
199
|
+
'is-hue': isHue,
|
|
200
|
+
'is-alpha': !isHue,
|
|
201
|
+
'is-disabled': disabled,
|
|
202
|
+
});
|
|
203
|
+
const trackClass = usePrefixedClassNames('color-slider-track');
|
|
204
|
+
const thumbClass = usePrefixedClassNames('color-slider-thumb');
|
|
205
|
+
const inputClass = usePrefixedClassNames('color-slider-input');
|
|
206
|
+
const moveTo = (event) => {
|
|
207
|
+
const { x } = pointIn(event.currentTarget, event);
|
|
208
|
+
change(withValue(x * max));
|
|
209
|
+
};
|
|
210
|
+
const onPointerDown = (event) => {
|
|
211
|
+
if (disabled || !startsDrag(event))
|
|
212
|
+
return;
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
215
|
+
draggingRef.current = true;
|
|
216
|
+
moveTo(event);
|
|
217
|
+
rootRef.current?.querySelector('input')?.focus({ preventScroll: true });
|
|
218
|
+
};
|
|
219
|
+
const onPointerMove = (event) => {
|
|
220
|
+
if (draggingRef.current)
|
|
221
|
+
moveTo(event);
|
|
222
|
+
};
|
|
223
|
+
const endDrag = (event) => {
|
|
224
|
+
if (!draggingRef.current)
|
|
225
|
+
return;
|
|
226
|
+
draggingRef.current = false;
|
|
227
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
228
|
+
end();
|
|
229
|
+
};
|
|
230
|
+
const onKeyDown = (event) => {
|
|
231
|
+
const by = event.shiftKey ? 10 : 1;
|
|
232
|
+
const right = isRtl(rootRef.current) ? -by : by;
|
|
233
|
+
const steps = {
|
|
234
|
+
ArrowRight: right,
|
|
235
|
+
ArrowLeft: -right,
|
|
236
|
+
ArrowUp: by,
|
|
237
|
+
ArrowDown: -by,
|
|
238
|
+
PageUp: 10,
|
|
239
|
+
PageDown: -10,
|
|
240
|
+
Home: -max,
|
|
241
|
+
End: max,
|
|
242
|
+
};
|
|
243
|
+
const step = steps[event.key];
|
|
244
|
+
if (step === undefined)
|
|
245
|
+
return;
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
change(withValue(clamp$2(value + step, 0, max)), true);
|
|
248
|
+
};
|
|
249
|
+
if (!picker)
|
|
250
|
+
return null;
|
|
251
|
+
return (jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
|
|
252
|
+
...style,
|
|
253
|
+
'--color-picker-hue': Math.round(color.h),
|
|
254
|
+
'--color-picker-rgb': rgbChannels(color),
|
|
255
|
+
'--color-slider-position': `${(value / max) * 100}%`,
|
|
256
|
+
}, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, ...rest, children: [jsx("div", { className: trackClass }), jsx("div", { className: thumbClass, role: "presentation", children: jsx("input", { type: "range", className: inputClass, min: 0, max: max, step: 1, value: value, disabled: disabled,
|
|
257
|
+
// Spelled out, as the square's is, so Safari's Tab reaches it
|
|
258
|
+
// with its default settings, which skip a range input without
|
|
259
|
+
// one.
|
|
260
|
+
tabIndex: 0, "aria-label": isHue ? labels.hue : labels.alpha, "aria-valuetext": isHue ? degrees(value) : percent(value), onChange: event => change(withValue(Number(event.target.value)), true), onKeyDown: onKeyDown }) })] }));
|
|
261
|
+
});
|
|
262
|
+
ChannelSlider.displayName = 'ChannelSlider';
|
|
263
|
+
/**
|
|
264
|
+
* The hue slider, from red round the spectrum and back to red.
|
|
265
|
+
*
|
|
266
|
+
* @function
|
|
267
|
+
* @param {ColorPickerSliderProps} props - Props for ColorPicker.HueSlider.
|
|
268
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the slider's `<div>`.
|
|
269
|
+
* @returns {JSX.Element | null} The slider, or nothing outside a ColorPicker.
|
|
270
|
+
*/
|
|
271
|
+
const ColorPickerHueSlider = forwardRef((props, ref) => jsx(ChannelSlider, { ref: ref, ...props, channel: "hue" }));
|
|
272
|
+
ColorPickerHueSlider.displayName = 'ColorPicker.HueSlider';
|
|
273
|
+
/**
|
|
274
|
+
* The alpha slider, from see-through to solid.
|
|
275
|
+
*
|
|
276
|
+
* @function
|
|
277
|
+
* @param {ColorPickerSliderProps} props - Props for ColorPicker.AlphaSlider.
|
|
278
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the slider's `<div>`.
|
|
279
|
+
* @returns {JSX.Element | null} The slider, or nothing outside a ColorPicker.
|
|
280
|
+
*/
|
|
281
|
+
const ColorPickerAlphaSlider = forwardRef((props, ref) => jsx(ChannelSlider, { ref: ref, ...props, channel: "alpha" }));
|
|
282
|
+
ColorPickerAlphaSlider.displayName = 'ColorPicker.AlphaSlider';
|
|
283
|
+
/** Arrow keys and how far they move along the swatches. */
|
|
284
|
+
const SWATCH_STEPS = {
|
|
285
|
+
ArrowRight: 1,
|
|
286
|
+
ArrowDown: 1,
|
|
287
|
+
ArrowLeft: -1,
|
|
288
|
+
ArrowUp: -1,
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* The swatches, a radio group of preset colors that sets the picker's color.
|
|
292
|
+
*
|
|
293
|
+
* @function
|
|
294
|
+
* @param {ColorPickerSwatchesProps} props - Props for ColorPicker.Swatches.
|
|
295
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the group's `<div>`.
|
|
296
|
+
* @returns {JSX.Element | null} The swatches, or nothing outside a ColorPicker or with none to show.
|
|
297
|
+
*/
|
|
298
|
+
const ColorPickerSwatches = forwardRef(({ className, swatches: ownSwatches, ...props }, ref) => {
|
|
299
|
+
const picker = usePicker('Swatches');
|
|
300
|
+
const live = picker ?? standalone;
|
|
301
|
+
const { color, change, disabled, labels, format, alpha } = live;
|
|
302
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
303
|
+
const classPrefix = useClassPrefix();
|
|
304
|
+
const groupClass = usePrefixedClassNames('color-picker-swatches');
|
|
305
|
+
const rootRef = useRef(null);
|
|
306
|
+
const given = ownSwatches ?? live.swatches;
|
|
307
|
+
const list = useMemo(() => (given === 'bulma' ? BULMA_SWATCHES : (given ?? [])).map(entry => typeof entry === 'string' ? { color: entry } : entry), [given]);
|
|
308
|
+
// A color the picker can't read as written, such as a var(), is read off
|
|
309
|
+
// the page, as the text the swatch's own custom property resolves to.
|
|
310
|
+
const chipsRef = useRef([]);
|
|
311
|
+
const textOf = useCallback((i) => {
|
|
312
|
+
const chip = chipsRef.current[i];
|
|
313
|
+
if (parseColor(list[i].color) || !chip)
|
|
314
|
+
return list[i].color;
|
|
315
|
+
return getComputedStyle(chip)
|
|
316
|
+
.getPropertyValue('--color-swatch-color')
|
|
317
|
+
.trim();
|
|
318
|
+
}, [list]);
|
|
319
|
+
// Those texts, read after every render rather than once: a Theme can
|
|
320
|
+
// change the variables behind them while the picker is open, and the
|
|
321
|
+
// chosen swatch has to follow. A read that changes nothing renders
|
|
322
|
+
// nothing more.
|
|
323
|
+
const [resolved, setResolved] = useState([]);
|
|
324
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose; the updater keeps the state when nothing changed, so it settles
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
const next = list.map((_, i) => textOf(i));
|
|
327
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- a var() color can only be read off the page once its swatch is on it
|
|
328
|
+
setResolved(previous => previous.length === next.length &&
|
|
329
|
+
previous.every((text, i) => text === next[i])
|
|
330
|
+
? previous
|
|
331
|
+
: next);
|
|
332
|
+
});
|
|
333
|
+
const colorOf = (i) => parseColor(list[i].color) ?? parseColor(resolved[i]);
|
|
334
|
+
// The same color, as far as the picker's value can tell: the same 8-bit
|
|
335
|
+
// color, or the same text, since an HSL value is rounded.
|
|
336
|
+
const shownText = formatColor(color, format, alpha);
|
|
337
|
+
const checked = list.findIndex((_, i) => {
|
|
338
|
+
const swatch = colorOf(i);
|
|
339
|
+
if (!swatch)
|
|
340
|
+
return false;
|
|
341
|
+
const solid = alpha ? swatch : { ...swatch, a: 1 };
|
|
342
|
+
return (sameColor(solid, color) || formatColor(solid, format, alpha) === shownText);
|
|
343
|
+
});
|
|
344
|
+
const [focused, setFocused] = useState(null);
|
|
345
|
+
const tabStop = focused ?? (checked >= 0 ? checked : 0);
|
|
346
|
+
// Read afresh, so a theme that changed since the last render counts.
|
|
347
|
+
const pick = (i) => {
|
|
348
|
+
const text = textOf(i);
|
|
349
|
+
const swatch = parseColor(text);
|
|
350
|
+
if (swatch)
|
|
351
|
+
change(keepingChannels(swatch, color, text), true);
|
|
352
|
+
};
|
|
353
|
+
const onKeyDown = (i) => (event) => {
|
|
354
|
+
const buttons = Array.from(rootRef.current.querySelectorAll('[role="radio"]'));
|
|
355
|
+
let next;
|
|
356
|
+
if (event.key === 'Home')
|
|
357
|
+
next = 0;
|
|
358
|
+
else if (event.key === 'End')
|
|
359
|
+
next = buttons.length - 1;
|
|
360
|
+
else if (SWATCH_STEPS[event.key]) {
|
|
361
|
+
// Left and Right follow the screen, so right to left they swap.
|
|
362
|
+
const sideways = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
|
|
363
|
+
const flip = sideways && isRtl(rootRef.current) ? -1 : 1;
|
|
364
|
+
next =
|
|
365
|
+
(i + SWATCH_STEPS[event.key] * flip + buttons.length) % buttons.length;
|
|
366
|
+
}
|
|
367
|
+
if (next === undefined)
|
|
368
|
+
return;
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
buttons[next]?.focus();
|
|
371
|
+
pick(next);
|
|
372
|
+
};
|
|
373
|
+
if (!picker || list.length === 0)
|
|
374
|
+
return null;
|
|
375
|
+
return (jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
|
|
376
|
+
const swatch = colorOf(i);
|
|
377
|
+
const isChecked = i === checked;
|
|
378
|
+
// The check is drawn in black or white with a ring of the other, so
|
|
379
|
+
// it shows on any color. On a see-through swatch the page shows
|
|
380
|
+
// through, so it takes the scheme's text color and background,
|
|
381
|
+
// which follow dark mode, in place of the swatch's own.
|
|
382
|
+
const seeThrough = !!swatch && swatch.a < 1;
|
|
383
|
+
return (jsx("button", { type: "button", role: "radio", "aria-checked": isChecked, "aria-label": entry.label || entry.color, title: entry.label, tabIndex: i === tabStop ? 0 : -1, disabled: disabled, className: prefixedClassNames(classPrefix, 'color-picker-swatch', {
|
|
384
|
+
'is-checked': isChecked,
|
|
385
|
+
'is-see-through': seeThrough,
|
|
386
|
+
'is-dark-ink': !!swatch && !seeThrough && prefersDarkInk(swatch),
|
|
387
|
+
}), onClick: () => pick(i), onKeyDown: onKeyDown(i), onFocus: () => setFocused(i), onBlur: () => setFocused(null), children: jsx(ColorSwatch, { ref: node => {
|
|
388
|
+
chipsRef.current[i] = node;
|
|
389
|
+
}, color: entry.color, "aria-hidden": "true", children: isChecked && (jsx(CheckIcon, { haloClass: prefixedClassNames(classPrefix, 'color-picker-check-halo') })) }) }, `${entry.color}-${i}`));
|
|
390
|
+
}) }));
|
|
391
|
+
});
|
|
392
|
+
ColorPickerSwatches.displayName = 'ColorPicker.Swatches';
|
|
393
|
+
/**
|
|
394
|
+
* The check on the chosen swatch: a ring in `haloClass`, and the check over
|
|
395
|
+
* it in the current color.
|
|
396
|
+
*/
|
|
397
|
+
const CheckIcon = ({ haloClass }) => (jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: ['halo', 'ink'].map(part => (jsx("path", { className: part === 'halo' ? haloClass : undefined, d: "M3.5 8.5l3 3 6-7", fill: "none", stroke: "currentColor", strokeWidth: part === 'halo' ? 4.5 : 2, strokeLinecap: "round", strokeLinejoin: "round" }, part))) }));
|
|
398
|
+
/**
|
|
399
|
+
* The value as text you can type in, beside a preview swatch, with the eye dropper when the picker has `eyeDropper`.
|
|
400
|
+
*
|
|
401
|
+
* @function
|
|
402
|
+
* @param {ColorPickerInputProps} props - Props for ColorPicker.Input.
|
|
403
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the row's `<div>`.
|
|
404
|
+
* @returns {JSX.Element | null} The row, or nothing outside a ColorPicker.
|
|
405
|
+
*/
|
|
406
|
+
const ColorPickerInput = forwardRef(({ className, ...props }, ref) => {
|
|
407
|
+
const picker = usePicker('Input');
|
|
408
|
+
const live = picker ?? standalone;
|
|
409
|
+
const { color, text, change, disabled, labels, eyeDropper } = live;
|
|
410
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
411
|
+
const rowClass = usePrefixedClassNames('color-picker-value');
|
|
412
|
+
const previewClass = usePrefixedClassNames('color-picker-preview');
|
|
413
|
+
// What's been typed and not yet committed, or null while the field
|
|
414
|
+
// shows the value.
|
|
415
|
+
const [draft, setDraft] = useState(null);
|
|
416
|
+
const invalid = draft !== null && !parseColor(draft);
|
|
417
|
+
const inputClass = usePrefixedClassNames('input', {
|
|
418
|
+
'is-small': true,
|
|
419
|
+
'is-danger': invalid,
|
|
420
|
+
});
|
|
421
|
+
const buttonClass = usePrefixedClassNames('button', { 'is-small': true });
|
|
422
|
+
const hydrated = useIsHydrated();
|
|
423
|
+
const EyeDropper = hydrated
|
|
424
|
+
? window.EyeDropper
|
|
425
|
+
: undefined;
|
|
426
|
+
const commit = () => {
|
|
427
|
+
if (draft === null)
|
|
428
|
+
return;
|
|
429
|
+
const parsed = parseColor(draft);
|
|
430
|
+
setDraft(null);
|
|
431
|
+
if (parsed)
|
|
432
|
+
change(keepingChannels(parsed, color, draft), true);
|
|
433
|
+
};
|
|
434
|
+
const onKeyDown = (event) => {
|
|
435
|
+
if (draft === null)
|
|
436
|
+
return;
|
|
437
|
+
if (event.key === 'Enter') {
|
|
438
|
+
// Commits rather than submitting a form; an unreadable value stays
|
|
439
|
+
// to be fixed.
|
|
440
|
+
event.preventDefault();
|
|
441
|
+
if (!invalid)
|
|
442
|
+
commit();
|
|
443
|
+
}
|
|
444
|
+
else if (event.key === 'Escape') {
|
|
445
|
+
// Puts the value back, and keeps the key from closing a popover too.
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
event.stopPropagation();
|
|
448
|
+
setDraft(null);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
// Escape, or a click away, cancels it, which rejects.
|
|
452
|
+
const pickFromScreen = (Dropper) => new Dropper().open().then(({ sRGBHex }) => {
|
|
453
|
+
const parsed = parseColor(sRGBHex);
|
|
454
|
+
// The screen gives a solid color as hex; the alpha stays as it was,
|
|
455
|
+
// and a gray keeps the hue the picker had.
|
|
456
|
+
if (parsed) {
|
|
457
|
+
change(keepingChannels({ ...parsed, a: color.a }, color, sRGBHex), true);
|
|
458
|
+
}
|
|
459
|
+
}, noop);
|
|
460
|
+
if (!picker)
|
|
461
|
+
return null;
|
|
462
|
+
return (jsxs("div", { ref: ref, className: classNames(rowClass, bulmaHelperClasses, className), ...rest, children: [jsx(ColorSwatch, { color: text, className: previewClass, "aria-hidden": "true" }), jsx("input", { type: "text", className: inputClass, value: draft ?? text, disabled: disabled, "aria-label": labels.value, "aria-invalid": invalid || undefined, spellCheck: false, autoComplete: "off", autoCapitalize: "off", onChange: event => setDraft(event.target.value), onKeyDown: onKeyDown, onBlur: commit }), eyeDropper && EyeDropper && (jsx("button", { type: "button", className: buttonClass, "aria-label": labels.eyeDropper, title: labels.eyeDropper, disabled: disabled, onClick: () => pickFromScreen(EyeDropper), children: jsx(EyeDropperIcon, {}) }))] }));
|
|
463
|
+
});
|
|
464
|
+
ColorPickerInput.displayName = 'ColorPicker.Input';
|
|
465
|
+
/** A pipette. */
|
|
466
|
+
const EyeDropperIcon = () => (jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: jsx("path", { d: "M10.5 2.5l3 3M9 4l3 3-6.5 6.5H3v-3z", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" }) }));
|
|
467
|
+
/**
|
|
468
|
+
* The `ColorPicker` component is a panel for choosing a color: a saturation and brightness square, a hue slider, and the value as text, with alpha and swatches when asked for.
|
|
469
|
+
*
|
|
470
|
+
* Every part is operable by keyboard, and the square is two native range
|
|
471
|
+
* inputs, one per axis, with one tab stop. The value is a CSS color string
|
|
472
|
+
* in `format`, HSL by default.
|
|
473
|
+
*
|
|
474
|
+
* @function
|
|
475
|
+
* @param {ColorPickerProps} props - Props for the ColorPicker component.
|
|
476
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the picker's `<div>`.
|
|
477
|
+
* @returns {JSX.Element} The rendered picker.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* <ColorPicker label="Brand color" defaultValue="#1e6b99" swatches="bulma" />
|
|
481
|
+
*/
|
|
482
|
+
const ColorPickerRoot = forwardRef(({ className, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value, defaultValue, onChange, onChangeEnd, format = 'hsl', alpha = false, swatches, eyeDropper = false, disabled: disabledProp, name, form, labels, children, ...props }, ref) => {
|
|
483
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
484
|
+
const insideField = useInsideField();
|
|
485
|
+
const insideControl = useInsideControl();
|
|
486
|
+
const ownField = rendersOwnField('ColorPicker', {
|
|
487
|
+
insideField,
|
|
488
|
+
insideControl,
|
|
489
|
+
label,
|
|
490
|
+
message,
|
|
491
|
+
horizontal,
|
|
492
|
+
fieldClassName,
|
|
493
|
+
});
|
|
494
|
+
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
495
|
+
label,
|
|
496
|
+
labelProps,
|
|
497
|
+
rendersLabel: ownField,
|
|
498
|
+
callerProps: props,
|
|
499
|
+
});
|
|
500
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
501
|
+
const pickerClass = usePrefixedClassNames('color-picker', {
|
|
502
|
+
'is-disabled': disabled,
|
|
503
|
+
});
|
|
504
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
505
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
506
|
+
});
|
|
507
|
+
const context = useColorPicker({
|
|
508
|
+
value,
|
|
509
|
+
defaultValue,
|
|
510
|
+
onChange,
|
|
511
|
+
onChangeEnd,
|
|
512
|
+
format,
|
|
513
|
+
alpha,
|
|
514
|
+
disabled,
|
|
515
|
+
swatches,
|
|
516
|
+
eyeDropper,
|
|
517
|
+
labels,
|
|
518
|
+
});
|
|
519
|
+
const picker = (jsxs("div", { ref: ref, className: classNames(pickerClass, bulmaHelperClasses, className), role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: [jsx(ColorPickerContext.Provider, { value: context, children: children ?? (jsxs(Fragment, { children: [jsx(ColorPickerArea, {}), jsx(ColorPickerHueSlider, {}), alpha && jsx(ColorPickerAlphaSlider, {}), jsx(ColorPickerInput, {}), swatches && jsx(ColorPickerSwatches, {})] })) }), name && (jsx("input", { type: "hidden", name: name, form: form, value: context.text, disabled: disabled }))] }));
|
|
520
|
+
let content = picker;
|
|
521
|
+
if (rendersOwnControl('ColorPicker', { insideControl })) {
|
|
522
|
+
content = jsx(Control, { children: content });
|
|
523
|
+
}
|
|
524
|
+
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
525
|
+
if (ownField) {
|
|
526
|
+
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
527
|
+
}
|
|
528
|
+
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
529
|
+
});
|
|
530
|
+
ColorPickerRoot.displayName = 'ColorPicker';
|
|
531
|
+
const ColorPicker = withSubComponents(ColorPickerRoot, {
|
|
532
|
+
Area: ColorPickerArea,
|
|
533
|
+
HueSlider: ColorPickerHueSlider,
|
|
534
|
+
AlphaSlider: ColorPickerAlphaSlider,
|
|
535
|
+
Swatches: ColorPickerSwatches,
|
|
536
|
+
Input: ColorPickerInput,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
export { ColorPicker, ColorPickerAlphaSlider, ColorPickerArea, ColorPickerHueSlider, ColorPickerInput, ColorPickerSwatches };
|
|
540
|
+
//# sourceMappingURL=ColorPicker.js.map
|