@tremolo-ui/react 0.7.0 → 0.9.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/index.cjs +666 -1171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -315
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +316 -315
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +643 -1123
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/AnimationCanvas/index.tsx +4 -18
- package/src/components/DropZone/index.tsx +2 -1
- package/src/components/FileInput/index.tsx +11 -13
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +21 -20
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +99 -52
- package/src/components/NumberInput/InputField.tsx +20 -33
- package/src/components/NumberInput/Stepper.tsx +76 -100
- package/src/components/NumberInput/StepperButton.tsx +15 -8
- package/src/components/NumberInput/context.tsx +26 -3
- package/src/components/NumberInput/index.tsx +175 -91
- package/src/components/Piano/index.tsx +54 -144
- package/src/components/PointsEditor/Container.tsx +15 -26
- package/src/components/PointsEditor/Point.tsx +94 -110
- package/src/components/PointsEditor/context.tsx +8 -60
- package/src/components/PointsEditor/index.tsx +41 -324
- package/src/components/Slider/Marks.tsx +5 -6
- package/src/components/Slider/MarksOption.tsx +9 -19
- package/src/components/Slider/Thumb.tsx +20 -11
- package/src/components/Slider/Track.tsx +16 -13
- package/src/components/Slider/context.tsx +3 -3
- package/src/components/Slider/index.tsx +110 -72
- package/src/components/XYPad/Area.tsx +27 -49
- package/src/components/XYPad/Thumb.tsx +20 -8
- package/src/components/XYPad/context.tsx +2 -4
- package/src/components/XYPad/index.tsx +102 -83
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useChangeGesture.ts +93 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -146
- package/src/hooks/useAnimationFrame.ts +37 -15
- package/src/hooks/useEventListener.ts +93 -22
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -4
- package/src/index.ts +11 -32
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -46
- package/src/components/_util/css-length.ts +0 -13
- package/src/components/_util/decimal-digits.ts +0 -23
- package/src/components/_util/visually-hidden.ts +0 -25
- package/src/hooks/useInterval.ts +0 -27
- package/src/input-event.ts +0 -32
|
@@ -3,8 +3,6 @@ import {
|
|
|
3
3
|
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
5
|
ReactNode,
|
|
6
|
-
RefObject,
|
|
7
|
-
useCallback,
|
|
8
6
|
useEffect,
|
|
9
7
|
useMemo,
|
|
10
8
|
useRef,
|
|
@@ -12,89 +10,27 @@ import {
|
|
|
12
10
|
} from 'react'
|
|
13
11
|
|
|
14
12
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
createPointsEditor,
|
|
14
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
17
15
|
type InputEventOption,
|
|
18
|
-
type ModifierState,
|
|
19
16
|
type ModifierValue,
|
|
20
|
-
type SelectionBoxInstance,
|
|
21
17
|
type SelectionBoxRect,
|
|
22
|
-
type XY,
|
|
23
18
|
} from '@tremolo-ui/dom'
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
import {
|
|
20
|
+
POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
21
|
+
POINTS_EDITOR_DEFAULT_WHEEL,
|
|
22
|
+
} from '@tremolo-ui/dom/internal'
|
|
28
23
|
|
|
29
24
|
import { Background } from './Background'
|
|
30
25
|
import { Container } from './Container'
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
26
|
+
import { PointsEditorProvider } from './context'
|
|
27
|
+
import { Point } from './Point'
|
|
33
28
|
import { SelectionBox } from './SelectionBox'
|
|
34
29
|
|
|
35
|
-
import type { CSSVariables } from '../../css-variables'
|
|
36
|
-
|
|
37
30
|
/** One array for every editor with selection turned off, so memos hold still. */
|
|
38
31
|
const EMPTY: readonly string[] = []
|
|
39
32
|
|
|
40
|
-
/**
|
|
41
|
-
* How far a point may move before something in the selection leaves its range.
|
|
42
|
-
*
|
|
43
|
-
* Clamping each point on its own would break the shape of the selection: the
|
|
44
|
-
* one that reached the edge would stop while the rest carried on. One amount
|
|
45
|
-
* for all of them means the whole selection stops together.
|
|
46
|
-
*/
|
|
47
|
-
function allowedDelta(
|
|
48
|
-
delta: PointBaseType,
|
|
49
|
-
entries: { start: PointBaseType; registration: PointRegistration }[],
|
|
50
|
-
): PointBaseType {
|
|
51
|
-
let loX = -Infinity
|
|
52
|
-
let hiX = Infinity
|
|
53
|
-
let loY = -Infinity
|
|
54
|
-
let hiY = Infinity
|
|
55
|
-
|
|
56
|
-
for (const { start, registration } of entries) {
|
|
57
|
-
loX = Math.max(loX, (registration.min?.x ?? 0) - start.x)
|
|
58
|
-
hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x)
|
|
59
|
-
loY = Math.max(loY, (registration.min?.y ?? 0) - start.y)
|
|
60
|
-
hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// A point that started outside its own range leaves nothing to move within.
|
|
64
|
-
return {
|
|
65
|
-
x: hiX < loX ? 0 : clamp(delta.x, loX, hiX),
|
|
66
|
-
y: hiY < loY ? 0 : clamp(delta.y, loY, hiY),
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/*
|
|
71
|
-
TODO:
|
|
72
|
-
|
|
73
|
-
- grid
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
78
|
-
* in a hundred steps whatever its pixel size.
|
|
79
|
-
*/
|
|
80
|
-
const DEFAULT_WHEEL: ModifierValue<InputEventOption> = ['normalized', 0.01]
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Shift is the fine-adjustment key everywhere else, so it is bound here too
|
|
84
|
-
* — but only on the keyboard. On the wheel it already means the x axis, and
|
|
85
|
-
* browsers hand shift+wheel over as horizontal scrolling anyway.
|
|
86
|
-
*/
|
|
87
|
-
const DEFAULT_KEYBOARD: ModifierValue<InputEventOption> = {
|
|
88
|
-
default: ['normalized', 0.01],
|
|
89
|
-
shift: ['normalized', 0.001],
|
|
90
|
-
}
|
|
91
|
-
|
|
92
33
|
export interface PointsEditorProps {
|
|
93
|
-
/** Sets `--width`; the size the theme gives it stands when omitted. */
|
|
94
|
-
width?: number | string
|
|
95
|
-
/** Sets `--height`; the size the theme gives it stands when omitted. */
|
|
96
|
-
height?: number | string
|
|
97
|
-
|
|
98
34
|
/**
|
|
99
35
|
* Make the points unchangeable and remove them from the tab order.
|
|
100
36
|
* The parts carry `data-disabled` while it is set.
|
|
@@ -104,17 +40,15 @@ export interface PointsEditorProps {
|
|
|
104
40
|
* Make the points unmovable.
|
|
105
41
|
* The parts carry `data-readonly` while it is set.
|
|
106
42
|
*/
|
|
107
|
-
|
|
43
|
+
readOnly?: boolean
|
|
108
44
|
|
|
109
45
|
/**
|
|
110
46
|
* The cursor to show while dragging a point. It is set on the dragged
|
|
111
47
|
* point, so it stays while the pointer is outside it.
|
|
112
48
|
*
|
|
113
|
-
* @default
|
|
49
|
+
* @default 'grabbing'
|
|
114
50
|
*/
|
|
115
|
-
|
|
116
|
-
cursor?: CSSProperties['cursor']
|
|
117
|
-
}
|
|
51
|
+
dragCursor?: CSSProperties['cursor']
|
|
118
52
|
|
|
119
53
|
/**
|
|
120
54
|
* How much one notch of the wheel moves the focused `Point`. Scrolling
|
|
@@ -212,8 +146,6 @@ export interface PointsEditorProps {
|
|
|
212
146
|
* </PointsEditor.Root>
|
|
213
147
|
*/
|
|
214
148
|
children: ReactNode
|
|
215
|
-
|
|
216
|
-
style?: CSSProperties & CSSVariables<'width' | 'height'>
|
|
217
149
|
}
|
|
218
150
|
|
|
219
151
|
type Props = PointsEditorProps &
|
|
@@ -222,18 +154,16 @@ type Props = PointsEditorProps &
|
|
|
222
154
|
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
223
155
|
(
|
|
224
156
|
{
|
|
225
|
-
width,
|
|
226
|
-
height,
|
|
227
157
|
disabled = false,
|
|
228
|
-
|
|
229
|
-
wheel =
|
|
230
|
-
keyboard =
|
|
158
|
+
readOnly = false,
|
|
159
|
+
wheel = POINTS_EDITOR_DEFAULT_WHEEL,
|
|
160
|
+
keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
231
161
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
232
162
|
selectable = false,
|
|
233
163
|
selection: selectionProp,
|
|
234
164
|
defaultSelection,
|
|
235
165
|
onSelectionChange,
|
|
236
|
-
|
|
166
|
+
dragCursor = 'grabbing',
|
|
237
167
|
style,
|
|
238
168
|
className,
|
|
239
169
|
children,
|
|
@@ -242,12 +172,12 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
242
172
|
forwardedRef,
|
|
243
173
|
) => {
|
|
244
174
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
245
|
-
|
|
246
175
|
// Picked apart so that the memo below depends on values rather than on the
|
|
247
176
|
// object literal a caller writes inline, which is new on every render.
|
|
248
|
-
const { cursor = 'grabbing' } = externalStyles ?? {}
|
|
249
177
|
|
|
250
178
|
// --- selection ---
|
|
179
|
+
// The selection lives here, where React state can hold it; which points a
|
|
180
|
+
// press or a box selects, and how a selection moves, is the core's.
|
|
251
181
|
const controlled = selectionProp !== undefined
|
|
252
182
|
const [ownSelection, setOwnSelection] = useState<string[]>(
|
|
253
183
|
defaultSelection ?? [],
|
|
@@ -256,262 +186,55 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
256
186
|
// point it started on and `data-selected` never turns on.
|
|
257
187
|
const selection = selectable ? (selectionProp ?? ownSelection) : EMPTY
|
|
258
188
|
|
|
259
|
-
// Drags read the selection from a native event handler, which runs after
|
|
260
|
-
// the commit, so a ref is current by the time it matters.
|
|
261
|
-
const selectionRef = useRef(selection)
|
|
262
|
-
const changeHandlerRef = useRef(onSelectionChange)
|
|
263
|
-
useEffect(() => {
|
|
264
|
-
selectionRef.current = selection
|
|
265
|
-
changeHandlerRef.current = onSelectionChange
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
const changeSelection = useCallback(
|
|
269
|
-
(next: string[]) => {
|
|
270
|
-
selectionRef.current = next
|
|
271
|
-
if (!controlled) setOwnSelection(next)
|
|
272
|
-
changeHandlerRef.current?.(next)
|
|
273
|
-
},
|
|
274
|
-
[controlled],
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Every mounted point, by id. A registration is a ref rather than a value:
|
|
279
|
-
* the value inside changes on every frame of a drag, and a registry keyed
|
|
280
|
-
* on it would be rebuilt just as often.
|
|
281
|
-
*/
|
|
282
|
-
const points = useRef(new Map<string, RefObject<PointRegistration>>())
|
|
283
|
-
|
|
284
|
-
const registerPoint = useCallback(
|
|
285
|
-
(id: string, entry: RefObject<PointRegistration>) => {
|
|
286
|
-
points.current.set(id, entry)
|
|
287
|
-
return () => {
|
|
288
|
-
points.current.delete(id)
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
[],
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
/** What the current drag picked up, and where those points started. */
|
|
295
|
-
const dragRef = useRef<{ id: string; start: PointBaseType }[]>([])
|
|
296
|
-
|
|
297
|
-
const snapshot = useCallback((ids: readonly string[]) => {
|
|
298
|
-
return ids.flatMap((id) => {
|
|
299
|
-
const registration = points.current.get(id)?.current
|
|
300
|
-
return registration ? [{ id, start: { ...registration.value } }] : []
|
|
301
|
-
})
|
|
302
|
-
}, [])
|
|
303
|
-
|
|
304
|
-
const applyDeltaTo = useCallback(
|
|
305
|
-
(
|
|
306
|
-
entries: { id: string; start: PointBaseType }[],
|
|
307
|
-
delta: PointBaseType,
|
|
308
|
-
) => {
|
|
309
|
-
const withRegistration = entries.flatMap((entry) => {
|
|
310
|
-
const registration = points.current.get(entry.id)?.current
|
|
311
|
-
return registration ? [{ ...entry, registration }] : []
|
|
312
|
-
})
|
|
313
|
-
const allowed = allowedDelta(delta, withRegistration)
|
|
314
|
-
for (const { start, registration } of withRegistration) {
|
|
315
|
-
if (registration.readonly) continue
|
|
316
|
-
// Rounded here as well as in the pipeline: a move is a subtraction
|
|
317
|
-
// and an addition of its own, and that is enough to put the binary
|
|
318
|
-
// artefact back (0.2 + 0.1 lands on 0.30000000000000004).
|
|
319
|
-
registration.onChange?.({
|
|
320
|
-
x: toPrecision(start.x + allowed.x),
|
|
321
|
-
y: toPrecision(start.y + allowed.y),
|
|
322
|
-
})
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
[],
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
const beginPointDrag = useCallback(
|
|
329
|
-
(id: string, modifiers: ModifierState) => {
|
|
330
|
-
if (!selectable) {
|
|
331
|
-
dragRef.current = snapshot([id])
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
const current = selectionRef.current
|
|
335
|
-
// Ctrl / meta rather than shift: shift is the fine-adjustment key on
|
|
336
|
-
// every control here, and it cannot be both.
|
|
337
|
-
const additive = modifiers.ctrlKey || modifiers.metaKey
|
|
338
|
-
let next: readonly string[]
|
|
339
|
-
if (additive) {
|
|
340
|
-
next = current.includes(id)
|
|
341
|
-
? current.filter((x) => x !== id)
|
|
342
|
-
: [...current, id]
|
|
343
|
-
} else if (current.includes(id)) {
|
|
344
|
-
// Already part of a group: keep it, so the group can be dragged.
|
|
345
|
-
next = current
|
|
346
|
-
} else {
|
|
347
|
-
next = [id]
|
|
348
|
-
}
|
|
349
|
-
changeSelection([...next])
|
|
350
|
-
// A press that took the point out of the selection was a deselect, not
|
|
351
|
-
// the start of a move, so there is nothing to drag.
|
|
352
|
-
dragRef.current = next.includes(id) ? snapshot(next) : []
|
|
353
|
-
},
|
|
354
|
-
[selectable, changeSelection, snapshot],
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
const movePointDrag = useCallback(
|
|
358
|
-
(delta: PointBaseType) => applyDeltaTo(dragRef.current, delta),
|
|
359
|
-
[applyDeltaTo],
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
const nudgeSelection = useCallback(
|
|
363
|
-
(id: string, delta: PointBaseType) => {
|
|
364
|
-
// From wherever the points are now: a key press is not a drag, so
|
|
365
|
-
// there is no earlier position to measure against.
|
|
366
|
-
const ids = selectionRef.current.includes(id)
|
|
367
|
-
? selectionRef.current
|
|
368
|
-
: [id]
|
|
369
|
-
applyDeltaTo(snapshot(ids), delta)
|
|
370
|
-
},
|
|
371
|
-
[applyDeltaTo, snapshot],
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
const isPointElement = useCallback(
|
|
375
|
-
(element: Element | null | undefined) => {
|
|
376
|
-
if (!element) return false
|
|
377
|
-
for (const [, entry] of points.current) {
|
|
378
|
-
if (entry.current.element?.contains(element)) return true
|
|
379
|
-
}
|
|
380
|
-
return false
|
|
381
|
-
},
|
|
382
|
-
[],
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
const nudgeFocusedPoint = useCallback(
|
|
386
|
-
(axis: 'x' | 'y', direction: number, modifiers: ModifierState) => {
|
|
387
|
-
const container = containerRef.current
|
|
388
|
-
const active = container?.ownerDocument.activeElement
|
|
389
|
-
if (!container || !active || !container.contains(active)) return false
|
|
390
|
-
for (const [id, entry] of points.current) {
|
|
391
|
-
const { element, wheel, readonly, onChange, value } = entry.current
|
|
392
|
-
// A point answers only for the focus inside its own inputs, so both
|
|
393
|
-
// axes stay part of the same interaction.
|
|
394
|
-
if (!element?.contains(active)) continue
|
|
395
|
-
if (!wheel || readonly || !onChange) return false
|
|
396
|
-
const next = applyDelta(
|
|
397
|
-
value[axis],
|
|
398
|
-
direction,
|
|
399
|
-
wheel,
|
|
400
|
-
AXIS,
|
|
401
|
-
modifiers,
|
|
402
|
-
)
|
|
403
|
-
// As a move, so that the rest of the selection comes along and the
|
|
404
|
-
// whole group stops together at the edge.
|
|
405
|
-
nudgeSelection(id, {
|
|
406
|
-
x: axis === 'x' ? next - value.x : 0,
|
|
407
|
-
y: axis === 'y' ? next - value.y : 0,
|
|
408
|
-
})
|
|
409
|
-
return true
|
|
410
|
-
}
|
|
411
|
-
return false
|
|
412
|
-
},
|
|
413
|
-
[nudgeSelection],
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
// --- selection box ---
|
|
417
|
-
// The box itself lives in the core: which items a rectangle covers, and
|
|
418
|
-
// what a press adds to or replaces, are not React's to decide. What is
|
|
419
|
-
// left here is the registry it reads and the state the box is drawn from.
|
|
420
189
|
const [selectionBox, setSelectionBox] = useState<SelectionBoxRect | null>(
|
|
421
190
|
null,
|
|
422
191
|
)
|
|
423
|
-
const selectionBoxRef = useRef<SelectionBoxInstance<string> | null>(null)
|
|
424
|
-
selectionBoxRef.current ??= createSelectionBox<string>({
|
|
425
|
-
*items(): Generator<readonly [string, XY<number>]> {
|
|
426
|
-
for (const [id, entry] of points.current) {
|
|
427
|
-
const { x, y } = entry.current.value
|
|
428
|
-
yield [id, [x, y]]
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
onBoxChange: setSelectionBox,
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
// The selection is reported to whoever owns it, and that handler is a new
|
|
435
|
-
// function whenever the caller's is, so it is handed over rather than
|
|
436
|
-
// closed over.
|
|
437
|
-
useEffect(() => {
|
|
438
|
-
selectionBoxRef.current?.update({ onSelectionChange: changeSelection })
|
|
439
|
-
}, [changeSelection])
|
|
440
|
-
|
|
441
|
-
useEffect(() => {
|
|
442
|
-
const box = selectionBoxRef.current
|
|
443
|
-
return () => box?.destroy()
|
|
444
|
-
}, [])
|
|
445
192
|
|
|
446
|
-
const
|
|
447
|
-
(
|
|
448
|
-
if (!selectable) return
|
|
449
|
-
selectionBoxRef.current?.begin([at.x, at.y], {
|
|
450
|
-
// Ctrl / meta rather than shift: shift is the fine-adjustment key on
|
|
451
|
-
// every control here, and it cannot be both.
|
|
452
|
-
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
453
|
-
selection: selectionRef.current,
|
|
454
|
-
})
|
|
455
|
-
},
|
|
456
|
-
[selectable],
|
|
193
|
+
const [editor] = useState(() =>
|
|
194
|
+
createPointsEditor({ onSelectionBoxChange: setSelectionBox }),
|
|
457
195
|
)
|
|
458
196
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
197
|
+
// Drags read the selection from a native event handler, which runs after
|
|
198
|
+
// the commit, so pushing it from an effect is current by the time it
|
|
199
|
+
// matters. The handler goes the same way: it closes over this render.
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
editor.update({
|
|
202
|
+
selectable,
|
|
203
|
+
selection,
|
|
204
|
+
onSelectionChange: (next) => {
|
|
205
|
+
if (!controlled) setOwnSelection(next)
|
|
206
|
+
onSelectionChange?.(next)
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
})
|
|
462
210
|
|
|
463
|
-
|
|
464
|
-
if (!selectionBoxRef.current?.end()) return
|
|
465
|
-
// A selection box is drawn on the container, which is not a control and
|
|
466
|
-
// cannot hold focus, so the press that started it left the focus on
|
|
467
|
-
// nothing. The arrow keys and the wheel reach a point only through the
|
|
468
|
-
// focus, so it is handed to one of the points the box selected —
|
|
469
|
-
// whichever point takes it moves the whole selection.
|
|
470
|
-
const [first] = selectionRef.current
|
|
471
|
-
if (first) points.current.get(first)?.current.element?.focus()
|
|
472
|
-
}, [])
|
|
211
|
+
useEffect(() => () => editor.destroy(), [editor])
|
|
473
212
|
|
|
474
213
|
const context = useMemo(
|
|
475
214
|
() => ({
|
|
476
215
|
disabled,
|
|
477
|
-
|
|
216
|
+
readOnly,
|
|
478
217
|
wheel,
|
|
479
218
|
keyboard,
|
|
480
219
|
dragSensitivity,
|
|
481
|
-
|
|
220
|
+
dragCursor,
|
|
482
221
|
containerRef,
|
|
483
222
|
selectable,
|
|
484
223
|
selection,
|
|
485
|
-
|
|
486
|
-
isPointElement,
|
|
487
|
-
beginPointDrag,
|
|
488
|
-
movePointDrag,
|
|
489
|
-
nudgeSelection,
|
|
490
|
-
nudgeFocusedPoint,
|
|
224
|
+
editor,
|
|
491
225
|
selectionBox,
|
|
492
|
-
beginSelectionBox,
|
|
493
|
-
moveSelectionBox,
|
|
494
|
-
endSelectionBox,
|
|
495
226
|
}),
|
|
496
227
|
[
|
|
497
228
|
disabled,
|
|
498
|
-
|
|
229
|
+
readOnly,
|
|
499
230
|
wheel,
|
|
500
231
|
keyboard,
|
|
501
232
|
dragSensitivity,
|
|
502
|
-
|
|
233
|
+
dragCursor,
|
|
503
234
|
selectable,
|
|
504
235
|
selection,
|
|
505
|
-
|
|
506
|
-
isPointElement,
|
|
507
|
-
beginPointDrag,
|
|
508
|
-
movePointDrag,
|
|
509
|
-
nudgeSelection,
|
|
510
|
-
nudgeFocusedPoint,
|
|
236
|
+
editor,
|
|
511
237
|
selectionBox,
|
|
512
|
-
beginSelectionBox,
|
|
513
|
-
moveSelectionBox,
|
|
514
|
-
endSelectionBox,
|
|
515
238
|
],
|
|
516
239
|
)
|
|
517
240
|
|
|
@@ -521,11 +244,9 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
521
244
|
ref={forwardedRef}
|
|
522
245
|
className={className}
|
|
523
246
|
data-disabled={disabled ? '' : undefined}
|
|
524
|
-
data-readonly={
|
|
247
|
+
data-readonly={readOnly ? '' : undefined}
|
|
525
248
|
style={
|
|
526
249
|
{
|
|
527
|
-
'--width': cssLength(width),
|
|
528
|
-
'--height': cssLength(height),
|
|
529
250
|
// The layers inside are placed against this box.
|
|
530
251
|
position: 'relative',
|
|
531
252
|
...style,
|
|
@@ -555,8 +276,4 @@ export {
|
|
|
555
276
|
usePointsEditorContext,
|
|
556
277
|
type PointsEditorContextValue,
|
|
557
278
|
} from './context'
|
|
558
|
-
export {
|
|
559
|
-
clampPoint,
|
|
560
|
-
type PointBaseType,
|
|
561
|
-
type PointsEditorPointProps,
|
|
562
|
-
} from './Point'
|
|
279
|
+
export { type PointsEditorPointProps } from './Point'
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type MarksOptions } from '@tremolo-ui/dom'
|
|
4
|
+
import { cssLength, sliderMarks } from '@tremolo-ui/dom/internal'
|
|
5
|
+
|
|
4
6
|
import { Placement } from '../_util/Placement'
|
|
5
7
|
|
|
6
8
|
import { useSliderContext } from './context'
|
|
7
9
|
import { MarksOption } from './MarksOption'
|
|
8
|
-
import { generateOptionsList, MarksOptions } from './type'
|
|
9
10
|
|
|
10
11
|
import type { CSSVariables } from '../../css-variables'
|
|
11
12
|
|
|
@@ -40,12 +41,10 @@ export function Marks({
|
|
|
40
41
|
const min = useSliderContext((s) => s.min)
|
|
41
42
|
const max = useSliderContext((s) => s.max)
|
|
42
43
|
const step = useSliderContext((s) => s.step)
|
|
43
|
-
const vertical = useSliderContext((s) => s.vertical)
|
|
44
|
+
const vertical = useSliderContext((s) => s.orientation === 'vertical')
|
|
44
45
|
const reverse = useSliderContext((s) => s.reverse)
|
|
45
46
|
|
|
46
|
-
const optionsList = options
|
|
47
|
-
? generateOptionsList(options, min, max, step)
|
|
48
|
-
: []
|
|
47
|
+
const optionsList = options ? sliderMarks(options, min, max, step) : []
|
|
49
48
|
if (vertical !== reverse) optionsList.reverse()
|
|
50
49
|
|
|
51
50
|
return (
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef, CSSProperties
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { valuePercent } from '@tremolo-ui/dom'
|
|
4
|
+
import { cssLength } from '@tremolo-ui/dom/internal'
|
|
4
5
|
|
|
5
|
-
import { cssLength } from '../_util/css-length'
|
|
6
6
|
import { useCheckPlacement } from '../_util/Placement'
|
|
7
7
|
|
|
8
8
|
import { useSliderContext } from './context'
|
|
@@ -29,8 +29,6 @@ export interface SliderMarksOptionProps {
|
|
|
29
29
|
length?: number | string
|
|
30
30
|
/** Space between the mark and the label. Sets `--gap`. */
|
|
31
31
|
gap?: number | string
|
|
32
|
-
/** Width of the label. Sets `--label-width`. */
|
|
33
|
-
labelWidth?: number | string
|
|
34
32
|
/** Classes for the mark and the label inside the option. */
|
|
35
33
|
classes?: {
|
|
36
34
|
mark?: string
|
|
@@ -43,7 +41,7 @@ export interface SliderMarksOptionProps {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
style?: CSSProperties &
|
|
46
|
-
CSSVariables<'thickness' | 'length' | 'gap' | '
|
|
44
|
+
CSSVariables<'thickness' | 'length' | 'gap' | 'translate'>
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
export function MarksOption({
|
|
@@ -53,7 +51,6 @@ export function MarksOption({
|
|
|
53
51
|
thickness,
|
|
54
52
|
length,
|
|
55
53
|
gap,
|
|
56
|
-
labelWidth,
|
|
57
54
|
classes,
|
|
58
55
|
styles,
|
|
59
56
|
className,
|
|
@@ -66,17 +63,11 @@ export function MarksOption({
|
|
|
66
63
|
const min = useSliderContext((s) => s.min)
|
|
67
64
|
const max = useSliderContext((s) => s.max)
|
|
68
65
|
const scale = useSliderContext((s) => s.scale)
|
|
69
|
-
const vertical = useSliderContext((s) => s.vertical)
|
|
66
|
+
const vertical = useSliderContext((s) => s.orientation === 'vertical')
|
|
70
67
|
const reverse = useSliderContext((s) => s.reverse)
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// The marks have to sit on the same curve the thumb runs along.
|
|
75
|
-
const percent = scale.normalize(value, min, max) * 100
|
|
76
|
-
return toFixed(vertical !== reverse ? 100 - percent : percent)
|
|
77
|
-
},
|
|
78
|
-
[vertical, reverse, max, min, scale],
|
|
79
|
-
)
|
|
69
|
+
// The marks have to sit on the same curve the thumb runs along.
|
|
70
|
+
const percent = valuePercent(value, { min, max, scale }, vertical !== reverse)
|
|
80
71
|
|
|
81
72
|
return (
|
|
82
73
|
<div
|
|
@@ -87,7 +78,6 @@ export function MarksOption({
|
|
|
87
78
|
'--thickness': cssLength(thickness),
|
|
88
79
|
'--length': cssLength(length),
|
|
89
80
|
'--gap': cssLength(gap),
|
|
90
|
-
'--label-width': cssLength(labelWidth),
|
|
91
81
|
// The mechanics of the position below, as on the thumb.
|
|
92
82
|
position: 'absolute',
|
|
93
83
|
translate: vertical
|
|
@@ -96,8 +86,8 @@ export function MarksOption({
|
|
|
96
86
|
zIndex: 10,
|
|
97
87
|
...style,
|
|
98
88
|
// Where the mark belongs on the track: the value, not a style.
|
|
99
|
-
left: !vertical ? `${
|
|
100
|
-
top: vertical ? `${
|
|
89
|
+
left: !vertical ? `${percent}%` : undefined,
|
|
90
|
+
top: vertical ? `${percent}%` : undefined,
|
|
101
91
|
} as CSSProperties
|
|
102
92
|
}
|
|
103
93
|
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
3
4
|
forwardRef,
|
|
5
|
+
Ref,
|
|
4
6
|
useImperativeHandle,
|
|
5
7
|
useRef,
|
|
6
|
-
CSSProperties,
|
|
7
8
|
} from 'react'
|
|
8
9
|
|
|
9
10
|
import { useCheckPlacement } from '../_util/Placement'
|
|
@@ -22,6 +23,12 @@ export interface SliderThumbProps {
|
|
|
22
23
|
color?: string
|
|
23
24
|
|
|
24
25
|
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Receives `focus` and `blur` for the range input inside, which do nothing
|
|
29
|
+
* while the slider is disabled. `ref` reaches the thumb element itself.
|
|
30
|
+
*/
|
|
31
|
+
actionsRef?: Ref<SliderThumbMethods>
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface SliderThumbMethods {
|
|
@@ -32,7 +39,7 @@ export interface SliderThumbMethods {
|
|
|
32
39
|
type Props = SliderThumbProps &
|
|
33
40
|
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderThumbProps>
|
|
34
41
|
|
|
35
|
-
export const Thumb = /* @__PURE__ */ forwardRef<
|
|
42
|
+
export const Thumb = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
36
43
|
function Thumb(
|
|
37
44
|
{
|
|
38
45
|
color,
|
|
@@ -48,6 +55,7 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
48
55
|
'aria-labelledby': ariaLabelledby,
|
|
49
56
|
'aria-describedby': ariaDescribedby,
|
|
50
57
|
'aria-valuetext': ariaValuetext,
|
|
58
|
+
actionsRef,
|
|
51
59
|
...props
|
|
52
60
|
},
|
|
53
61
|
forwardedRef,
|
|
@@ -58,9 +66,9 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
58
66
|
min,
|
|
59
67
|
max,
|
|
60
68
|
step,
|
|
61
|
-
|
|
69
|
+
orientation,
|
|
62
70
|
disabled,
|
|
63
|
-
|
|
71
|
+
readOnly,
|
|
64
72
|
onChange,
|
|
65
73
|
percent,
|
|
66
74
|
thumbRef,
|
|
@@ -78,15 +86,16 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
78
86
|
},
|
|
79
87
|
})
|
|
80
88
|
|
|
81
|
-
useImperativeHandle(
|
|
89
|
+
useImperativeHandle(actionsRef, methods, [disabled])
|
|
82
90
|
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
83
91
|
useImperativeHandle(thumbRef, methods, [disabled])
|
|
84
92
|
|
|
85
93
|
return (
|
|
86
94
|
<div
|
|
95
|
+
ref={forwardedRef}
|
|
87
96
|
className={className}
|
|
88
97
|
data-disabled={disabled ? '' : undefined}
|
|
89
|
-
data-readonly={
|
|
98
|
+
data-readonly={readOnly ? '' : undefined}
|
|
90
99
|
{...props}
|
|
91
100
|
style={{
|
|
92
101
|
...{ '--color': color },
|
|
@@ -101,8 +110,8 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
101
110
|
// Where the thumb sits is the component's decision, not a style: a
|
|
102
111
|
// `left` from the caller would take it off the track, so it is
|
|
103
112
|
// written after theirs.
|
|
104
|
-
top: vertical ? `${percent}%` : '50%',
|
|
105
|
-
left:
|
|
113
|
+
top: orientation === 'vertical' ? `${percent}%` : '50%',
|
|
114
|
+
left: orientation === 'vertical' ? '50%' : `${percent}%`,
|
|
106
115
|
}}
|
|
107
116
|
>
|
|
108
117
|
<VisuallyHiddenRangeInput
|
|
@@ -112,14 +121,14 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
112
121
|
max={max}
|
|
113
122
|
step={step}
|
|
114
123
|
disabled={disabled}
|
|
115
|
-
aria-readonly={
|
|
116
|
-
aria-orientation={
|
|
124
|
+
aria-readonly={readOnly}
|
|
125
|
+
aria-orientation={orientation}
|
|
117
126
|
aria-label={ariaLabel}
|
|
118
127
|
aria-labelledby={ariaLabelledby}
|
|
119
128
|
aria-describedby={ariaDescribedby}
|
|
120
129
|
aria-valuetext={ariaValuetext}
|
|
121
130
|
onChange={(event) => {
|
|
122
|
-
if (
|
|
131
|
+
if (readOnly) {
|
|
123
132
|
event.currentTarget.value = String(value)
|
|
124
133
|
return
|
|
125
134
|
}
|