@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
|
@@ -1,56 +1,34 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useComposedRefs } from '../../compose-refs'
|
|
4
|
-
import { cssLength } from '../_util/css-length'
|
|
5
4
|
import { Placement } from '../_util/Placement'
|
|
6
5
|
|
|
7
6
|
import { useXYPadContext } from './context'
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
ref={composedRef}
|
|
39
|
-
className={className}
|
|
40
|
-
style={
|
|
41
|
-
{
|
|
42
|
-
'--color': color,
|
|
43
|
-
'--width': cssLength(width),
|
|
44
|
-
'--height': cssLength(height),
|
|
45
|
-
// The thumb inside is placed against this box.
|
|
46
|
-
position: 'relative',
|
|
47
|
-
...style,
|
|
48
|
-
} as CSSProperties
|
|
49
|
-
}
|
|
50
|
-
{...props}
|
|
51
|
-
>
|
|
52
|
-
<Placement name="XYPad.Area">{children}</Placement>
|
|
53
|
-
</div>
|
|
54
|
-
)
|
|
55
|
-
},
|
|
56
|
-
)
|
|
8
|
+
export const Area = /* @__PURE__ */ forwardRef<
|
|
9
|
+
HTMLDivElement,
|
|
10
|
+
ComponentPropsWithoutRef<'div'>
|
|
11
|
+
>(function Area({ children, className, style, ...props }, forwardedRef) {
|
|
12
|
+
const { areaRef } = useXYPadContext()
|
|
13
|
+
|
|
14
|
+
// The area is what the pointer position is normalized against, so the
|
|
15
|
+
// context ref is composed with any ref the caller passed.
|
|
16
|
+
const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, areaRef)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
ref={composedRef}
|
|
21
|
+
className={className}
|
|
22
|
+
style={
|
|
23
|
+
{
|
|
24
|
+
// The thumb inside is placed against this box.
|
|
25
|
+
position: 'relative',
|
|
26
|
+
...style,
|
|
27
|
+
} as CSSProperties
|
|
28
|
+
}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<Placement name="XYPad.Area">{children}</Placement>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
})
|
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
CSSProperties,
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
|
+
import { toXY, type XYInput } from '@tremolo-ui/dom'
|
|
11
|
+
|
|
10
12
|
import { useCheckPlacement } from '../_util/Placement'
|
|
11
13
|
import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
12
14
|
|
|
13
|
-
import {
|
|
15
|
+
import { useXYPadContext } from './context'
|
|
14
16
|
|
|
15
17
|
import type { CSSVariables } from '../../css-variables'
|
|
16
18
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { createContext, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type XY } from '@tremolo-ui/dom'
|
|
4
4
|
import type { Scale } from '@tremolo-ui/functions'
|
|
5
5
|
|
|
6
6
|
import type { XYPadThumbMethods } from './Thumb'
|
|
7
7
|
|
|
8
|
-
export { toXY, type XY, type XYInput }
|
|
9
|
-
|
|
10
8
|
export type XYPadContextValue = {
|
|
11
9
|
value: XY<number>
|
|
12
10
|
min: XY<number>
|
|
@@ -12,26 +12,30 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
applyDelta,
|
|
15
|
+
arrowKeyMove,
|
|
15
16
|
type AxisOptions,
|
|
17
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
18
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
19
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
16
20
|
InputEventOption,
|
|
17
21
|
ModifierState,
|
|
18
22
|
type ModifierValue,
|
|
19
23
|
selectModifier,
|
|
24
|
+
toXY,
|
|
25
|
+
valuePercent,
|
|
26
|
+
wheelMove,
|
|
27
|
+
type XY,
|
|
28
|
+
type XYInput,
|
|
20
29
|
} from '@tremolo-ui/dom'
|
|
21
|
-
import { linearScale, type Scale
|
|
30
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
22
31
|
|
|
23
32
|
import { useComposedRefs } from '../../compose-refs'
|
|
24
33
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
25
34
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
26
35
|
import { useWheel } from '../../hooks/useWheel'
|
|
27
|
-
import {
|
|
28
|
-
DEFAULT_DRAG_SENSITIVITY,
|
|
29
|
-
DEFAULT_KEYBOARD_OPTIONS,
|
|
30
|
-
DEFAULT_WHEEL_OPTIONS,
|
|
31
|
-
} from '../../input-event'
|
|
32
36
|
|
|
33
37
|
import { Area } from './Area'
|
|
34
|
-
import {
|
|
38
|
+
import { XYPadProvider } from './context'
|
|
35
39
|
import { Thumb, XYPadThumbMethods } from './Thumb'
|
|
36
40
|
|
|
37
41
|
const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
@@ -227,14 +231,17 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
227
231
|
const scale = useMemo(() => toXY(_scale), [_scale])
|
|
228
232
|
const reverse = useMemo(() => toXY(_reverse), [_reverse])
|
|
229
233
|
|
|
230
|
-
const percent = useMemo(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
const percent = useMemo(
|
|
235
|
+
(): XY<number> =>
|
|
236
|
+
[0, 1].map((i) =>
|
|
237
|
+
valuePercent(
|
|
238
|
+
value[i],
|
|
239
|
+
{ min: min[i], max: max[i], scale: scale[i] },
|
|
240
|
+
reverse[i],
|
|
241
|
+
),
|
|
242
|
+
) as XY<number>,
|
|
243
|
+
[value, min, max, scale, reverse],
|
|
244
|
+
)
|
|
238
245
|
|
|
239
246
|
// --- internal functions ---
|
|
240
247
|
// `AxisOptions` extends `ValueRange`, so the same pair also describes the
|
|
@@ -291,21 +298,15 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
291
298
|
|
|
292
299
|
const handleKeyDown = useCallback(
|
|
293
300
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
294
|
-
const key = event.key
|
|
295
|
-
if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
296
|
-
return
|
|
297
|
-
|
|
298
301
|
// The key picks the axis, whichever of the two inputs holds the
|
|
299
|
-
// focus: the
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
// focus: the focus lands on the x input, so reading the axis off the
|
|
303
|
+
// input would leave the y axis with no keys at all.
|
|
304
|
+
const move = arrowKeyMove(event.key)
|
|
305
|
+
if (!move) return
|
|
303
306
|
event.preventDefault()
|
|
304
307
|
if (!onChange || inactive || !keyboard) return
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (reverse[i]) direction *= -1
|
|
308
|
-
onChange(nudge(i, direction, keyboard, event))
|
|
308
|
+
const { axis: i, direction } = move
|
|
309
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event))
|
|
309
310
|
},
|
|
310
311
|
[onChange, inactive, keyboard, reverse, nudge],
|
|
311
312
|
)
|
|
@@ -337,19 +338,11 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
337
338
|
|
|
338
339
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
339
340
|
if (!onChange || inactive || !wheel) return
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// moved keeps shift working as the x-axis modifier — and picks up a
|
|
343
|
-
// trackpad's own horizontal gesture, which never had a modifier.
|
|
344
|
-
const horizontal = event.deltaX !== 0
|
|
345
|
-
const delta = horizontal ? event.deltaX : event.deltaY
|
|
346
|
-
if (delta === 0) return
|
|
347
|
-
const i: 0 | 1 = horizontal || event.shiftKey ? 0 : 1
|
|
341
|
+
const move = wheelMove(event)
|
|
342
|
+
if (!move) return
|
|
348
343
|
event.preventDefault()
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (reverse[i]) direction *= -1
|
|
352
|
-
onChange(nudge(i, direction, wheel, event))
|
|
344
|
+
const { axis: i, direction } = move
|
|
345
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event))
|
|
353
346
|
}, WHEEL_OPTIONS)
|
|
354
347
|
|
|
355
348
|
// Composed once, so React attaches the refs a single time instead of
|
|
@@ -444,10 +437,4 @@ export const XYPad = {
|
|
|
444
437
|
}
|
|
445
438
|
|
|
446
439
|
export { type XYPadThumbProps, type XYPadThumbMethods } from './Thumb'
|
|
447
|
-
export { type
|
|
448
|
-
export {
|
|
449
|
-
useXYPadContext,
|
|
450
|
-
type XYPadContextValue,
|
|
451
|
-
type XY,
|
|
452
|
-
type XYInput,
|
|
453
|
-
} from './context'
|
|
440
|
+
export { useXYPadContext, type XYPadContextValue } from './context'
|
|
@@ -1,162 +1,25 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
applyDelta,
|
|
5
|
-
type InputEventOption,
|
|
6
|
-
type ModifierState,
|
|
7
|
-
type ModifierValue,
|
|
8
|
-
} from '@tremolo-ui/dom'
|
|
9
|
-
import { linearScale, type ValueRange } from '@tremolo-ui/functions'
|
|
10
|
-
|
|
11
|
-
/** Positions probed across the travel. The ends are left out so that the
|
|
12
|
-
* clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
|
|
13
|
-
const PROBES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
|
14
|
-
|
|
15
|
-
const NONE: ModifierState = {
|
|
16
|
-
shiftKey: false,
|
|
17
|
-
altKey: false,
|
|
18
|
-
ctrlKey: false,
|
|
19
|
-
metaKey: false,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const MODIFIER_STATE: Record<string, ModifierState> = {
|
|
23
|
-
shift: { ...NONE, shiftKey: true },
|
|
24
|
-
alt: { ...NONE, altKey: true },
|
|
25
|
-
ctrl: { ...NONE, ctrlKey: true },
|
|
26
|
-
meta: { ...NONE, metaKey: true },
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Each entry of a modifier-aware input option, as a name and held keys. */
|
|
30
|
-
function entries(
|
|
31
|
-
options: ModifierValue<InputEventOption>,
|
|
32
|
-
): { name: string; modifiers: ModifierState }[] {
|
|
33
|
-
if (Array.isArray(options)) return [{ name: '', modifiers: NONE }]
|
|
34
|
-
return Object.keys(options).map((key) => ({
|
|
35
|
-
name: key === 'default' ? '' : key,
|
|
36
|
-
modifiers: MODIFIER_STATE[key] ?? NONE,
|
|
37
|
-
}))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface Outcome {
|
|
41
|
-
/** The press changed the value at least once across the travel. */
|
|
42
|
-
moved: boolean
|
|
43
|
-
/** The change reached the displayed text at least once. */
|
|
44
|
-
visible: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Press every entry of `options` at nine points along the travel and report
|
|
49
|
-
* whether anything came of it.
|
|
50
|
-
*
|
|
51
|
-
* Run against `applyDelta` itself rather than against a reading of `step`:
|
|
52
|
-
* the whole point is that the amount, the step and the scale interact, and
|
|
53
|
-
* the pipeline is the only thing that knows how.
|
|
54
|
-
*/
|
|
55
|
-
function probe(
|
|
56
|
-
options: ModifierValue<InputEventOption>,
|
|
57
|
-
range: ValueRange,
|
|
58
|
-
format?: (value: number) => string,
|
|
59
|
-
): Outcome {
|
|
60
|
-
const { min, max, scale = linearScale } = range
|
|
61
|
-
const outcome: Outcome = { moved: false, visible: false }
|
|
62
|
-
|
|
63
|
-
for (const { modifiers } of entries(options)) {
|
|
64
|
-
for (const position of PROBES) {
|
|
65
|
-
const value = scale.denormalize(position, min, max)
|
|
66
|
-
const up = applyDelta(value, 1, options, range, modifiers)
|
|
67
|
-
const down = applyDelta(value, -1, options, range, modifiers)
|
|
68
|
-
if (up !== value || down !== value) outcome.moved = true
|
|
69
|
-
if (!format) continue
|
|
70
|
-
const shown = format(value)
|
|
71
|
-
if (format(up) !== shown || format(down) !== shown) outcome.visible = true
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return outcome
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface CheckStepsOptions {
|
|
79
|
-
/** The component, for the message. */
|
|
80
|
-
component: string
|
|
81
|
-
/** The axis, for a component that has more than one. */
|
|
82
|
-
axis?: string
|
|
83
|
-
/**
|
|
84
|
-
* The range to probe, or `null` to check nothing. An unbounded input has no
|
|
85
|
-
* travel to sample, so `NumberInput` passes `null` when `min` and `max` are
|
|
86
|
-
* not both there.
|
|
87
|
-
*/
|
|
88
|
-
range: ValueRange | null
|
|
89
|
-
keyboard?: ModifierValue<InputEventOption> | null
|
|
90
|
-
wheel?: ModifierValue<InputEventOption> | null
|
|
91
|
-
/**
|
|
92
|
-
* How the value is displayed, where the component shows one. Called with
|
|
93
|
-
* probe values only, and only in development.
|
|
94
|
-
*/
|
|
95
|
-
format?: (value: number) => string
|
|
96
|
-
}
|
|
3
|
+
import { checkSteps, type CheckStepsOptions } from '@tremolo-ui/dom'
|
|
97
4
|
|
|
98
5
|
/**
|
|
99
6
|
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
100
|
-
* a change the user can see.
|
|
101
|
-
*
|
|
102
|
-
* Two settings that are each fine on their own can cancel out, and nothing
|
|
103
|
-
* fails when they do — the control simply sits there:
|
|
104
|
-
*
|
|
105
|
-
* - **`step` coarser than the amount.** `keyboard={['raw', 0.1]}` with
|
|
106
|
-
* `step={1}` rounds every press straight back to where it started
|
|
107
|
-
* - **the display coarser than the amount.** A `format` showing two decimals
|
|
108
|
-
* of a kHz value cannot show a press worth 1 Hz
|
|
109
|
-
*
|
|
110
|
-
* The second is only reported when the press is invisible at *every* point
|
|
111
|
-
* along the travel. A display that rounds is a deliberate choice and is
|
|
112
|
-
* normally right — it is being too coarse everywhere that makes it a mistake.
|
|
7
|
+
* a change the user can see. The probing is `checkSteps` in the core.
|
|
113
8
|
*
|
|
114
9
|
* @internal
|
|
115
10
|
*/
|
|
116
|
-
export function useCheckSteps({
|
|
117
|
-
component,
|
|
118
|
-
axis,
|
|
119
|
-
range,
|
|
120
|
-
keyboard,
|
|
121
|
-
wheel,
|
|
122
|
-
format,
|
|
123
|
-
}: CheckStepsOptions) {
|
|
11
|
+
export function useCheckSteps(options: CheckStepsOptions) {
|
|
12
|
+
const { component, axis, range, keyboard, wheel } = options
|
|
124
13
|
const { min, max, step, scale } = range ?? {}
|
|
125
|
-
const where = axis ? `${component} (${axis})` : component
|
|
126
14
|
|
|
127
15
|
useEffect(() => {
|
|
128
16
|
try {
|
|
129
17
|
// Inline and first, so a bundler folds the comparison and drops the
|
|
130
|
-
// whole block —
|
|
131
|
-
//
|
|
132
|
-
// it is wrapped.
|
|
18
|
+
// whole block — and with it the only call to `checkSteps`, which then
|
|
19
|
+
// falls out of the bundle too. See `useCheckPlacement` for why it
|
|
20
|
+
// cannot go through a helper, and why it is wrapped.
|
|
133
21
|
if (process.env.NODE_ENV === 'production') return
|
|
134
|
-
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
for (const [name, options] of [
|
|
139
|
-
['keyboard', keyboard],
|
|
140
|
-
['wheel', wheel],
|
|
141
|
-
] as const) {
|
|
142
|
-
if (!options) continue
|
|
143
|
-
const { moved, visible } = probe(options, range, format)
|
|
144
|
-
if (!moved) {
|
|
145
|
-
console.warn(
|
|
146
|
-
`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` +
|
|
147
|
-
(step !== undefined
|
|
148
|
-
? ` Each press is smaller than \`step\` (${step}), so it rounds`
|
|
149
|
-
: ' Each press rounds') +
|
|
150
|
-
' straight back to where it started.',
|
|
151
|
-
)
|
|
152
|
-
} else if (format && !visible) {
|
|
153
|
-
console.warn(
|
|
154
|
-
`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\`` +
|
|
155
|
-
' shows the same text before and after, everywhere in the range.' +
|
|
156
|
-
' The display is too coarse for it to be seen.',
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
22
|
+
for (const warning of checkSteps(options)) console.warn(warning)
|
|
160
23
|
} catch {
|
|
161
24
|
// No bundler substituted NODE_ENV, so a production build cannot be told
|
|
162
25
|
// from a development one. Say nothing rather than break.
|
|
@@ -165,5 +28,5 @@ export function useCheckSteps({
|
|
|
165
28
|
// on what actually decides the outcome. `format` is left out on purpose:
|
|
166
29
|
// an inline arrow function would make this run every render.
|
|
167
30
|
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
-
}, [
|
|
31
|
+
}, [component, axis, min, max, step, scale, keyboard, wheel])
|
|
169
32
|
}
|
|
@@ -1,65 +1,49 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useInterval } from './useInterval'
|
|
3
|
+
import { createLongPress, type LongPressInstance } from '@tremolo-ui/dom'
|
|
5
4
|
|
|
5
|
+
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Repeat `callback` while a pointer is held down: once on the press, then
|
|
9
|
+
* every `interval` after `initialDelay`. The repeat itself is
|
|
10
|
+
* `createLongPress` in `@tremolo-ui/dom`.
|
|
11
|
+
*
|
|
12
|
+
* Returns the function that starts a press, for an `onPointerDown`.
|
|
13
|
+
*/
|
|
6
14
|
export function useLongPress(
|
|
7
15
|
callback: () => void,
|
|
8
16
|
initialDelay = 500,
|
|
9
17
|
interval = 40,
|
|
10
18
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
useInterval(
|
|
17
|
-
() => {
|
|
18
|
-
callback()
|
|
19
|
-
setDelay(interval)
|
|
20
|
-
},
|
|
21
|
-
pressed ? delay : null,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const stop = useCallback(
|
|
25
|
-
(pointerId?: number) => {
|
|
26
|
-
if (!isPressed.current) return
|
|
27
|
-
if (
|
|
28
|
-
pointerId !== undefined &&
|
|
29
|
-
activePointerId.current !== null &&
|
|
30
|
-
activePointerId.current !== pointerId
|
|
31
|
-
)
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
isPressed.current = false
|
|
35
|
-
activePointerId.current = null
|
|
36
|
-
setPressed(false)
|
|
37
|
-
setDelay(initialDelay)
|
|
38
|
-
},
|
|
39
|
-
[initialDelay],
|
|
40
|
-
)
|
|
19
|
+
// Updated before layout effects run, so that a press started from one
|
|
20
|
+
// right after a re-render already calls the new callback.
|
|
21
|
+
const onPress = useCallbackRef(callback)
|
|
22
|
+
const instanceRef = useRef<LongPressInstance | null>(null)
|
|
41
23
|
|
|
42
|
-
|
|
43
|
-
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
instanceRef.current?.update({ delay: initialDelay, interval })
|
|
44
26
|
})
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const instance = createLongPress({
|
|
30
|
+
onPress: () => onPress(),
|
|
31
|
+
delay: initialDelay,
|
|
32
|
+
interval,
|
|
33
|
+
})
|
|
34
|
+
instanceRef.current = instance
|
|
35
|
+
return () => {
|
|
36
|
+
instanceRef.current = null
|
|
37
|
+
instance.destroy()
|
|
38
|
+
}
|
|
39
|
+
// Created once: the options are pushed with update() above, so that a new
|
|
40
|
+
// delay does not end a press in progress.
|
|
41
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, [])
|
|
53
43
|
|
|
54
44
|
return useCallback(
|
|
55
|
-
(event?: Pick<PointerEvent, 'button' | 'pointerId'>) =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isPressed.current = true
|
|
59
|
-
activePointerId.current = event?.pointerId ?? null
|
|
60
|
-
callback()
|
|
61
|
-
setPressed(true)
|
|
62
|
-
},
|
|
63
|
-
[callback],
|
|
45
|
+
(event?: Pick<PointerEvent, 'button' | 'pointerId'>) =>
|
|
46
|
+
instanceRef.current?.start(event),
|
|
47
|
+
[],
|
|
64
48
|
)
|
|
65
49
|
}
|
|
@@ -7,14 +7,6 @@ import {
|
|
|
7
7
|
type MIDIAccessState,
|
|
8
8
|
} from '@tremolo-ui/dom'
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
NOT_SUPPORTED,
|
|
12
|
-
PERMISSION_DENIED,
|
|
13
|
-
UNAVAILABLE,
|
|
14
|
-
type MIDIAccessError,
|
|
15
|
-
type MIDIAccessOptions,
|
|
16
|
-
} from '@tremolo-ui/dom'
|
|
17
|
-
|
|
18
10
|
const INITIAL_STATE: MIDIAccessState = {
|
|
19
11
|
midiAccess: null,
|
|
20
12
|
error: null,
|
|
@@ -2,13 +2,10 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
createMIDIInput,
|
|
5
|
-
PITCH_BEND_CENTER,
|
|
6
5
|
type MIDIInputHandlers,
|
|
7
6
|
type MIDIInputInstance,
|
|
8
7
|
} from '@tremolo-ui/dom'
|
|
9
8
|
|
|
10
|
-
export { PITCH_BEND_CENTER, type MIDIInputHandlers }
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* Handle MIDI input events. To be used with {@link useMIDIAccess}.
|
|
14
11
|
*
|
package/src/index.ts
CHANGED
|
@@ -33,28 +33,23 @@ export {
|
|
|
33
33
|
} from './components/NumberInput'
|
|
34
34
|
export {
|
|
35
35
|
Piano,
|
|
36
|
-
SHORTCUTS,
|
|
37
36
|
type PianoProps,
|
|
38
37
|
type PianoMethods,
|
|
39
38
|
type KeyState,
|
|
40
39
|
type KeyAttributes,
|
|
41
|
-
type KeyboardShortcuts,
|
|
42
40
|
} from './components/Piano'
|
|
43
41
|
export {
|
|
44
42
|
PointsEditor,
|
|
45
43
|
usePointsEditorContext,
|
|
46
|
-
clampPoint,
|
|
47
44
|
type PointsEditorProps,
|
|
48
45
|
type PointsEditorContextValue,
|
|
49
46
|
type PointsEditorPointProps,
|
|
50
|
-
type PointBaseType,
|
|
51
47
|
} from './components/PointsEditor'
|
|
52
48
|
export {
|
|
53
49
|
Slider,
|
|
54
50
|
useSliderContext,
|
|
55
51
|
type SliderContextValue,
|
|
56
52
|
type SliderMarksOptionProps,
|
|
57
|
-
type MarksOptions,
|
|
58
53
|
type SliderMarksProps,
|
|
59
54
|
type SliderMethods,
|
|
60
55
|
type SliderProps,
|
|
@@ -68,11 +63,8 @@ export {
|
|
|
68
63
|
type XYPadContextValue,
|
|
69
64
|
type XYPadProps,
|
|
70
65
|
type XYPadMethods,
|
|
71
|
-
type XYPadAreaProps,
|
|
72
66
|
type XYPadThumbProps,
|
|
73
67
|
type XYPadThumbMethods,
|
|
74
|
-
type XY,
|
|
75
|
-
type XYInput,
|
|
76
68
|
} from './components/XYPad'
|
|
77
69
|
|
|
78
70
|
export { type CSSVariables } from './css-variables'
|
|
@@ -85,24 +77,7 @@ export { useDropZone, type UseDropZoneOptions } from './hooks/useDropZone'
|
|
|
85
77
|
export { useEventListener } from './hooks/useEventListener'
|
|
86
78
|
export { useInterval } from './hooks/useInterval'
|
|
87
79
|
export { useLongPress } from './hooks/useLongPress'
|
|
88
|
-
export {
|
|
89
|
-
|
|
90
|
-
NOT_SUPPORTED,
|
|
91
|
-
PERMISSION_DENIED,
|
|
92
|
-
UNAVAILABLE,
|
|
93
|
-
type MIDIAccessError,
|
|
94
|
-
type MIDIAccessOptions,
|
|
95
|
-
} from './hooks/useMIDIAccess'
|
|
96
|
-
export {
|
|
97
|
-
useMIDIInput,
|
|
98
|
-
PITCH_BEND_CENTER,
|
|
99
|
-
type MIDIInputHandlers,
|
|
100
|
-
} from './hooks/useMIDIInput'
|
|
80
|
+
export { useMIDIAccess } from './hooks/useMIDIAccess'
|
|
81
|
+
export { useMIDIInput } from './hooks/useMIDIInput'
|
|
101
82
|
export { useMIDIMessage } from './hooks/useMIDIMessage'
|
|
102
83
|
export { useWheel, type UseWheelOptions } from './hooks/useWheel'
|
|
103
|
-
|
|
104
|
-
export {
|
|
105
|
-
DEFAULT_DRAG_SENSITIVITY,
|
|
106
|
-
DEFAULT_KEYBOARD_OPTIONS,
|
|
107
|
-
DEFAULT_WHEEL_OPTIONS,
|
|
108
|
-
} from './input-event'
|