@tremolo-ui/react 0.7.0 → 0.8.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 +346 -1015
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -221
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +76 -221
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +333 -978
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/DropZone/index.tsx +2 -1
- package/src/components/FileInput/index.tsx +8 -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 +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +19 -16
- package/src/components/NumberInput/InputField.tsx +12 -27
- package/src/components/NumberInput/Stepper.tsx +49 -97
- package/src/components/NumberInput/StepperButton.tsx +3 -4
- package/src/components/NumberInput/context.tsx +2 -1
- package/src/components/NumberInput/index.tsx +40 -71
- package/src/components/Piano/index.tsx +35 -142
- package/src/components/PointsEditor/Container.tsx +15 -26
- package/src/components/PointsEditor/Point.tsx +38 -85
- package/src/components/PointsEditor/context.tsx +5 -56
- package/src/components/PointsEditor/index.tsx +29 -311
- package/src/components/Slider/Marks.tsx +3 -5
- package/src/components/Slider/MarksOption.tsx +7 -18
- package/src/components/Slider/Track.tsx +2 -1
- package/src/components/Slider/index.tsx +29 -28
- package/src/components/XYPad/Area.tsx +27 -49
- package/src/components/XYPad/Thumb.tsx +3 -1
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +33 -46
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useCheckSteps.ts +9 -146
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/index.ts +2 -27
- 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/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,25 @@ 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
|
-
|
|
17
|
+
POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
18
|
+
POINTS_EDITOR_DEFAULT_WHEEL,
|
|
21
19
|
type SelectionBoxRect,
|
|
22
|
-
type XY,
|
|
23
20
|
} from '@tremolo-ui/dom'
|
|
24
|
-
import { clamp, toPrecision } from '@tremolo-ui/functions'
|
|
25
|
-
|
|
26
|
-
import { DEFAULT_DRAG_SENSITIVITY } from '../../input-event'
|
|
27
|
-
import { cssLength } from '../_util/css-length'
|
|
28
21
|
|
|
29
22
|
import { Background } from './Background'
|
|
30
23
|
import { Container } from './Container'
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
24
|
+
import { PointsEditorProvider } from './context'
|
|
25
|
+
import { Point } from './Point'
|
|
33
26
|
import { SelectionBox } from './SelectionBox'
|
|
34
27
|
|
|
35
|
-
import type { CSSVariables } from '../../css-variables'
|
|
36
|
-
|
|
37
28
|
/** One array for every editor with selection turned off, so memos hold still. */
|
|
38
29
|
const EMPTY: readonly string[] = []
|
|
39
30
|
|
|
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
31
|
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
32
|
/**
|
|
99
33
|
* Make the points unchangeable and remove them from the tab order.
|
|
100
34
|
* The parts carry `data-disabled` while it is set.
|
|
@@ -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,12 +154,10 @@ 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
158
|
readonly = false,
|
|
229
|
-
wheel =
|
|
230
|
-
keyboard =
|
|
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,
|
|
@@ -242,12 +172,13 @@ 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
177
|
const { cursor = 'grabbing' } = externalStyles ?? {}
|
|
249
178
|
|
|
250
179
|
// --- selection ---
|
|
180
|
+
// The selection lives here, where React state can hold it; which points a
|
|
181
|
+
// press or a box selects, and how a selection moves, is the core's.
|
|
251
182
|
const controlled = selectionProp !== undefined
|
|
252
183
|
const [ownSelection, setOwnSelection] = useState<string[]>(
|
|
253
184
|
defaultSelection ?? [],
|
|
@@ -256,220 +187,29 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
256
187
|
// point it started on and `data-selected` never turns on.
|
|
257
188
|
const selection = selectable ? (selectionProp ?? ownSelection) : EMPTY
|
|
258
189
|
|
|
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
190
|
const [selectionBox, setSelectionBox] = useState<SelectionBoxRect | null>(
|
|
421
191
|
null,
|
|
422
192
|
)
|
|
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
193
|
|
|
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],
|
|
194
|
+
const [editor] = useState(() =>
|
|
195
|
+
createPointsEditor({ onSelectionBoxChange: setSelectionBox }),
|
|
457
196
|
)
|
|
458
197
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
198
|
+
// Drags read the selection from a native event handler, which runs after
|
|
199
|
+
// the commit, so pushing it from an effect is current by the time it
|
|
200
|
+
// matters. The handler goes the same way: it closes over this render.
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
editor.update({
|
|
203
|
+
selectable,
|
|
204
|
+
selection,
|
|
205
|
+
onSelectionChange: (next) => {
|
|
206
|
+
if (!controlled) setOwnSelection(next)
|
|
207
|
+
onSelectionChange?.(next)
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
})
|
|
462
211
|
|
|
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
|
-
}, [])
|
|
212
|
+
useEffect(() => () => editor.destroy(), [editor])
|
|
473
213
|
|
|
474
214
|
const context = useMemo(
|
|
475
215
|
() => ({
|
|
@@ -482,16 +222,8 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
482
222
|
containerRef,
|
|
483
223
|
selectable,
|
|
484
224
|
selection,
|
|
485
|
-
|
|
486
|
-
isPointElement,
|
|
487
|
-
beginPointDrag,
|
|
488
|
-
movePointDrag,
|
|
489
|
-
nudgeSelection,
|
|
490
|
-
nudgeFocusedPoint,
|
|
225
|
+
editor,
|
|
491
226
|
selectionBox,
|
|
492
|
-
beginSelectionBox,
|
|
493
|
-
moveSelectionBox,
|
|
494
|
-
endSelectionBox,
|
|
495
227
|
}),
|
|
496
228
|
[
|
|
497
229
|
disabled,
|
|
@@ -502,16 +234,8 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
502
234
|
cursor,
|
|
503
235
|
selectable,
|
|
504
236
|
selection,
|
|
505
|
-
|
|
506
|
-
isPointElement,
|
|
507
|
-
beginPointDrag,
|
|
508
|
-
movePointDrag,
|
|
509
|
-
nudgeSelection,
|
|
510
|
-
nudgeFocusedPoint,
|
|
237
|
+
editor,
|
|
511
238
|
selectionBox,
|
|
512
|
-
beginSelectionBox,
|
|
513
|
-
moveSelectionBox,
|
|
514
|
-
endSelectionBox,
|
|
515
239
|
],
|
|
516
240
|
)
|
|
517
241
|
|
|
@@ -524,8 +248,6 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
524
248
|
data-readonly={readonly ? '' : undefined}
|
|
525
249
|
style={
|
|
526
250
|
{
|
|
527
|
-
'--width': cssLength(width),
|
|
528
|
-
'--height': cssLength(height),
|
|
529
251
|
// The layers inside are placed against this box.
|
|
530
252
|
position: 'relative',
|
|
531
253
|
...style,
|
|
@@ -555,8 +277,4 @@ export {
|
|
|
555
277
|
usePointsEditorContext,
|
|
556
278
|
type PointsEditorContextValue,
|
|
557
279
|
} from './context'
|
|
558
|
-
export {
|
|
559
|
-
clampPoint,
|
|
560
|
-
type PointBaseType,
|
|
561
|
-
type PointsEditorPointProps,
|
|
562
|
-
} from './Point'
|
|
280
|
+
export { type PointsEditorPointProps } from './Point'
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import { cssLength } from '
|
|
3
|
+
import { cssLength, type MarksOptions, sliderMarks } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
4
5
|
import { Placement } from '../_util/Placement'
|
|
5
6
|
|
|
6
7
|
import { useSliderContext } from './context'
|
|
7
8
|
import { MarksOption } from './MarksOption'
|
|
8
|
-
import { generateOptionsList, MarksOptions } from './type'
|
|
9
9
|
|
|
10
10
|
import type { CSSVariables } from '../../css-variables'
|
|
11
11
|
|
|
@@ -43,9 +43,7 @@ export function Marks({
|
|
|
43
43
|
const vertical = useSliderContext((s) => s.vertical)
|
|
44
44
|
const reverse = useSliderContext((s) => s.reverse)
|
|
45
45
|
|
|
46
|
-
const optionsList = options
|
|
47
|
-
? generateOptionsList(options, min, max, step)
|
|
48
|
-
: []
|
|
46
|
+
const optionsList = options ? sliderMarks(options, min, max, step) : []
|
|
49
47
|
if (vertical !== reverse) optionsList.reverse()
|
|
50
48
|
|
|
51
49
|
return (
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef, CSSProperties
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { cssLength, valuePercent } from '@tremolo-ui/dom'
|
|
4
4
|
|
|
5
|
-
import { cssLength } from '../_util/css-length'
|
|
6
5
|
import { useCheckPlacement } from '../_util/Placement'
|
|
7
6
|
|
|
8
7
|
import { useSliderContext } from './context'
|
|
@@ -29,8 +28,6 @@ export interface SliderMarksOptionProps {
|
|
|
29
28
|
length?: number | string
|
|
30
29
|
/** Space between the mark and the label. Sets `--gap`. */
|
|
31
30
|
gap?: number | string
|
|
32
|
-
/** Width of the label. Sets `--label-width`. */
|
|
33
|
-
labelWidth?: number | string
|
|
34
31
|
/** Classes for the mark and the label inside the option. */
|
|
35
32
|
classes?: {
|
|
36
33
|
mark?: string
|
|
@@ -43,7 +40,7 @@ export interface SliderMarksOptionProps {
|
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
style?: CSSProperties &
|
|
46
|
-
CSSVariables<'thickness' | 'length' | 'gap' | '
|
|
43
|
+
CSSVariables<'thickness' | 'length' | 'gap' | 'translate'>
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
export function MarksOption({
|
|
@@ -53,7 +50,6 @@ export function MarksOption({
|
|
|
53
50
|
thickness,
|
|
54
51
|
length,
|
|
55
52
|
gap,
|
|
56
|
-
labelWidth,
|
|
57
53
|
classes,
|
|
58
54
|
styles,
|
|
59
55
|
className,
|
|
@@ -69,14 +65,8 @@ export function MarksOption({
|
|
|
69
65
|
const vertical = useSliderContext((s) => s.vertical)
|
|
70
66
|
const reverse = useSliderContext((s) => s.reverse)
|
|
71
67
|
|
|
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
|
-
)
|
|
68
|
+
// The marks have to sit on the same curve the thumb runs along.
|
|
69
|
+
const percent = valuePercent(value, { min, max, scale }, vertical !== reverse)
|
|
80
70
|
|
|
81
71
|
return (
|
|
82
72
|
<div
|
|
@@ -87,7 +77,6 @@ export function MarksOption({
|
|
|
87
77
|
'--thickness': cssLength(thickness),
|
|
88
78
|
'--length': cssLength(length),
|
|
89
79
|
'--gap': cssLength(gap),
|
|
90
|
-
'--label-width': cssLength(labelWidth),
|
|
91
80
|
// The mechanics of the position below, as on the thumb.
|
|
92
81
|
position: 'absolute',
|
|
93
82
|
translate: vertical
|
|
@@ -96,8 +85,8 @@ export function MarksOption({
|
|
|
96
85
|
zIndex: 10,
|
|
97
86
|
...style,
|
|
98
87
|
// Where the mark belongs on the track: the value, not a style.
|
|
99
|
-
left: !vertical ? `${
|
|
100
|
-
top: vertical ? `${
|
|
88
|
+
left: !vertical ? `${percent}%` : undefined,
|
|
89
|
+
top: vertical ? `${percent}%` : undefined,
|
|
101
90
|
} as CSSProperties
|
|
102
91
|
}
|
|
103
92
|
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
|
|
2
2
|
|
|
3
|
+
import { cssLength } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
3
5
|
import { useComposedRefs } from '../../compose-refs'
|
|
4
|
-
import { cssLength } from '../_util/css-length'
|
|
5
6
|
import { Placement } from '../_util/Placement'
|
|
6
7
|
|
|
7
8
|
import { useSliderContext } from './context'
|
|
@@ -11,23 +11,24 @@ import React, {
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
applyDelta,
|
|
14
|
+
arrowKeyDirection,
|
|
14
15
|
type AxisOptions,
|
|
16
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
17
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
18
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
15
19
|
type InputEventOption,
|
|
16
20
|
type ModifierValue,
|
|
17
21
|
selectModifier,
|
|
22
|
+
valuePercent,
|
|
23
|
+
wheelDirection,
|
|
18
24
|
type XY,
|
|
19
25
|
} from '@tremolo-ui/dom'
|
|
20
|
-
import { linearScale, type Scale
|
|
26
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
21
27
|
|
|
22
28
|
import { useComposedRefs } from '../../compose-refs'
|
|
23
29
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
24
30
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
25
31
|
import { useWheel } from '../../hooks/useWheel'
|
|
26
|
-
import {
|
|
27
|
-
DEFAULT_DRAG_SENSITIVITY,
|
|
28
|
-
DEFAULT_KEYBOARD_OPTIONS,
|
|
29
|
-
DEFAULT_WHEEL_OPTIONS,
|
|
30
|
-
} from '../../input-event'
|
|
31
32
|
|
|
32
33
|
import { SliderProvider } from './context'
|
|
33
34
|
import { Marks } from './Marks'
|
|
@@ -216,15 +217,13 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
216
217
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
217
218
|
const inactive = disabled || readonly
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
const rev = toFixed(100 - p)
|
|
221
|
-
// NOTE
|
|
220
|
+
// Measured from the left or the top, as CSS places things:
|
|
222
221
|
// normal -> normal (right)
|
|
223
|
-
// vertical ->
|
|
224
|
-
// reverse ->
|
|
222
|
+
// vertical -> reversed (up)
|
|
223
|
+
// reverse -> reversed (left)
|
|
225
224
|
// vertical & reverse -> normal (down)
|
|
226
225
|
const displayReversed = vertical !== reverse
|
|
227
|
-
const percent =
|
|
226
|
+
const percent = valuePercent(value, { min, max, scale }, displayReversed)
|
|
228
227
|
|
|
229
228
|
// --- internal functions ---
|
|
230
229
|
// The pointer is normalized against the track on both axes; only the one
|
|
@@ -239,14 +238,19 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
239
238
|
|
|
240
239
|
const handleKeyDown = useCallback(
|
|
241
240
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
return
|
|
241
|
+
const direction = arrowKeyDirection(event.key)
|
|
242
|
+
if (direction === null) return
|
|
245
243
|
event.preventDefault()
|
|
246
244
|
if (!keyboard || !onChange || inactive) return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
onChange(
|
|
246
|
+
applyDelta(
|
|
247
|
+
value,
|
|
248
|
+
reverse ? -direction : direction,
|
|
249
|
+
keyboard,
|
|
250
|
+
axis,
|
|
251
|
+
event,
|
|
252
|
+
),
|
|
253
|
+
)
|
|
250
254
|
},
|
|
251
255
|
[keyboard, onChange, inactive, reverse, value, axis],
|
|
252
256
|
)
|
|
@@ -279,16 +283,14 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
279
283
|
|
|
280
284
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
281
285
|
if (!wheel || !onChange || inactive) return
|
|
286
|
+
// A notch the slider does not read — a sideways scroll on a vertical
|
|
287
|
+
// slider — is left to the page rather than swallowed.
|
|
288
|
+
const direction = wheelDirection(event, { horizontal: !vertical })
|
|
289
|
+
if (direction === null) return
|
|
282
290
|
event.preventDefault()
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
} else {
|
|
287
|
-
if (event.deltaY === 0) return
|
|
288
|
-
direction = event.deltaY > 0 ? -1 : 1
|
|
289
|
-
}
|
|
290
|
-
if (reverse) direction *= -1
|
|
291
|
-
onChange(applyDelta(value, direction, wheel, axis, event))
|
|
291
|
+
onChange(
|
|
292
|
+
applyDelta(value, reverse ? -direction : direction, wheel, axis, event),
|
|
293
|
+
)
|
|
292
294
|
}, WHEEL_OPTIONS)
|
|
293
295
|
|
|
294
296
|
// Composed once, so React attaches the refs a single time instead of
|
|
@@ -394,4 +396,3 @@ export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
|
|
|
394
396
|
export { type SliderTrackProps } from './Track'
|
|
395
397
|
export { type SliderMarksProps } from './Marks'
|
|
396
398
|
export { type SliderMarksOptionProps } from './MarksOption'
|
|
397
|
-
export { type MarksOptions } from './type'
|