@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,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AriaAttributes,
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
|
+
CSSProperties,
|
|
4
5
|
forwardRef,
|
|
6
|
+
Ref,
|
|
5
7
|
useImperativeHandle,
|
|
6
8
|
useRef,
|
|
7
|
-
CSSProperties,
|
|
8
9
|
} from 'react'
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { type XYInput } from '@tremolo-ui/dom'
|
|
12
|
+
import { toXY } from '@tremolo-ui/dom/internal'
|
|
11
13
|
|
|
12
14
|
import { useCheckPlacement } from '../_util/Placement'
|
|
13
15
|
import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
@@ -41,6 +43,12 @@ export interface XYPadThumbProps {
|
|
|
41
43
|
'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
|
|
42
44
|
|
|
43
45
|
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Receives `focus` and `blur` for the range inputs inside, which do nothing
|
|
49
|
+
* while the pad is disabled. `ref` reaches the thumb element itself.
|
|
50
|
+
*/
|
|
51
|
+
actionsRef?: Ref<XYPadThumbMethods>
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export interface XYPadThumbMethods {
|
|
@@ -51,7 +59,7 @@ export interface XYPadThumbMethods {
|
|
|
51
59
|
type Props = XYPadThumbProps &
|
|
52
60
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadThumbProps>
|
|
53
61
|
|
|
54
|
-
export const Thumb = /* @__PURE__ */ forwardRef<
|
|
62
|
+
export const Thumb = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
55
63
|
function Thumb(
|
|
56
64
|
{
|
|
57
65
|
color,
|
|
@@ -62,6 +70,7 @@ export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
|
|
|
62
70
|
'aria-labelledby': ariaLabelledby,
|
|
63
71
|
'aria-describedby': ariaDescribedby,
|
|
64
72
|
'aria-valuetext': ariaValuetext,
|
|
73
|
+
actionsRef,
|
|
65
74
|
...props
|
|
66
75
|
},
|
|
67
76
|
forwardedRef,
|
|
@@ -74,7 +83,7 @@ export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
|
|
|
74
83
|
max,
|
|
75
84
|
step,
|
|
76
85
|
disabled,
|
|
77
|
-
|
|
86
|
+
readOnly,
|
|
78
87
|
onChange,
|
|
79
88
|
percent,
|
|
80
89
|
thumbRef,
|
|
@@ -99,15 +108,16 @@ export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
|
|
|
99
108
|
},
|
|
100
109
|
})
|
|
101
110
|
|
|
102
|
-
useImperativeHandle(
|
|
111
|
+
useImperativeHandle(actionsRef, methods, [disabled])
|
|
103
112
|
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
104
113
|
useImperativeHandle(thumbRef, methods, [disabled])
|
|
105
114
|
|
|
106
115
|
return (
|
|
107
116
|
<div
|
|
117
|
+
ref={forwardedRef}
|
|
108
118
|
className={className}
|
|
109
119
|
data-disabled={disabled ? '' : undefined}
|
|
110
|
-
data-readonly={
|
|
120
|
+
data-readonly={readOnly ? '' : undefined}
|
|
111
121
|
{...props}
|
|
112
122
|
style={{
|
|
113
123
|
...{ '--color': color },
|
|
@@ -135,14 +145,14 @@ export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
|
|
|
135
145
|
max={max[axis]}
|
|
136
146
|
step={step[axis]}
|
|
137
147
|
disabled={disabled}
|
|
138
|
-
aria-readonly={
|
|
148
|
+
aria-readonly={readOnly}
|
|
139
149
|
aria-orientation={axis === 0 ? 'horizontal' : 'vertical'}
|
|
140
150
|
aria-label={labels[axis] ?? (axis === 0 ? 'x' : 'y')}
|
|
141
151
|
aria-labelledby={labelledby[axis]}
|
|
142
152
|
aria-describedby={describedby[axis]}
|
|
143
153
|
aria-valuetext={valueText[axis]}
|
|
144
154
|
onChange={(event) => {
|
|
145
|
-
if (
|
|
155
|
+
if (readOnly) {
|
|
146
156
|
event.currentTarget.value = String(value[axis])
|
|
147
157
|
return
|
|
148
158
|
}
|
|
@@ -11,25 +11,29 @@ import {
|
|
|
11
11
|
} from 'react'
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
applyDelta,
|
|
15
|
-
arrowKeyMove,
|
|
16
14
|
type AxisOptions,
|
|
15
|
+
type ChangeSource,
|
|
17
16
|
DEFAULT_DRAG_SENSITIVITY,
|
|
18
17
|
DEFAULT_KEYBOARD_OPTIONS,
|
|
19
18
|
DEFAULT_WHEEL_OPTIONS,
|
|
20
19
|
InputEventOption,
|
|
21
20
|
ModifierState,
|
|
22
21
|
type ModifierValue,
|
|
23
|
-
selectModifier,
|
|
24
|
-
toXY,
|
|
25
22
|
valuePercent,
|
|
26
|
-
wheelMove,
|
|
27
23
|
type XY,
|
|
28
24
|
type XYInput,
|
|
29
25
|
} from '@tremolo-ui/dom'
|
|
26
|
+
import {
|
|
27
|
+
applyDelta,
|
|
28
|
+
arrowKeyMove,
|
|
29
|
+
selectModifier,
|
|
30
|
+
toXY,
|
|
31
|
+
wheelMove,
|
|
32
|
+
} from '@tremolo-ui/dom/internal'
|
|
30
33
|
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
31
34
|
|
|
32
35
|
import { useComposedRefs } from '../../compose-refs'
|
|
36
|
+
import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
|
|
33
37
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
34
38
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
35
39
|
import { useWheel } from '../../hooks/useWheel'
|
|
@@ -38,10 +42,6 @@ import { Area } from './Area'
|
|
|
38
42
|
import { XYPadProvider } from './context'
|
|
39
43
|
import { Thumb, XYPadThumbMethods } from './Thumb'
|
|
40
44
|
|
|
41
|
-
const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
42
|
-
cursor: 'pointer',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
45
|
/**
|
|
46
46
|
* Two-dimensional slider component.
|
|
47
47
|
*
|
|
@@ -134,11 +134,9 @@ export interface XYPadProps {
|
|
|
134
134
|
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
135
135
|
* stays while the pointer is outside the pad.
|
|
136
136
|
*
|
|
137
|
-
* @default
|
|
137
|
+
* @default 'pointer'
|
|
138
138
|
*/
|
|
139
|
-
|
|
140
|
-
cursor?: CSSProperties['cursor']
|
|
141
|
-
}
|
|
139
|
+
dragCursor?: CSSProperties['cursor']
|
|
142
140
|
|
|
143
141
|
/**
|
|
144
142
|
* Make the pad unchangeable and remove its thumb from the tab order.
|
|
@@ -149,14 +147,29 @@ export interface XYPadProps {
|
|
|
149
147
|
* Make the pad unchangeable while leaving its thumb focusable.
|
|
150
148
|
* The parts carry `data-readonly` while it is set.
|
|
151
149
|
*/
|
|
152
|
-
|
|
150
|
+
readOnly?: boolean
|
|
153
151
|
|
|
154
152
|
/** Called with the new value when a drag, the wheel or an arrow key moves it. */
|
|
155
153
|
onChange?: (value: XY<number>) => void
|
|
156
|
-
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Called when a change of the value starts — a press on the area, the first wheel notch
|
|
156
|
+
* or arrow key — with the value
|
|
157
|
+
* before it and what it is made with. A host recording automation can treat
|
|
158
|
+
* the control as touched from here until `onChangeEnd`.
|
|
159
|
+
*/
|
|
160
|
+
onChangeStart?: (value: XY<number>, source: ChangeSource) => void
|
|
161
|
+
/**
|
|
162
|
+
* Called when the change ends, with the value it ended on: on release, or
|
|
163
|
+
* `changeEndDelay` after the last wheel notch or arrow key.
|
|
164
|
+
*/
|
|
165
|
+
onChangeEnd?: (value: XY<number>, source: ChangeSource) => void
|
|
166
|
+
/**
|
|
167
|
+
* How long after the last wheel notch or arrow key the change counts as
|
|
168
|
+
* over, in milliseconds. Neither has an event that says it is done.
|
|
169
|
+
*
|
|
170
|
+
* @default 500
|
|
171
|
+
*/
|
|
172
|
+
changeEndDelay?: number
|
|
160
173
|
|
|
161
174
|
/**
|
|
162
175
|
* The pad renders exactly what you compose here; there is no default
|
|
@@ -170,12 +183,18 @@ export interface XYPadProps {
|
|
|
170
183
|
* </XYPad.Root>
|
|
171
184
|
*/
|
|
172
185
|
children: ReactNode
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Receives `focus` and `blur`, which act on the range inputs inside the thumb —
|
|
189
|
+
* the elements that take the focus — and do nothing while
|
|
190
|
+
* the pad is disabled. `ref` reaches the root element itself.
|
|
191
|
+
*/
|
|
192
|
+
actionsRef?: Ref<XYPadMethods>
|
|
173
193
|
}
|
|
174
194
|
|
|
175
195
|
export interface XYPadMethods {
|
|
176
196
|
focus: () => void
|
|
177
197
|
blur: () => void
|
|
178
|
-
original: Ref<HTMLDivElement>
|
|
179
198
|
}
|
|
180
199
|
|
|
181
200
|
type Props = XYPadProps &
|
|
@@ -187,7 +206,7 @@ type Props = XYPadProps &
|
|
|
187
206
|
*/
|
|
188
207
|
const WHEEL_OPTIONS = { requireFocus: true }
|
|
189
208
|
|
|
190
|
-
export const Root = /* @__PURE__ */ forwardRef<
|
|
209
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
191
210
|
(
|
|
192
211
|
{
|
|
193
212
|
value,
|
|
@@ -201,17 +220,19 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
201
220
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
202
221
|
className,
|
|
203
222
|
style,
|
|
204
|
-
|
|
223
|
+
dragCursor = 'pointer',
|
|
205
224
|
disabled = false,
|
|
206
|
-
|
|
225
|
+
readOnly = false,
|
|
207
226
|
onChange,
|
|
208
|
-
|
|
209
|
-
|
|
227
|
+
onChangeStart,
|
|
228
|
+
onChangeEnd,
|
|
229
|
+
changeEndDelay,
|
|
210
230
|
onPointerDown,
|
|
211
231
|
onKeyDown,
|
|
212
232
|
onFocus,
|
|
213
233
|
onBlur,
|
|
214
234
|
children,
|
|
235
|
+
actionsRef,
|
|
215
236
|
...props
|
|
216
237
|
}: Props,
|
|
217
238
|
forwardedRef,
|
|
@@ -222,8 +243,20 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
222
243
|
const thumbRef = useRef<XYPadThumbMethods>(null)
|
|
223
244
|
|
|
224
245
|
// --- interpret props ---
|
|
225
|
-
const
|
|
226
|
-
|
|
246
|
+
const inactive = disabled || readOnly
|
|
247
|
+
|
|
248
|
+
const gesture = useChangeGesture(
|
|
249
|
+
value,
|
|
250
|
+
{ onChangeStart, onChangeEnd, changeEndDelay },
|
|
251
|
+
inactive,
|
|
252
|
+
)
|
|
253
|
+
const change = useCallback(
|
|
254
|
+
(next: XY<number>) => {
|
|
255
|
+
gesture.changed(next)
|
|
256
|
+
onChange?.(next)
|
|
257
|
+
},
|
|
258
|
+
[gesture, onChange],
|
|
259
|
+
)
|
|
227
260
|
|
|
228
261
|
const min = useMemo(() => toXY(_min), [_min])
|
|
229
262
|
const max = useMemo(() => toXY(_max), [_max])
|
|
@@ -306,9 +339,10 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
306
339
|
event.preventDefault()
|
|
307
340
|
if (!onChange || inactive || !keyboard) return
|
|
308
341
|
const { axis: i, direction } = move
|
|
309
|
-
|
|
342
|
+
gesture.pulse('keyboard')
|
|
343
|
+
change(nudge(i, reverse[i] ? -direction : direction, keyboard, event))
|
|
310
344
|
},
|
|
311
|
-
[onChange, inactive, keyboard, reverse, nudge],
|
|
345
|
+
[onChange, inactive, keyboard, reverse, nudge, gesture, change],
|
|
312
346
|
)
|
|
313
347
|
|
|
314
348
|
// --- hooks ---
|
|
@@ -319,21 +353,18 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
319
353
|
sensitivity: (state) =>
|
|
320
354
|
selectModifier(dragSensitivity, state.event).value,
|
|
321
355
|
updateOnPointerDown: true,
|
|
322
|
-
cursor: inactive ? undefined :
|
|
356
|
+
cursor: inactive ? undefined : dragCursor,
|
|
323
357
|
shouldStart: () => !inactive,
|
|
324
358
|
onChange: (v) => {
|
|
325
359
|
if (inactive) return
|
|
326
|
-
|
|
360
|
+
change(v)
|
|
327
361
|
},
|
|
328
|
-
onDragStart: (
|
|
362
|
+
onDragStart: () => {
|
|
329
363
|
if (inactive) return
|
|
364
|
+
gesture.hold('pointer')
|
|
330
365
|
thumbRef.current?.focus()
|
|
331
|
-
onDragStart?.(v)
|
|
332
|
-
},
|
|
333
|
-
onDragEnd: (v) => {
|
|
334
|
-
if (inactive) return
|
|
335
|
-
onDragEnd?.(v)
|
|
336
366
|
},
|
|
367
|
+
onDragEnd: () => gesture.end(),
|
|
337
368
|
})
|
|
338
369
|
|
|
339
370
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
@@ -342,12 +373,14 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
342
373
|
if (!move) return
|
|
343
374
|
event.preventDefault()
|
|
344
375
|
const { axis: i, direction } = move
|
|
345
|
-
|
|
376
|
+
gesture.pulse('wheel')
|
|
377
|
+
change(nudge(i, reverse[i] ? -direction : direction, wheel, event))
|
|
346
378
|
}, WHEEL_OPTIONS)
|
|
347
379
|
|
|
348
380
|
// Composed once, so React attaches the refs a single time instead of
|
|
349
381
|
// detaching and re-attaching on every render.
|
|
350
382
|
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
383
|
+
forwardedRef,
|
|
351
384
|
rootRef,
|
|
352
385
|
dragRefCallback,
|
|
353
386
|
wheelRefCallback,
|
|
@@ -362,7 +395,7 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
362
395
|
scale,
|
|
363
396
|
reverse,
|
|
364
397
|
disabled,
|
|
365
|
-
|
|
398
|
+
readOnly,
|
|
366
399
|
onChange,
|
|
367
400
|
percent,
|
|
368
401
|
areaRef,
|
|
@@ -376,13 +409,13 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
376
409
|
scale,
|
|
377
410
|
reverse,
|
|
378
411
|
disabled,
|
|
379
|
-
|
|
412
|
+
readOnly,
|
|
380
413
|
onChange,
|
|
381
414
|
percent,
|
|
382
415
|
],
|
|
383
416
|
)
|
|
384
417
|
|
|
385
|
-
useImperativeHandle(
|
|
418
|
+
useImperativeHandle(actionsRef, () => {
|
|
386
419
|
return {
|
|
387
420
|
focus() {
|
|
388
421
|
if (!disabled) thumbRef.current?.focus()
|
|
@@ -390,7 +423,6 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
390
423
|
blur() {
|
|
391
424
|
thumbRef.current?.blur()
|
|
392
425
|
},
|
|
393
|
-
original: rootRef,
|
|
394
426
|
}
|
|
395
427
|
}, [disabled])
|
|
396
428
|
|
|
@@ -403,7 +435,7 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
|
403
435
|
role="group"
|
|
404
436
|
tabIndex={-1}
|
|
405
437
|
data-disabled={disabled ? '' : undefined}
|
|
406
|
-
data-readonly={
|
|
438
|
+
data-readonly={readOnly ? '' : undefined}
|
|
407
439
|
style={style}
|
|
408
440
|
onPointerDown={onPointerDown}
|
|
409
441
|
onKeyDown={(event) => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useInsertionEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createChangeGesture,
|
|
12
|
+
type ChangeGestureInstance,
|
|
13
|
+
type ChangeSource,
|
|
14
|
+
} from '@tremolo-ui/dom'
|
|
15
|
+
|
|
16
|
+
export interface ChangeGestureProps<V> {
|
|
17
|
+
onChangeStart?: (value: V, source: ChangeSource) => void
|
|
18
|
+
onChangeEnd?: (value: V, source: ChangeSource) => void
|
|
19
|
+
changeEndDelay?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ChangeGesture<V> = Pick<
|
|
23
|
+
ChangeGestureInstance,
|
|
24
|
+
'hold' | 'pulse' | 'instant' | 'end' | 'active'
|
|
25
|
+
> & {
|
|
26
|
+
/**
|
|
27
|
+
* Record a value the control has just reported through `onChange`, for
|
|
28
|
+
* `onChangeEnd` to give. The prop lags behind until the parent renders, and
|
|
29
|
+
* a drag ends in the same event as its last change.
|
|
30
|
+
*/
|
|
31
|
+
changed: (value: V) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The core's change gesture, reporting the control's value to
|
|
36
|
+
* `onChangeStart` / `onChangeEnd`. The start gets the value before the change,
|
|
37
|
+
* the end the last one reported.
|
|
38
|
+
*
|
|
39
|
+
* Internal
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
export function useChangeGesture<V>(
|
|
43
|
+
value: V,
|
|
44
|
+
{ onChangeStart, onChangeEnd, changeEndDelay }: ChangeGestureProps<V>,
|
|
45
|
+
inactive: boolean,
|
|
46
|
+
): ChangeGesture<V> {
|
|
47
|
+
const [instance] = useState(() => createChangeGesture())
|
|
48
|
+
const latest = useRef({ value, onChangeStart, onChangeEnd })
|
|
49
|
+
// What `onChangeEnd` gives: the value at the start, then each one reported.
|
|
50
|
+
const lastValue = useRef(value)
|
|
51
|
+
|
|
52
|
+
useInsertionEffect(() => {
|
|
53
|
+
latest.current = { value, onChangeStart, onChangeEnd }
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// The callbacks are handed over here rather than at creation: they read the
|
|
57
|
+
// refs, which belong outside render. Nothing can start a gesture before the
|
|
58
|
+
// element is mounted, so they are in place by the first event.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
instance.update({
|
|
61
|
+
endDelay: changeEndDelay,
|
|
62
|
+
onStart: (source) => {
|
|
63
|
+
lastValue.current = latest.current.value
|
|
64
|
+
latest.current.onChangeStart?.(latest.current.value, source)
|
|
65
|
+
},
|
|
66
|
+
onEnd: (source) =>
|
|
67
|
+
latest.current.onChangeEnd?.(lastValue.current, source),
|
|
68
|
+
})
|
|
69
|
+
}, [instance, changeEndDelay])
|
|
70
|
+
|
|
71
|
+
// A control that turns inactive mid-gesture, or goes away, should not leave
|
|
72
|
+
// a host thinking it is still being touched.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (inactive) instance.end()
|
|
75
|
+
}, [instance, inactive])
|
|
76
|
+
useEffect(() => () => instance.end(), [instance])
|
|
77
|
+
|
|
78
|
+
const changed = useCallback((next: V) => {
|
|
79
|
+
lastValue.current = next
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
return useMemo(
|
|
83
|
+
() => ({
|
|
84
|
+
hold: instance.hold,
|
|
85
|
+
pulse: instance.pulse,
|
|
86
|
+
instant: instance.instant,
|
|
87
|
+
end: instance.end,
|
|
88
|
+
active: instance.active,
|
|
89
|
+
changed,
|
|
90
|
+
}),
|
|
91
|
+
[instance, changed],
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
|
|
3
|
-
import { checkSteps, type CheckStepsOptions } from '@tremolo-ui/dom'
|
|
3
|
+
import { checkSteps, type CheckStepsOptions } from '@tremolo-ui/dom/internal'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
4
4
|
|
|
5
|
+
export interface UseAnimationFrameOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Stop the loop. Turning it back on starts a new one, whose first frame has
|
|
8
|
+
* a `deltaTime` of `0`.
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Call `callback` on every animation frame for as long as the component is
|
|
16
|
+
* mounted.
|
|
17
|
+
*
|
|
18
|
+
* The callback is read on every frame, so it can be written inline and see
|
|
19
|
+
* the latest render without restarting the loop.
|
|
20
|
+
*
|
|
21
|
+
* @param callback receives the frame's timestamp and the milliseconds since
|
|
22
|
+
* the previous frame, which is `0` on the first one
|
|
23
|
+
*/
|
|
5
24
|
export function useAnimationFrame(
|
|
6
|
-
callback
|
|
7
|
-
|
|
25
|
+
callback: (timestamp: DOMHighResTimeStamp, deltaTime: number) => void,
|
|
26
|
+
{ disabled = false }: UseAnimationFrameOptions = {},
|
|
8
27
|
) {
|
|
9
|
-
const reqIdRef = useRef(-1)
|
|
10
28
|
// Read through a ref rather than depended on: `useAnimationFrame(() => ...)`
|
|
11
29
|
// is a new function on every render, and a callback that renders would then
|
|
12
|
-
// cancel and re-schedule its own loop on every frame.
|
|
13
|
-
// is the caller's `deps`, and nothing else.
|
|
30
|
+
// cancel and re-schedule its own loop on every frame.
|
|
14
31
|
const runCallback = useCallbackRef(callback)
|
|
15
32
|
|
|
16
33
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
if (disabled) return
|
|
35
|
+
|
|
36
|
+
let frameId = 0
|
|
37
|
+
let previous: DOMHighResTimeStamp | undefined
|
|
38
|
+
|
|
39
|
+
// Scheduled before calling back, so that a slow callback does not delay
|
|
40
|
+
// the next request.
|
|
41
|
+
const loop = (timestamp: DOMHighResTimeStamp) => {
|
|
42
|
+
frameId = requestAnimationFrame(loop)
|
|
43
|
+
runCallback(timestamp, previous === undefined ? 0 : timestamp - previous)
|
|
44
|
+
previous = timestamp
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
return () => cancelAnimationFrame(
|
|
26
|
-
|
|
27
|
-
}, [runCallback, ...deps])
|
|
47
|
+
frameId = requestAnimationFrame(loop)
|
|
48
|
+
return () => cancelAnimationFrame(frameId)
|
|
49
|
+
}, [runCallback, disabled])
|
|
28
50
|
}
|
|
@@ -2,52 +2,123 @@ import { useCallback, useEffect, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* What {@link useEventListener} listens on. A function is called after every
|
|
7
|
+
* render, so it can return an element out of a ref; `null` listens on nothing.
|
|
8
|
+
*/
|
|
9
|
+
export type UseEventListenerTarget =
|
|
10
|
+
| EventTarget
|
|
11
|
+
| null
|
|
12
|
+
| (() => EventTarget | null)
|
|
7
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The options of `addEventListener`, as a boolean for `capture` or as an
|
|
16
|
+
* object. `signal` is left out: the hook already owns when the listener goes.
|
|
17
|
+
*/
|
|
18
|
+
export type UseEventListenerOptions =
|
|
19
|
+
| boolean
|
|
20
|
+
| Pick<AddEventListenerOptions, 'capture' | 'once' | 'passive'>
|
|
21
|
+
|
|
22
|
+
interface Registration {
|
|
23
|
+
node: EventTarget
|
|
24
|
+
event: string
|
|
25
|
+
capture: boolean
|
|
26
|
+
once: boolean | undefined
|
|
27
|
+
passive: boolean | undefined
|
|
28
|
+
remove: VoidFunction
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Listen to an event on a target for as long as the component is mounted.
|
|
33
|
+
*
|
|
34
|
+
* The handler is read on every event, and the target and the options are
|
|
35
|
+
* compared by what they resolve to, so all three can be written inline. The
|
|
36
|
+
* listener is re-attached only when the element, the event or one of the
|
|
37
|
+
* options actually changes.
|
|
38
|
+
*
|
|
39
|
+
* @returns a function that removes the listener. It stays removed until the
|
|
40
|
+
* target, the event or the options change.
|
|
41
|
+
*/
|
|
8
42
|
export function useEventListener<K extends keyof DocumentEventMap>(
|
|
9
|
-
target:
|
|
43
|
+
target: UseEventListenerTarget,
|
|
10
44
|
event: K,
|
|
11
45
|
handler: (event: DocumentEventMap[K]) => void,
|
|
12
|
-
options?:
|
|
46
|
+
options?: UseEventListenerOptions,
|
|
13
47
|
): VoidFunction
|
|
14
48
|
export function useEventListener<K extends keyof WindowEventMap>(
|
|
15
|
-
target:
|
|
49
|
+
target: UseEventListenerTarget,
|
|
16
50
|
event: K,
|
|
17
51
|
handler: (event: WindowEventMap[K]) => void,
|
|
18
|
-
options?:
|
|
52
|
+
options?: UseEventListenerOptions,
|
|
19
53
|
): VoidFunction
|
|
20
54
|
export function useEventListener<K extends keyof GlobalEventHandlersEventMap>(
|
|
21
|
-
target:
|
|
55
|
+
target: UseEventListenerTarget,
|
|
22
56
|
event: K,
|
|
23
57
|
handler: (event: GlobalEventHandlersEventMap[K]) => void,
|
|
24
|
-
options?:
|
|
58
|
+
options?: UseEventListenerOptions,
|
|
25
59
|
): VoidFunction
|
|
26
60
|
export function useEventListener(
|
|
27
|
-
target:
|
|
61
|
+
target: UseEventListenerTarget,
|
|
28
62
|
event: string,
|
|
29
63
|
handler: (event: Event) => void,
|
|
30
|
-
options?:
|
|
64
|
+
options?: UseEventListenerOptions,
|
|
31
65
|
) {
|
|
32
66
|
const listener = useCallbackRef(handler)
|
|
33
|
-
const
|
|
67
|
+
const registrationRef = useRef<Registration | null>(null)
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
capture = false,
|
|
71
|
+
once,
|
|
72
|
+
passive,
|
|
73
|
+
} = typeof options === 'boolean' ? { capture: options } : (options ?? {})
|
|
34
74
|
|
|
75
|
+
// No dependency list: an inline target or options object is a new value on
|
|
76
|
+
// every render, so what decides a re-attach is the comparison below, not
|
|
77
|
+
// their identity.
|
|
35
78
|
useEffect(() => {
|
|
36
|
-
const node = typeof target === 'function' ? target() :
|
|
79
|
+
const node = typeof target === 'function' ? target() : target
|
|
80
|
+
const current = registrationRef.current
|
|
37
81
|
|
|
82
|
+
if (
|
|
83
|
+
current &&
|
|
84
|
+
current.node === node &&
|
|
85
|
+
current.event === event &&
|
|
86
|
+
current.capture === capture &&
|
|
87
|
+
current.once === once &&
|
|
88
|
+
current.passive === passive
|
|
89
|
+
) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
current?.remove()
|
|
94
|
+
registrationRef.current = null
|
|
38
95
|
if (!node) return
|
|
39
96
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
const listenerOptions = { capture, once, passive }
|
|
98
|
+
node.addEventListener(event, listener, listenerOptions)
|
|
99
|
+
registrationRef.current = {
|
|
100
|
+
node,
|
|
101
|
+
event,
|
|
102
|
+
capture,
|
|
103
|
+
once,
|
|
104
|
+
passive,
|
|
105
|
+
remove: () => node.removeEventListener(event, listener, listenerOptions),
|
|
43
106
|
}
|
|
44
|
-
|
|
107
|
+
})
|
|
45
108
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
109
|
+
useEffect(
|
|
110
|
+
() => () => {
|
|
111
|
+
registrationRef.current?.remove()
|
|
112
|
+
registrationRef.current = null
|
|
113
|
+
},
|
|
114
|
+
[],
|
|
115
|
+
)
|
|
51
116
|
|
|
52
|
-
|
|
117
|
+
// Kept registered, so a render with the same target does not attach again.
|
|
118
|
+
return useCallback(() => {
|
|
119
|
+
const current = registrationRef.current
|
|
120
|
+
if (!current) return
|
|
121
|
+
current.remove()
|
|
122
|
+
current.remove = () => {}
|
|
123
|
+
}, [])
|
|
53
124
|
}
|