@tremolo-ui/react 0.8.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 +416 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +256 -110
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +256 -110
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +368 -203
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/AnimationCanvas/index.tsx +4 -18
- package/src/components/FileInput/index.tsx +4 -1
- package/src/components/Knob/Thumb.tsx +19 -19
- package/src/components/Knob/index.tsx +85 -41
- package/src/components/NumberInput/InputField.tsx +9 -7
- package/src/components/NumberInput/Stepper.tsx +30 -6
- package/src/components/NumberInput/StepperButton.tsx +12 -4
- package/src/components/NumberInput/context.tsx +24 -2
- package/src/components/NumberInput/index.tsx +142 -27
- package/src/components/Piano/index.tsx +24 -7
- package/src/components/PointsEditor/Container.tsx +1 -1
- package/src/components/PointsEditor/Point.tsx +61 -30
- package/src/components/PointsEditor/context.tsx +3 -4
- package/src/components/PointsEditor/index.tsx +14 -15
- package/src/components/Slider/Marks.tsx +3 -2
- package/src/components/Slider/MarksOption.tsx +3 -2
- package/src/components/Slider/Thumb.tsx +20 -11
- package/src/components/Slider/Track.tsx +15 -13
- package/src/components/Slider/context.tsx +3 -3
- package/src/components/Slider/index.tsx +85 -48
- package/src/components/XYPad/Thumb.tsx +18 -8
- package/src/components/XYPad/context.tsx +1 -1
- package/src/components/XYPad/index.tsx +74 -42
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useChangeGesture.ts +93 -0
- package/src/hooks/_internal/useCheckSteps.ts +1 -1
- package/src/hooks/useAnimationFrame.ts +37 -15
- package/src/hooks/useEventListener.ts +93 -22
- package/src/hooks/useWheel.ts +4 -4
- package/src/index.ts +9 -5
- package/src/hooks/useInterval.ts +0 -27
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, RefObject, useContext } from 'react'
|
|
1
|
+
import { createContext, CSSProperties, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type InputEventOption,
|
|
@@ -22,7 +22,7 @@ export type NumberInputContextValue = {
|
|
|
22
22
|
scale: Scale
|
|
23
23
|
|
|
24
24
|
disabled: boolean
|
|
25
|
-
|
|
25
|
+
readOnly: boolean
|
|
26
26
|
clampValue: boolean
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -40,6 +40,8 @@ export type NumberInputContextValue = {
|
|
|
40
40
|
dragSensitivity: ModifierValue<number>
|
|
41
41
|
/** Whether a `Stepper` drag hides the pointer and reads its movement. */
|
|
42
42
|
pointerLock: boolean
|
|
43
|
+
/** The cursor shown while a `Stepper` is dragged. */
|
|
44
|
+
dragCursor: CSSProperties['cursor']
|
|
43
45
|
|
|
44
46
|
/** What `InputField` selects when it takes focus. */
|
|
45
47
|
selectOnFocus: 'all' | 'number' | 'none'
|
|
@@ -101,6 +103,26 @@ export function useNumberInputContext<T>(
|
|
|
101
103
|
return selector ? selector(context) : context
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The change gesture of `Root`, for the parts that change the value. Not on
|
|
108
|
+
* the public context: it is how the parts report, not something to read.
|
|
109
|
+
*/
|
|
110
|
+
export type NumberInputGestureValue = {
|
|
111
|
+
/** A press on a stepper, held until the pointer is released anywhere. */
|
|
112
|
+
press: () => void
|
|
113
|
+
/** An arrow key in `InputField`. */
|
|
114
|
+
key: () => void
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const NumberInputGestureContext =
|
|
118
|
+
/* @__PURE__ */ createContext<NumberInputGestureValue | null>(null)
|
|
119
|
+
|
|
120
|
+
export const NumberInputGestureProvider = NumberInputGestureContext.Provider
|
|
121
|
+
|
|
122
|
+
export function useNumberInputGesture(): NumberInputGestureValue | null {
|
|
123
|
+
return useContext(NumberInputGestureContext)
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
/** Set by `Stepper` once a drag has actually moved, so the steppers stand down. */
|
|
105
127
|
export type StepperContextValue = {
|
|
106
128
|
/** Whether the drag in progress has moved the value. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
3
4
|
forwardRef,
|
|
4
5
|
ReactNode,
|
|
6
|
+
Ref,
|
|
5
7
|
useCallback,
|
|
6
8
|
useImperativeHandle,
|
|
7
9
|
useMemo,
|
|
@@ -10,25 +12,30 @@ import {
|
|
|
10
12
|
} from 'react'
|
|
11
13
|
|
|
12
14
|
import {
|
|
15
|
+
type ChangeSource,
|
|
13
16
|
DEFAULT_DRAG_SENSITIVITY,
|
|
14
17
|
DEFAULT_KEYBOARD_OPTIONS,
|
|
15
18
|
DEFAULT_WHEEL_OPTIONS,
|
|
16
|
-
commitNumberInputText,
|
|
17
19
|
InputEventOption,
|
|
18
20
|
ModifierState,
|
|
19
21
|
type ModifierValue,
|
|
22
|
+
} from '@tremolo-ui/dom'
|
|
23
|
+
import {
|
|
24
|
+
commitNumberInputText,
|
|
20
25
|
numberInputBounds,
|
|
21
26
|
numberInputRanges,
|
|
22
27
|
nudgeNumberInput,
|
|
23
28
|
parseNumberText,
|
|
24
29
|
wheelDirection,
|
|
25
|
-
} from '@tremolo-ui/dom'
|
|
30
|
+
} from '@tremolo-ui/dom/internal'
|
|
26
31
|
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
27
32
|
|
|
33
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
34
|
+
import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
|
|
28
35
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
29
36
|
import { useWheel } from '../../hooks/useWheel'
|
|
30
37
|
|
|
31
|
-
import { NumberInputProvider } from './context'
|
|
38
|
+
import { NumberInputGestureProvider, NumberInputProvider } from './context'
|
|
32
39
|
import { DecrementStepper } from './DecrementStepper'
|
|
33
40
|
import { IncrementStepper } from './IncrementStepper'
|
|
34
41
|
import { InputField } from './InputField'
|
|
@@ -162,6 +169,13 @@ export interface NumberInputProps {
|
|
|
162
169
|
* @default false
|
|
163
170
|
*/
|
|
164
171
|
pointerLock?: boolean
|
|
172
|
+
/**
|
|
173
|
+
* The cursor to show while dragging a `Stepper`. It is set on the stepper
|
|
174
|
+
* being dragged, so it stays while the pointer is outside it.
|
|
175
|
+
*
|
|
176
|
+
* @default 'ns-resize'
|
|
177
|
+
*/
|
|
178
|
+
dragCursor?: CSSProperties['cursor']
|
|
165
179
|
|
|
166
180
|
/**
|
|
167
181
|
* Select the text when `InputField` takes focus: `'all'` selects all of it,
|
|
@@ -219,7 +233,7 @@ export interface NumberInputProps {
|
|
|
219
233
|
* Make the value unchangeable.
|
|
220
234
|
* The parts carry `data-readonly` while it is set.
|
|
221
235
|
*/
|
|
222
|
-
|
|
236
|
+
readOnly?: boolean
|
|
223
237
|
|
|
224
238
|
/**
|
|
225
239
|
* Called with the new value. While the user types, it is called for every
|
|
@@ -227,6 +241,27 @@ export interface NumberInputProps {
|
|
|
227
241
|
* again if clamping changes the value.
|
|
228
242
|
*/
|
|
229
243
|
onChange?: (value: number) => void
|
|
244
|
+
/**
|
|
245
|
+
* Called when a change of the value starts — a press on a stepper, the
|
|
246
|
+
* first wheel notch, arrow key or typed character — with the value before
|
|
247
|
+
* it and what it is made with. A host recording automation can treat the
|
|
248
|
+
* input as touched from here until `onChangeEnd`.
|
|
249
|
+
*/
|
|
250
|
+
onChangeStart?: (value: number, source: ChangeSource) => void
|
|
251
|
+
/**
|
|
252
|
+
* Called when the change ends, with the value it ended on: when the stepper
|
|
253
|
+
* is released, when typed text is committed, or `changeEndDelay` after the
|
|
254
|
+
* last wheel notch or arrow key.
|
|
255
|
+
*/
|
|
256
|
+
onChangeEnd?: (value: number, source: ChangeSource) => void
|
|
257
|
+
/**
|
|
258
|
+
* How long after the last wheel notch, arrow key or typed character the
|
|
259
|
+
* change counts as over, in milliseconds. None of them has an event that
|
|
260
|
+
* says it is done.
|
|
261
|
+
*
|
|
262
|
+
* @default 500
|
|
263
|
+
*/
|
|
264
|
+
changeEndDelay?: number
|
|
230
265
|
|
|
231
266
|
/**
|
|
232
267
|
* The input renders exactly what you compose here; there is no default
|
|
@@ -242,6 +277,13 @@ export interface NumberInputProps {
|
|
|
242
277
|
* </NumberInput.Root>
|
|
243
278
|
*/
|
|
244
279
|
children: ReactNode
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Receives `focus` and `blur`, which act on the input of `InputField` — the
|
|
283
|
+
* element that takes the focus — and do nothing while
|
|
284
|
+
* the input is disabled. `ref` reaches the root element itself.
|
|
285
|
+
*/
|
|
286
|
+
actionsRef?: Ref<NumberInputMethods>
|
|
245
287
|
}
|
|
246
288
|
|
|
247
289
|
export interface NumberInputMethods {
|
|
@@ -254,7 +296,7 @@ const defaultFormat = (value: number) => String(value)
|
|
|
254
296
|
type Props = NumberInputProps &
|
|
255
297
|
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
|
|
256
298
|
|
|
257
|
-
export const Root = /* @__PURE__ */ forwardRef<
|
|
299
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
258
300
|
(
|
|
259
301
|
{
|
|
260
302
|
value,
|
|
@@ -270,16 +312,21 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
270
312
|
drag = 1,
|
|
271
313
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
272
314
|
pointerLock = false,
|
|
315
|
+
dragCursor = 'ns-resize',
|
|
273
316
|
selectOnFocus = 'none',
|
|
274
317
|
unformatOnFocus = false,
|
|
275
318
|
keepCaretOnStep = false,
|
|
276
319
|
blurOnEnter = true,
|
|
277
320
|
disabled = false,
|
|
278
|
-
|
|
321
|
+
readOnly = false,
|
|
279
322
|
className,
|
|
280
323
|
style,
|
|
281
324
|
onChange,
|
|
325
|
+
onChangeStart,
|
|
326
|
+
onChangeEnd,
|
|
327
|
+
changeEndDelay,
|
|
282
328
|
children,
|
|
329
|
+
actionsRef,
|
|
283
330
|
...props
|
|
284
331
|
}: Props,
|
|
285
332
|
forwardedRef,
|
|
@@ -291,7 +338,13 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
291
338
|
* but the half-finished entry that has no value to be derived from yet.
|
|
292
339
|
*/
|
|
293
340
|
const [draft, setDraft] = useState<string | null>(null)
|
|
294
|
-
const inactive = disabled ||
|
|
341
|
+
const inactive = disabled || readOnly
|
|
342
|
+
|
|
343
|
+
const gesture = useChangeGesture(
|
|
344
|
+
value,
|
|
345
|
+
{ onChangeStart, onChangeEnd, changeEndDelay },
|
|
346
|
+
inactive,
|
|
347
|
+
)
|
|
295
348
|
|
|
296
349
|
// --- interpret props ---
|
|
297
350
|
const format = formatProp ?? defaultFormat
|
|
@@ -339,18 +392,25 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
339
392
|
// Deliberately unclamped: clamping here would make "1500" impossible
|
|
340
393
|
// to type into an input whose max is 100.
|
|
341
394
|
const parsed = parse(next)
|
|
342
|
-
if (Number.isFinite(parsed))
|
|
395
|
+
if (!Number.isFinite(parsed)) return
|
|
396
|
+
if (parsed !== value) {
|
|
397
|
+
gesture.pulse('keyboard')
|
|
398
|
+
gesture.changed(parsed)
|
|
399
|
+
}
|
|
400
|
+
onChange?.(parsed)
|
|
343
401
|
},
|
|
344
|
-
[inactive, parse, onChange],
|
|
402
|
+
[inactive, parse, onChange, value, gesture],
|
|
345
403
|
)
|
|
346
404
|
|
|
347
405
|
const changeValue = useCallback(
|
|
348
406
|
(next: number) => {
|
|
349
407
|
if (inactive) return
|
|
350
408
|
setDraft(null)
|
|
351
|
-
if (next
|
|
409
|
+
if (next === value) return
|
|
410
|
+
gesture.changed(next)
|
|
411
|
+
onChange?.(next)
|
|
352
412
|
},
|
|
353
|
-
[inactive, value, onChange],
|
|
413
|
+
[inactive, value, onChange, gesture],
|
|
354
414
|
)
|
|
355
415
|
|
|
356
416
|
const commitDraft = useCallback(() => {
|
|
@@ -362,9 +422,25 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
362
422
|
})
|
|
363
423
|
// Text with no number in it is not a value. Dropping the draft puts the
|
|
364
424
|
// input back to what it was showing.
|
|
365
|
-
if (committed === null)
|
|
366
|
-
|
|
367
|
-
|
|
425
|
+
if (committed === null) {
|
|
426
|
+
setDraft(null)
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
// The typing was a gesture of its own, and committing is where it ends.
|
|
430
|
+
if (committed !== value) gesture.pulse('keyboard')
|
|
431
|
+
changeValue(committed)
|
|
432
|
+
gesture.end()
|
|
433
|
+
}, [
|
|
434
|
+
draft,
|
|
435
|
+
inactive,
|
|
436
|
+
parse,
|
|
437
|
+
clampValue,
|
|
438
|
+
min,
|
|
439
|
+
max,
|
|
440
|
+
changeValue,
|
|
441
|
+
value,
|
|
442
|
+
gesture,
|
|
443
|
+
])
|
|
368
444
|
|
|
369
445
|
const nudge = useCallback(
|
|
370
446
|
(
|
|
@@ -385,6 +461,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
385
461
|
const direction = wheelDirection(event)
|
|
386
462
|
if (!wheel || inactive || direction === null) return
|
|
387
463
|
event.preventDefault()
|
|
464
|
+
gesture.pulse('wheel')
|
|
388
465
|
nudge(direction, wheel, event)
|
|
389
466
|
},
|
|
390
467
|
{ requireFocus: true },
|
|
@@ -398,7 +475,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
398
475
|
step,
|
|
399
476
|
scale,
|
|
400
477
|
disabled,
|
|
401
|
-
|
|
478
|
+
readOnly,
|
|
402
479
|
clampValue,
|
|
403
480
|
range,
|
|
404
481
|
rawRange,
|
|
@@ -408,6 +485,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
408
485
|
outOfRange,
|
|
409
486
|
dragSensitivity,
|
|
410
487
|
pointerLock,
|
|
488
|
+
dragCursor,
|
|
411
489
|
selectOnFocus,
|
|
412
490
|
unformatOnFocus,
|
|
413
491
|
keepCaretOnStep,
|
|
@@ -430,7 +508,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
430
508
|
step,
|
|
431
509
|
scale,
|
|
432
510
|
disabled,
|
|
433
|
-
|
|
511
|
+
readOnly,
|
|
434
512
|
clampValue,
|
|
435
513
|
range,
|
|
436
514
|
rawRange,
|
|
@@ -443,6 +521,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
443
521
|
drag,
|
|
444
522
|
dragSensitivity,
|
|
445
523
|
pointerLock,
|
|
524
|
+
dragCursor,
|
|
446
525
|
selectOnFocus,
|
|
447
526
|
unformatOnFocus,
|
|
448
527
|
keepCaretOnStep,
|
|
@@ -456,7 +535,14 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
456
535
|
],
|
|
457
536
|
)
|
|
458
537
|
|
|
459
|
-
|
|
538
|
+
// Composed once, so React attaches the refs a single time instead of
|
|
539
|
+
// detaching and re-attaching on every render.
|
|
540
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
541
|
+
forwardedRef,
|
|
542
|
+
wheelRefCallback,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
useImperativeHandle(actionsRef, () => {
|
|
460
546
|
return {
|
|
461
547
|
focus() {
|
|
462
548
|
if (!disabled) inputRef.current?.focus()
|
|
@@ -467,18 +553,47 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
467
553
|
}
|
|
468
554
|
}, [disabled])
|
|
469
555
|
|
|
556
|
+
const pressed = useRef(false)
|
|
557
|
+
const gestureContext = useMemo(
|
|
558
|
+
() => ({
|
|
559
|
+
press: () => {
|
|
560
|
+
if (inactive) return
|
|
561
|
+
gesture.hold('pointer')
|
|
562
|
+
// A press on a stepper reaches `Stepper` too; one release will do.
|
|
563
|
+
if (pressed.current) return
|
|
564
|
+
pressed.current = true
|
|
565
|
+
// Released wherever the pointer ends up: the stepper drag captures
|
|
566
|
+
// it, and a press that wanders off the button still ends.
|
|
567
|
+
const release = () => {
|
|
568
|
+
pressed.current = false
|
|
569
|
+
gesture.end()
|
|
570
|
+
window.removeEventListener('pointerup', release)
|
|
571
|
+
window.removeEventListener('pointercancel', release)
|
|
572
|
+
}
|
|
573
|
+
window.addEventListener('pointerup', release)
|
|
574
|
+
window.addEventListener('pointercancel', release)
|
|
575
|
+
},
|
|
576
|
+
key: () => {
|
|
577
|
+
if (!inactive) gesture.pulse('keyboard')
|
|
578
|
+
},
|
|
579
|
+
}),
|
|
580
|
+
[gesture, inactive],
|
|
581
|
+
)
|
|
582
|
+
|
|
470
583
|
return (
|
|
471
584
|
<NumberInputProvider value={context}>
|
|
472
|
-
<
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
585
|
+
<NumberInputGestureProvider value={gestureContext}>
|
|
586
|
+
<div
|
|
587
|
+
ref={rootRefCallback}
|
|
588
|
+
className={className}
|
|
589
|
+
data-disabled={disabled ? '' : undefined}
|
|
590
|
+
data-readonly={readOnly ? '' : undefined}
|
|
591
|
+
style={style}
|
|
592
|
+
{...props}
|
|
593
|
+
>
|
|
594
|
+
{children}
|
|
595
|
+
</div>
|
|
596
|
+
</NumberInputGestureProvider>
|
|
482
597
|
</NumberInputProvider>
|
|
483
598
|
)
|
|
484
599
|
},
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
5
|
ReactNode,
|
|
6
|
+
Ref,
|
|
6
7
|
useEffect,
|
|
7
8
|
useImperativeHandle,
|
|
8
9
|
useMemo,
|
|
@@ -11,20 +12,24 @@ import {
|
|
|
11
12
|
} from 'react'
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
|
-
blackKeyWidth,
|
|
15
15
|
createPianoInput,
|
|
16
|
-
fitWhiteKeyWidth,
|
|
17
|
-
getNoteRangeArray,
|
|
18
16
|
type KeyboardShortcuts,
|
|
19
17
|
type KeyboardShortcutsScope,
|
|
20
18
|
notePosition,
|
|
21
|
-
pianoWidth,
|
|
22
19
|
type NoteRange,
|
|
23
20
|
type PianoInputInstance,
|
|
24
21
|
type PianoLayout,
|
|
25
22
|
} from '@tremolo-ui/dom'
|
|
23
|
+
import {
|
|
24
|
+
blackKeyWidth,
|
|
25
|
+
fitWhiteKeyWidth,
|
|
26
|
+
getNoteRangeArray,
|
|
27
|
+
pianoWidth,
|
|
28
|
+
} from '@tremolo-ui/dom/internal'
|
|
26
29
|
import { isWhiteKey, noteKey } from '@tremolo-ui/functions'
|
|
27
30
|
|
|
31
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
34
|
* What {@link PianoProps.keyProps} may return for one key.
|
|
30
35
|
*
|
|
@@ -155,6 +160,12 @@ export interface PianoProps {
|
|
|
155
160
|
onPlayNote?: (note: number, velocity?: number) => void
|
|
156
161
|
/** Called once everything holding a note has let go of it. */
|
|
157
162
|
onStopNote?: (note: number) => void
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Receives `playNote` and `stopNote`, which press and release a key as the
|
|
166
|
+
* pointer or the keyboard would. `ref` reaches the root element itself.
|
|
167
|
+
*/
|
|
168
|
+
actionsRef?: Ref<PianoMethods>
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
export interface PianoMethods {
|
|
@@ -165,7 +176,7 @@ export interface PianoMethods {
|
|
|
165
176
|
type Props = PianoProps &
|
|
166
177
|
Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>
|
|
167
178
|
|
|
168
|
-
export const Root = /* @__PURE__ */ forwardRef<
|
|
179
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
169
180
|
function Root(
|
|
170
181
|
{
|
|
171
182
|
noteRange,
|
|
@@ -185,6 +196,7 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
185
196
|
classes,
|
|
186
197
|
onPlayNote,
|
|
187
198
|
onStopNote,
|
|
199
|
+
actionsRef,
|
|
188
200
|
...props
|
|
189
201
|
},
|
|
190
202
|
forwardedRef,
|
|
@@ -295,8 +307,13 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
295
307
|
return () => resizeObserver.disconnect()
|
|
296
308
|
}, [resizable, node, first, last, keyGap, blackKeyWidthRatio])
|
|
297
309
|
|
|
298
|
-
|
|
310
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
299
311
|
forwardedRef,
|
|
312
|
+
setNode,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
useImperativeHandle(
|
|
316
|
+
actionsRef,
|
|
300
317
|
() => ({
|
|
301
318
|
playNote: (note, velocity) =>
|
|
302
319
|
instanceRef.current?.noteOn(note, { source: 'api', velocity }),
|
|
@@ -308,7 +325,7 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
308
325
|
|
|
309
326
|
return (
|
|
310
327
|
<div
|
|
311
|
-
ref={
|
|
328
|
+
ref={rootRefCallback}
|
|
312
329
|
className={className}
|
|
313
330
|
data-resizable={resizable ? '' : undefined}
|
|
314
331
|
role="group"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
|
2
2
|
|
|
3
|
-
import { POINT_AXIS, wheelMove } from '@tremolo-ui/dom'
|
|
3
|
+
import { POINT_AXIS, wheelMove } from '@tremolo-ui/dom/internal'
|
|
4
4
|
|
|
5
5
|
import { useComposedRefs } from '../../compose-refs'
|
|
6
6
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
@@ -8,17 +8,21 @@ import {
|
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
clampPoint,
|
|
11
|
+
type ChangeSource,
|
|
13
12
|
type InputEventOption,
|
|
14
13
|
type ModifierValue,
|
|
15
|
-
POINT_AXIS,
|
|
16
14
|
type PointPosition,
|
|
17
15
|
type PointsEditorPoint,
|
|
18
|
-
selectModifier,
|
|
19
16
|
} from '@tremolo-ui/dom'
|
|
17
|
+
import {
|
|
18
|
+
arrowKeyMove,
|
|
19
|
+
clampPoint,
|
|
20
|
+
POINT_AXIS,
|
|
21
|
+
selectModifier,
|
|
22
|
+
} from '@tremolo-ui/dom/internal'
|
|
20
23
|
|
|
21
24
|
import { useComposedRefs } from '../../compose-refs'
|
|
25
|
+
import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
|
|
22
26
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
23
27
|
import { useCheckPlacement } from '../_util/Placement'
|
|
24
28
|
import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
@@ -49,8 +53,8 @@ export interface PointsEditorPointProps<T extends PointPosition> {
|
|
|
49
53
|
|
|
50
54
|
/** Overrides the `disabled` of `PointsEditor.Root`. */
|
|
51
55
|
disabled?: boolean
|
|
52
|
-
/** Overrides the `
|
|
53
|
-
|
|
56
|
+
/** Overrides the `readOnly` of `PointsEditor.Root`. */
|
|
57
|
+
readOnly?: boolean
|
|
54
58
|
|
|
55
59
|
/** Overrides the `wheel` of `PointsEditor.Root`. */
|
|
56
60
|
wheel?: ModifierValue<InputEventOption> | null
|
|
@@ -70,10 +74,25 @@ export interface PointsEditorPointProps<T extends PointPosition> {
|
|
|
70
74
|
* arrow keys or the wheel, including when it moves along with a selection.
|
|
71
75
|
*/
|
|
72
76
|
onChange?: (value: PointPosition) => void
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Called when a change of this point starts — a drag on it, the first wheel
|
|
79
|
+
* notch or arrow key while it has the focus — with where it is and what the
|
|
80
|
+
* change is made with. The points that move along with a selection report
|
|
81
|
+
* through `onChange` only: the change belongs to the point being operated.
|
|
82
|
+
*/
|
|
83
|
+
onChangeStart?: (value: PointPosition, source: ChangeSource) => void
|
|
84
|
+
/**
|
|
85
|
+
* Called when that change ends, with where the point is: on release, or
|
|
86
|
+
* `changeEndDelay` after the last wheel notch or arrow key.
|
|
87
|
+
*/
|
|
88
|
+
onChangeEnd?: (value: PointPosition, source: ChangeSource) => void
|
|
89
|
+
/**
|
|
90
|
+
* How long after the last wheel notch or arrow key the change counts as
|
|
91
|
+
* over, in milliseconds. Neither has an event that says it is done.
|
|
92
|
+
*
|
|
93
|
+
* @default 500
|
|
94
|
+
*/
|
|
95
|
+
changeEndDelay?: number
|
|
77
96
|
|
|
78
97
|
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
79
98
|
}
|
|
@@ -98,15 +117,16 @@ export function Point<T extends PointPosition>({
|
|
|
98
117
|
color,
|
|
99
118
|
|
|
100
119
|
disabled: _disabled,
|
|
101
|
-
|
|
120
|
+
readOnly: _readOnly,
|
|
102
121
|
wheel: _wheel,
|
|
103
122
|
keyboard: _keyboard,
|
|
104
123
|
'aria-label': ariaLabel,
|
|
105
124
|
'aria-valuetext': ariaValuetext,
|
|
106
125
|
|
|
107
126
|
onChange,
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
onChangeStart,
|
|
128
|
+
onChangeEnd,
|
|
129
|
+
changeEndDelay,
|
|
110
130
|
|
|
111
131
|
className,
|
|
112
132
|
style,
|
|
@@ -118,9 +138,9 @@ export function Point<T extends PointPosition>({
|
|
|
118
138
|
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorPointProps<T>>) {
|
|
119
139
|
const {
|
|
120
140
|
containerRef,
|
|
121
|
-
|
|
141
|
+
dragCursor,
|
|
122
142
|
disabled: rootDisabled,
|
|
123
|
-
|
|
143
|
+
readOnly: rootReadOnly,
|
|
124
144
|
wheel: rootWheel,
|
|
125
145
|
keyboard: rootKeyboard,
|
|
126
146
|
dragSensitivity,
|
|
@@ -133,8 +153,8 @@ export function Point<T extends PointPosition>({
|
|
|
133
153
|
const selected = selection.includes(id)
|
|
134
154
|
|
|
135
155
|
const disabled = _disabled ?? rootDisabled
|
|
136
|
-
const
|
|
137
|
-
const inactive = disabled ||
|
|
156
|
+
const readOnly = _readOnly ?? rootReadOnly
|
|
157
|
+
const inactive = disabled || readOnly
|
|
138
158
|
// `null` means "no event" and has to survive the fallback, so `??` is not
|
|
139
159
|
// enough: only an omitted prop inherits from the root.
|
|
140
160
|
const wheel = _wheel === undefined ? rootWheel : _wheel
|
|
@@ -142,6 +162,19 @@ export function Point<T extends PointPosition>({
|
|
|
142
162
|
|
|
143
163
|
useCheckPlacement('PointsEditor.Point', 'PointsEditor.Container')
|
|
144
164
|
|
|
165
|
+
const current = clampPoint(value, min, max)
|
|
166
|
+
const gesture = useChangeGesture(
|
|
167
|
+
current,
|
|
168
|
+
{ onChangeStart, onChangeEnd, changeEndDelay },
|
|
169
|
+
inactive,
|
|
170
|
+
)
|
|
171
|
+
// Every move reaches the point through here, whatever moved it, so the
|
|
172
|
+
// last one is what `onChangeEnd` gives.
|
|
173
|
+
const handleChange = (next: PointPosition) => {
|
|
174
|
+
gesture.changed(next)
|
|
175
|
+
onChange?.(next)
|
|
176
|
+
}
|
|
177
|
+
|
|
145
178
|
// Compared against the focus below, so the point needs its own element.
|
|
146
179
|
const [element, setElement] = useState<HTMLDivElement | null>(null)
|
|
147
180
|
const xInputRef = useRef<HTMLInputElement>(null)
|
|
@@ -155,9 +188,10 @@ export function Point<T extends PointPosition>({
|
|
|
155
188
|
min,
|
|
156
189
|
max,
|
|
157
190
|
readonly: inactive,
|
|
158
|
-
onChange,
|
|
191
|
+
onChange: onChange ? handleChange : undefined,
|
|
159
192
|
element,
|
|
160
193
|
wheel,
|
|
194
|
+
beforeWheel: () => gesture.pulse('wheel'),
|
|
161
195
|
})
|
|
162
196
|
useEffect(() => {
|
|
163
197
|
registration.current = {
|
|
@@ -165,9 +199,10 @@ export function Point<T extends PointPosition>({
|
|
|
165
199
|
min,
|
|
166
200
|
max,
|
|
167
201
|
readonly: inactive,
|
|
168
|
-
onChange,
|
|
202
|
+
onChange: onChange ? handleChange : undefined,
|
|
169
203
|
element,
|
|
170
204
|
wheel,
|
|
205
|
+
beforeWheel: () => gesture.pulse('wheel'),
|
|
171
206
|
}
|
|
172
207
|
})
|
|
173
208
|
|
|
@@ -186,7 +221,7 @@ export function Point<T extends PointPosition>({
|
|
|
186
221
|
baseElementRef: containerRef,
|
|
187
222
|
sensitivity: (state) =>
|
|
188
223
|
selectModifier(dragSensitivity, state.event).value,
|
|
189
|
-
cursor: inactive ? undefined :
|
|
224
|
+
cursor: inactive ? undefined : dragCursor,
|
|
190
225
|
shouldStart: () => !disabled,
|
|
191
226
|
// The value is a move rather than a position: the point keeps the offset
|
|
192
227
|
// it was grabbed at, and everything else selected moves with it by the
|
|
@@ -204,14 +239,11 @@ export function Point<T extends PointPosition>({
|
|
|
204
239
|
pointerOrigin.current = { x, y }
|
|
205
240
|
xInputRef.current?.focus()
|
|
206
241
|
|
|
207
|
-
if (inactive)
|
|
208
|
-
onDragStart?.(clampPoint(value, min, max))
|
|
242
|
+
if (!inactive) gesture.hold('pointer')
|
|
209
243
|
},
|
|
210
244
|
onDragEnd: () => {
|
|
211
245
|
pointerOrigin.current = null
|
|
212
|
-
|
|
213
|
-
if (inactive) return
|
|
214
|
-
onDragEnd?.(clampPoint(value, min, max))
|
|
246
|
+
gesture.end()
|
|
215
247
|
},
|
|
216
248
|
})
|
|
217
249
|
|
|
@@ -229,6 +261,7 @@ export function Point<T extends PointPosition>({
|
|
|
229
261
|
if (!move) return
|
|
230
262
|
event.preventDefault()
|
|
231
263
|
if (!onChange || inactive || !keyboard) return
|
|
264
|
+
gesture.pulse('keyboard')
|
|
232
265
|
editor.nudgePoint(
|
|
233
266
|
id,
|
|
234
267
|
move.axis === 0 ? 'x' : 'y',
|
|
@@ -238,8 +271,6 @@ export function Point<T extends PointPosition>({
|
|
|
238
271
|
)
|
|
239
272
|
}
|
|
240
273
|
|
|
241
|
-
const current = clampPoint(value, min, max)
|
|
242
|
-
|
|
243
274
|
return (
|
|
244
275
|
// The visual point has two values, so its semantics live on the two range
|
|
245
276
|
// inputs nested inside it rather than on this drag handle.
|
|
@@ -248,7 +279,7 @@ export function Point<T extends PointPosition>({
|
|
|
248
279
|
ref={refCallback}
|
|
249
280
|
className={className}
|
|
250
281
|
data-disabled={disabled ? '' : undefined}
|
|
251
|
-
data-readonly={
|
|
282
|
+
data-readonly={readOnly ? '' : undefined}
|
|
252
283
|
// A press lands on the point, which cannot hold focus, and the browser
|
|
253
284
|
// answers that by clearing the focus to the body — undoing the focus the
|
|
254
285
|
// drag just gave the input. Taking the focus here keeps it inside, and
|
|
@@ -294,12 +325,12 @@ export function Point<T extends PointPosition>({
|
|
|
294
325
|
max={max?.[axis] ?? 1}
|
|
295
326
|
step="any"
|
|
296
327
|
disabled={disabled}
|
|
297
|
-
aria-readonly={
|
|
328
|
+
aria-readonly={readOnly}
|
|
298
329
|
aria-orientation={axis === 'x' ? 'horizontal' : 'vertical'}
|
|
299
330
|
aria-label={perAxis(ariaLabel, axis) ?? axis}
|
|
300
331
|
aria-valuetext={perAxis(ariaValuetext, axis)}
|
|
301
332
|
onChange={(event) => {
|
|
302
|
-
if (
|
|
333
|
+
if (readOnly) {
|
|
303
334
|
event.currentTarget.value = String(current[axis])
|
|
304
335
|
return
|
|
305
336
|
}
|