@tremolo-ui/react 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +666 -1171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -315
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +316 -315
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +643 -1123
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/AnimationCanvas/index.tsx +4 -18
- package/src/components/DropZone/index.tsx +2 -1
- package/src/components/FileInput/index.tsx +11 -13
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +21 -20
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +99 -52
- package/src/components/NumberInput/InputField.tsx +20 -33
- package/src/components/NumberInput/Stepper.tsx +76 -100
- package/src/components/NumberInput/StepperButton.tsx +15 -8
- package/src/components/NumberInput/context.tsx +26 -3
- package/src/components/NumberInput/index.tsx +175 -91
- package/src/components/Piano/index.tsx +54 -144
- package/src/components/PointsEditor/Container.tsx +15 -26
- package/src/components/PointsEditor/Point.tsx +94 -110
- package/src/components/PointsEditor/context.tsx +8 -60
- package/src/components/PointsEditor/index.tsx +41 -324
- package/src/components/Slider/Marks.tsx +5 -6
- package/src/components/Slider/MarksOption.tsx +9 -19
- package/src/components/Slider/Thumb.tsx +20 -11
- package/src/components/Slider/Track.tsx +16 -13
- package/src/components/Slider/context.tsx +3 -3
- package/src/components/Slider/index.tsx +110 -72
- package/src/components/XYPad/Area.tsx +27 -49
- package/src/components/XYPad/Thumb.tsx +20 -8
- package/src/components/XYPad/context.tsx +2 -4
- package/src/components/XYPad/index.tsx +102 -83
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useChangeGesture.ts +93 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -146
- package/src/hooks/useAnimationFrame.ts +37 -15
- package/src/hooks/useEventListener.ts +93 -22
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -4
- package/src/index.ts +11 -32
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -46
- package/src/components/_util/css-length.ts +0 -13
- package/src/components/_util/decimal-digits.ts +0 -23
- package/src/components/_util/visually-hidden.ts +0 -25
- package/src/hooks/useInterval.ts +0 -27
- package/src/input-event.ts +0 -32
|
@@ -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
|
}
|
|
@@ -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/hooks/useWheel.ts
CHANGED
|
@@ -4,9 +4,8 @@ import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
|
|
|
4
4
|
|
|
5
5
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
6
|
|
|
7
|
-
// `onWheel` is left out:
|
|
8
|
-
// the
|
|
9
|
-
// option would be silently ignored.
|
|
7
|
+
// `onWheel` is left out: this hook takes the handler as its first argument, so
|
|
8
|
+
// the option would be a second way to say the same thing.
|
|
10
9
|
export interface UseWheelOptions extends Omit<WheelOptions, 'onWheel'> {
|
|
11
10
|
/**
|
|
12
11
|
* Listen on this element rather than on the one the returned ref callback is
|
|
@@ -43,7 +42,8 @@ export function useWheel<T extends Element>(
|
|
|
43
42
|
const element = target ? target.current : node
|
|
44
43
|
if (!element) return
|
|
45
44
|
|
|
46
|
-
const instance = createWheel(element,
|
|
45
|
+
const instance = createWheel(element, {
|
|
46
|
+
onWheel: (event) => wheelHandler(event),
|
|
47
47
|
requireFocus,
|
|
48
48
|
})
|
|
49
49
|
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,6 @@ export {
|
|
|
4
4
|
type AnimationCanvasFixedProps,
|
|
5
5
|
type AnimationCanvasProps,
|
|
6
6
|
type AnimationCanvasResizableProps,
|
|
7
|
-
type DrawFunction,
|
|
8
|
-
type InitFunction,
|
|
9
7
|
} from './components/AnimationCanvas'
|
|
10
8
|
export { DropZone, type DropZoneProps } from './components/DropZone'
|
|
11
9
|
export {
|
|
@@ -33,28 +31,23 @@ export {
|
|
|
33
31
|
} from './components/NumberInput'
|
|
34
32
|
export {
|
|
35
33
|
Piano,
|
|
36
|
-
SHORTCUTS,
|
|
37
34
|
type PianoProps,
|
|
38
35
|
type PianoMethods,
|
|
39
36
|
type KeyState,
|
|
40
37
|
type KeyAttributes,
|
|
41
|
-
type KeyboardShortcuts,
|
|
42
38
|
} from './components/Piano'
|
|
43
39
|
export {
|
|
44
40
|
PointsEditor,
|
|
45
41
|
usePointsEditorContext,
|
|
46
|
-
clampPoint,
|
|
47
42
|
type PointsEditorProps,
|
|
48
43
|
type PointsEditorContextValue,
|
|
49
44
|
type PointsEditorPointProps,
|
|
50
|
-
type PointBaseType,
|
|
51
45
|
} from './components/PointsEditor'
|
|
52
46
|
export {
|
|
53
47
|
Slider,
|
|
54
48
|
useSliderContext,
|
|
55
49
|
type SliderContextValue,
|
|
56
50
|
type SliderMarksOptionProps,
|
|
57
|
-
type MarksOptions,
|
|
58
51
|
type SliderMarksProps,
|
|
59
52
|
type SliderMethods,
|
|
60
53
|
type SliderProps,
|
|
@@ -68,41 +61,27 @@ export {
|
|
|
68
61
|
type XYPadContextValue,
|
|
69
62
|
type XYPadProps,
|
|
70
63
|
type XYPadMethods,
|
|
71
|
-
type XYPadAreaProps,
|
|
72
64
|
type XYPadThumbProps,
|
|
73
65
|
type XYPadThumbMethods,
|
|
74
|
-
type XY,
|
|
75
|
-
type XYInput,
|
|
76
66
|
} from './components/XYPad'
|
|
77
67
|
|
|
78
68
|
export { type CSSVariables } from './css-variables'
|
|
79
69
|
|
|
80
70
|
// hooks
|
|
81
|
-
export {
|
|
71
|
+
export {
|
|
72
|
+
useAnimationFrame,
|
|
73
|
+
type UseAnimationFrameOptions,
|
|
74
|
+
} from './hooks/useAnimationFrame'
|
|
82
75
|
export { useDrag, type UseDragOptions } from './hooks/useDrag'
|
|
83
76
|
export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
|
|
84
77
|
export { useDropZone, type UseDropZoneOptions } from './hooks/useDropZone'
|
|
85
|
-
export { useEventListener } from './hooks/useEventListener'
|
|
86
|
-
export { useInterval } from './hooks/useInterval'
|
|
87
|
-
export { useLongPress } from './hooks/useLongPress'
|
|
88
|
-
export {
|
|
89
|
-
useMIDIAccess,
|
|
90
|
-
NOT_SUPPORTED,
|
|
91
|
-
PERMISSION_DENIED,
|
|
92
|
-
UNAVAILABLE,
|
|
93
|
-
type MIDIAccessError,
|
|
94
|
-
type MIDIAccessOptions,
|
|
95
|
-
} from './hooks/useMIDIAccess'
|
|
96
78
|
export {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
type
|
|
100
|
-
} from './hooks/
|
|
79
|
+
useEventListener,
|
|
80
|
+
type UseEventListenerOptions,
|
|
81
|
+
type UseEventListenerTarget,
|
|
82
|
+
} from './hooks/useEventListener'
|
|
83
|
+
export { useLongPress } from './hooks/useLongPress'
|
|
84
|
+
export { useMIDIAccess } from './hooks/useMIDIAccess'
|
|
85
|
+
export { useMIDIInput } from './hooks/useMIDIInput'
|
|
101
86
|
export { useMIDIMessage } from './hooks/useMIDIMessage'
|
|
102
87
|
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'
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
export type KeyboardShortcuts = {
|
|
2
|
-
/**
|
|
3
|
-
* Keys laid out from `noteRange.first`, one entry per semitone.
|
|
4
|
-
*
|
|
5
|
-
* An empty string leaves that note without a shortcut: `KeyboardEvent.key` is
|
|
6
|
-
* never empty, so the entry can never match. Use it to skip the black keys
|
|
7
|
-
* (see {@link SHORTCUTS.HOME_ROW_NATURAL}) and keep the remaining entries
|
|
8
|
-
* lined up with the notes.
|
|
9
|
-
*/
|
|
10
|
-
keys: string[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
15
|
-
*/
|
|
16
|
-
export const SHORTCUTS = {
|
|
17
|
-
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
18
|
-
HOME_ROW: {
|
|
19
|
-
keys: [
|
|
20
|
-
'a',
|
|
21
|
-
'w',
|
|
22
|
-
's',
|
|
23
|
-
'e',
|
|
24
|
-
'd',
|
|
25
|
-
'f',
|
|
26
|
-
't',
|
|
27
|
-
'g',
|
|
28
|
-
'y',
|
|
29
|
-
'h',
|
|
30
|
-
'u',
|
|
31
|
-
'j',
|
|
32
|
-
'k',
|
|
33
|
-
'o',
|
|
34
|
-
'l',
|
|
35
|
-
'p',
|
|
36
|
-
';',
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
40
|
-
HOME_ROW_NATURAL: {
|
|
41
|
-
keys: [
|
|
42
|
-
'a',
|
|
43
|
-
'',
|
|
44
|
-
's',
|
|
45
|
-
'',
|
|
46
|
-
'd',
|
|
47
|
-
'f',
|
|
48
|
-
'',
|
|
49
|
-
'g',
|
|
50
|
-
'',
|
|
51
|
-
'h',
|
|
52
|
-
'',
|
|
53
|
-
'j',
|
|
54
|
-
'k',
|
|
55
|
-
'',
|
|
56
|
-
'l',
|
|
57
|
-
'',
|
|
58
|
-
';',
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { toFixed } from '@tremolo-ui/functions'
|
|
2
|
-
|
|
3
|
-
import { decimalDigits } from '../_util/decimal-digits'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* How `Slider.Marks` fills itself in when it is given no children: one option
|
|
7
|
-
* every `per`, or every `step` of the slider. The object form turns off the
|
|
8
|
-
* mark or the label for the whole set; a single option is customized by
|
|
9
|
-
* writing `Slider.MarksOption` out instead.
|
|
10
|
-
*/
|
|
11
|
-
export type MarksOptions =
|
|
12
|
-
| 'step'
|
|
13
|
-
| number
|
|
14
|
-
| {
|
|
15
|
-
per: 'step' | number
|
|
16
|
-
mark?: boolean
|
|
17
|
-
label?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function generateOptionsList(
|
|
21
|
-
options: MarksOptions,
|
|
22
|
-
min: number,
|
|
23
|
-
max: number,
|
|
24
|
-
step: number,
|
|
25
|
-
) {
|
|
26
|
-
const {
|
|
27
|
-
per,
|
|
28
|
-
mark = true,
|
|
29
|
-
label = true,
|
|
30
|
-
} = typeof options === 'object' ? options : { per: options }
|
|
31
|
-
const optionsList: {
|
|
32
|
-
value: number
|
|
33
|
-
mark: boolean
|
|
34
|
-
label: boolean
|
|
35
|
-
}[] = []
|
|
36
|
-
const interval = per === 'step' ? step : per
|
|
37
|
-
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1
|
|
38
|
-
for (let i = 0; i < count; i++) {
|
|
39
|
-
const value = toFixed(
|
|
40
|
-
interval * (Math.ceil(min / interval) + i),
|
|
41
|
-
decimalDigits(interval),
|
|
42
|
-
)
|
|
43
|
-
optionsList.push({ value: value, mark: mark, label: label })
|
|
44
|
-
}
|
|
45
|
-
return optionsList
|
|
46
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A number as a CSS length, for a custom property.
|
|
3
|
-
*
|
|
4
|
-
* React appends `px` to a bare number for the properties it knows take a
|
|
5
|
-
* length, and a custom property is never one of them: `--size: 50` would come
|
|
6
|
-
* out as the invalid `50`. Anything already a string is passed through, so
|
|
7
|
-
* `'3rem'` and `'100%'` still work.
|
|
8
|
-
*/
|
|
9
|
-
export function cssLength(
|
|
10
|
-
value: number | string | undefined,
|
|
11
|
-
): string | undefined {
|
|
12
|
-
return typeof value === 'number' ? `${value}px` : value
|
|
13
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How many digits a number carries after the decimal point.
|
|
3
|
-
*
|
|
4
|
-
* `Slider.Marks` builds its labels by multiplying an interval, which leaves
|
|
5
|
-
* binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
|
|
6
|
-
* count is what tells `toFixed` how far to round that back.
|
|
7
|
-
*
|
|
8
|
-
* The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
|
|
9
|
-
* which has no decimal point at all, so reading the text after the point
|
|
10
|
-
* reports no digits and the interval rounds away to whole numbers.
|
|
11
|
-
*/
|
|
12
|
-
export function decimalDigits(x: number): number {
|
|
13
|
-
if (!Number.isFinite(x)) return 0
|
|
14
|
-
|
|
15
|
-
const text = String(x)
|
|
16
|
-
const e = text.indexOf('e')
|
|
17
|
-
if (e === -1) return text.split('.')[1]?.length ?? 0
|
|
18
|
-
|
|
19
|
-
const fraction = text.slice(0, e).split('.')[1]?.length ?? 0
|
|
20
|
-
// A negative exponent pushes the point further right, a positive one pulls
|
|
21
|
-
// it back past the digits that are there.
|
|
22
|
-
return Math.max(0, fraction - Number(text.slice(e + 1)))
|
|
23
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from 'react'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Take an element out of sight while leaving it in the accessibility tree and
|
|
5
|
-
* in the tab order.
|
|
6
|
-
*
|
|
7
|
-
* `display: none` and `visibility: hidden` would remove it from both, and the
|
|
8
|
-
* native control underneath a headless component is what carries the ARIA and
|
|
9
|
-
* the keyboard behaviour. `pointer-events: none` is safe because nothing is
|
|
10
|
-
* ever clicked here directly: a `<label>` forwards its click, and a drag is
|
|
11
|
-
* handled by the part that is visible.
|
|
12
|
-
*/
|
|
13
|
-
export const visuallyHiddenStyle: CSSProperties = {
|
|
14
|
-
position: 'absolute',
|
|
15
|
-
width: 1,
|
|
16
|
-
height: 1,
|
|
17
|
-
padding: 0,
|
|
18
|
-
margin: -1,
|
|
19
|
-
overflow: 'hidden',
|
|
20
|
-
clip: 'rect(0, 0, 0, 0)',
|
|
21
|
-
clipPath: 'inset(50%)',
|
|
22
|
-
whiteSpace: 'nowrap',
|
|
23
|
-
border: 0,
|
|
24
|
-
pointerEvents: 'none',
|
|
25
|
-
}
|
package/src/hooks/useInterval.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react'
|
|
2
|
-
|
|
3
|
-
export function useInterval(callback: () => void, delay: number | null) {
|
|
4
|
-
const savedCallback = useRef(callback)
|
|
5
|
-
|
|
6
|
-
// Remember the latest callback if it changes.
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
savedCallback.current = callback
|
|
9
|
-
}, [callback])
|
|
10
|
-
|
|
11
|
-
// Set up the interval.
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
// Don't schedule if no delay is specified.
|
|
14
|
-
// Note: 0 is a valid value for delay.
|
|
15
|
-
if (delay === null) {
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const id = setInterval(() => {
|
|
20
|
-
savedCallback.current()
|
|
21
|
-
}, delay)
|
|
22
|
-
|
|
23
|
-
return () => {
|
|
24
|
-
clearInterval(id)
|
|
25
|
-
}
|
|
26
|
-
}, [delay])
|
|
27
|
-
}
|
package/src/input-event.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { type InputEventOption, type ModifierValue } from '@tremolo-ui/dom'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
5
|
-
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
6
|
-
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
7
|
-
* `useCheckSteps` warns about.
|
|
8
|
-
*
|
|
9
|
-
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
10
|
-
* amount move at all — see `applyDelta` in `@tremolo-ui/dom`.
|
|
11
|
-
*/
|
|
12
|
-
export const DEFAULT_KEYBOARD_OPTIONS: ModifierValue<InputEventOption> = {
|
|
13
|
-
default: ['raw', 1],
|
|
14
|
-
shift: ['raw', 0.1],
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
19
|
-
*
|
|
20
|
-
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
21
|
-
* and fills `deltaX`, so no modifier is bound here.
|
|
22
|
-
*/
|
|
23
|
-
export const DEFAULT_WHEEL_OPTIONS: ModifierValue<InputEventOption> = ['raw', 1]
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The drag sensitivity used by value controls by default. Shift makes the
|
|
27
|
-
* same movement cover a tenth of the range, matching the arrow keys.
|
|
28
|
-
*/
|
|
29
|
-
export const DEFAULT_DRAG_SENSITIVITY: ModifierValue<number> = {
|
|
30
|
-
default: 1,
|
|
31
|
-
shift: 0.1,
|
|
32
|
-
}
|