@tremolo-ui/react 0.4.0 → 0.5.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 +1125 -1437
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +561 -243
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +561 -243
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1125 -1438
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
- package/src/components/AnimationCanvas/index.tsx +84 -155
- package/src/components/Knob/ActiveLine.tsx +6 -6
- package/src/components/Knob/InactiveLine.tsx +9 -11
- package/src/components/Knob/SVGRoot.tsx +9 -18
- package/src/components/Knob/context.tsx +57 -89
- package/src/components/Knob/index.css +6 -2
- package/src/components/Knob/index.tsx +90 -61
- package/src/components/NumberInput/DecrementStepper.tsx +17 -50
- package/src/components/NumberInput/IncrementStepper.tsx +17 -50
- package/src/components/NumberInput/InputField.tsx +123 -0
- package/src/components/NumberInput/Stepper.tsx +90 -12
- package/src/components/NumberInput/context.tsx +74 -146
- package/src/components/NumberInput/index.css +38 -102
- package/src/components/NumberInput/index.tsx +252 -98
- package/src/components/NumberInput/stepperButton.tsx +90 -0
- package/src/components/Piano/index.tsx +272 -288
- package/src/components/Piano/keyboardShortcuts.ts +34 -4
- package/src/components/PointsEditor/Background.tsx +8 -2
- package/src/components/PointsEditor/Container.tsx +19 -9
- package/src/components/PointsEditor/Point.tsx +104 -24
- package/src/components/PointsEditor/context.tsx +28 -74
- package/src/components/PointsEditor/index.css +5 -0
- package/src/components/PointsEditor/index.tsx +77 -43
- package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
- package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
- package/src/components/Slider/Thumb.tsx +55 -68
- package/src/components/Slider/Track.tsx +15 -15
- package/src/components/Slider/context.tsx +36 -51
- package/src/components/Slider/index.css +18 -5
- package/src/components/Slider/index.tsx +127 -146
- package/src/components/Slider/type.ts +4 -4
- package/src/components/XYPad/Area.tsx +16 -7
- package/src/components/XYPad/Thumb.tsx +62 -74
- package/src/components/XYPad/context.tsx +46 -0
- package/src/components/XYPad/index.css +12 -0
- package/src/components/XYPad/index.tsx +190 -230
- package/src/hooks/useDrag.ts +1 -1
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +29 -9
- package/src/index.ts +15 -9
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -100
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { RefObject, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createDragValue,
|
|
5
|
+
elementMapping,
|
|
6
|
+
relativeMapping,
|
|
7
|
+
type AxisOptions,
|
|
8
|
+
type DragValueInstance,
|
|
9
|
+
type XY,
|
|
10
|
+
type XYInput,
|
|
11
|
+
} from '@tremolo-ui/dom'
|
|
12
|
+
|
|
13
|
+
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
14
|
+
|
|
15
|
+
export interface UseDragValueOptions {
|
|
16
|
+
/** Scaling of each axis; a single value applies to both. */
|
|
17
|
+
axis: XYInput<AxisOptions>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize the pointer against the bounding rect of this element, so that
|
|
21
|
+
* the value *is* the position pointed at.
|
|
22
|
+
*
|
|
23
|
+
* Give either this or `getValue`.
|
|
24
|
+
*/
|
|
25
|
+
baseElementRef?: RefObject<Element | null>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Move the value away from where it stood when the drag started, by the
|
|
29
|
+
* distance dragged. The pointer position itself carries no meaning.
|
|
30
|
+
*
|
|
31
|
+
* Give either this or `baseElementRef`.
|
|
32
|
+
*/
|
|
33
|
+
getValue?: () => XY<number>
|
|
34
|
+
/**
|
|
35
|
+
* Pixels of movement that span the whole range, with `getValue`.
|
|
36
|
+
* @default 100
|
|
37
|
+
*/
|
|
38
|
+
pixelRange?: XYInput<number>
|
|
39
|
+
|
|
40
|
+
/** @see DragValueOptions.updateOnPointerDown */
|
|
41
|
+
updateOnPointerDown?: boolean
|
|
42
|
+
/** @default 0 */
|
|
43
|
+
threshold?: number
|
|
44
|
+
/** CSS cursor to show while dragging. Applied to the element itself. */
|
|
45
|
+
cursor?: string
|
|
46
|
+
|
|
47
|
+
onChange?: (value: XY<number>) => void
|
|
48
|
+
onDragStart?: (value: XY<number>) => void
|
|
49
|
+
onDragEnd?: (value: XY<number>) => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Drive a value with a pointer drag.
|
|
54
|
+
*
|
|
55
|
+
* @returns a ref callback for the element that starts the drag, and whether a drag is in progress
|
|
56
|
+
*/
|
|
57
|
+
export function useDragValue<T extends Element>(
|
|
58
|
+
options: UseDragValueOptions,
|
|
59
|
+
): { refCallback: (node: T | null) => void; dragging: boolean } {
|
|
60
|
+
const {
|
|
61
|
+
axis,
|
|
62
|
+
baseElementRef,
|
|
63
|
+
pixelRange,
|
|
64
|
+
updateOnPointerDown,
|
|
65
|
+
threshold,
|
|
66
|
+
cursor,
|
|
67
|
+
} = options
|
|
68
|
+
|
|
69
|
+
if (!baseElementRef && !options.getValue) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'useDragValue: give either baseElementRef or getValue, so that the drag has something to move',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [dragging, setDragging] = useState(false)
|
|
76
|
+
|
|
77
|
+
const valueGetter = useCallbackRef(options.getValue)
|
|
78
|
+
const changeHandler = useCallbackRef(options.onChange)
|
|
79
|
+
const dragStartHandler = useCallbackRef(options.onDragStart)
|
|
80
|
+
const dragEndHandler = useCallbackRef(options.onDragEnd)
|
|
81
|
+
|
|
82
|
+
// Read when the instance is created. The effect below keeps it current, and
|
|
83
|
+
// runs right after, so a stale setting is replaced within the same commit.
|
|
84
|
+
const latest = useRef(options)
|
|
85
|
+
|
|
86
|
+
const instanceRef = useRef<DragValueInstance | null>(null)
|
|
87
|
+
|
|
88
|
+
// See useDrag for why the node is held in state.
|
|
89
|
+
const [node, setNode] = useState<T | null>(null)
|
|
90
|
+
|
|
91
|
+
const [pixelRangeX, pixelRangeY] = Array.isArray(pixelRange)
|
|
92
|
+
? pixelRange
|
|
93
|
+
: [pixelRange, pixelRange]
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!node) return
|
|
97
|
+
|
|
98
|
+
const instance = createDragValue(node, {
|
|
99
|
+
axis: latest.current.axis,
|
|
100
|
+
mapping: baseElementRef
|
|
101
|
+
? elementMapping(() => baseElementRef.current)
|
|
102
|
+
: relativeMapping({
|
|
103
|
+
pixelRange: [pixelRangeX ?? 100, pixelRangeY ?? 100],
|
|
104
|
+
}),
|
|
105
|
+
getValue: () => valueGetter() ?? [0, 0],
|
|
106
|
+
updateOnPointerDown: latest.current.updateOnPointerDown,
|
|
107
|
+
threshold: latest.current.threshold,
|
|
108
|
+
cursor: latest.current.cursor,
|
|
109
|
+
onChange: (value) => changeHandler(value),
|
|
110
|
+
onDragStart: (value) => {
|
|
111
|
+
setDragging(true)
|
|
112
|
+
dragStartHandler(value)
|
|
113
|
+
},
|
|
114
|
+
onDragEnd: (value) => {
|
|
115
|
+
setDragging(false)
|
|
116
|
+
dragEndHandler(value)
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
instanceRef.current = instance
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
instanceRef.current = null
|
|
123
|
+
instance.destroy()
|
|
124
|
+
}
|
|
125
|
+
}, [
|
|
126
|
+
node,
|
|
127
|
+
baseElementRef,
|
|
128
|
+
pixelRangeX,
|
|
129
|
+
pixelRangeY,
|
|
130
|
+
valueGetter,
|
|
131
|
+
changeHandler,
|
|
132
|
+
dragStartHandler,
|
|
133
|
+
dragEndHandler,
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
// Runs after every render: the settings come from props and are cheap to
|
|
137
|
+
// push, and updating in place leaves a drag in progress untouched.
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
latest.current = options
|
|
140
|
+
instanceRef.current?.update({
|
|
141
|
+
axis,
|
|
142
|
+
updateOnPointerDown,
|
|
143
|
+
threshold,
|
|
144
|
+
cursor,
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return { refCallback: setNode, dragging }
|
|
149
|
+
}
|
package/src/hooks/useWheel.ts
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import { createWheel } from '@tremolo-ui/dom'
|
|
4
|
-
|
|
5
|
-
import { useCallbackRef } from './useCallbackRef'
|
|
1
|
+
import { RefObject, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
5
|
+
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
|
+
|
|
7
|
+
export interface UseWheelOptions extends WheelOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Listen on this element rather than on the one the returned ref callback is
|
|
10
|
+
* attached to, and ignore that callback.
|
|
11
|
+
*
|
|
12
|
+
* For a control made of several movable parts: a wheel event only reaches
|
|
13
|
+
* what the cursor is over, so a listener per part responds only while the
|
|
14
|
+
* cursor is over that same part. Listening on the element they share lets
|
|
15
|
+
* every part see the event, and each one decide whether it is the one to act.
|
|
16
|
+
*
|
|
17
|
+
* The element is read when the listener is attached, so a ref filled in by a
|
|
18
|
+
* parent is fine as long as the parent is above in the tree.
|
|
19
|
+
*/
|
|
20
|
+
target?: RefObject<Element | null>
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
/**
|
|
8
24
|
* Listen to wheel events on an element.
|
|
9
25
|
*
|
|
10
26
|
* The listener is not passive, so the handler may call `preventDefault()`.
|
|
11
27
|
*
|
|
12
|
-
* @returns a ref callback to attach to the element
|
|
28
|
+
* @returns a ref callback to attach to the element, unused when `target` is given
|
|
13
29
|
*/
|
|
14
30
|
export function useWheel<T extends Element>(
|
|
15
31
|
onWheel: (event: WheelEvent) => void,
|
|
32
|
+
{ requireFocus = false, target }: UseWheelOptions = {},
|
|
16
33
|
): (node: T | null) => void {
|
|
17
34
|
const wheelHandler = useCallbackRef(onWheel)
|
|
18
35
|
|
|
@@ -20,12 +37,15 @@ export function useWheel<T extends Element>(
|
|
|
20
37
|
const [node, setNode] = useState<T | null>(null)
|
|
21
38
|
|
|
22
39
|
useEffect(() => {
|
|
23
|
-
|
|
40
|
+
const element = target ? target.current : node
|
|
41
|
+
if (!element) return
|
|
24
42
|
|
|
25
|
-
const instance = createWheel(
|
|
43
|
+
const instance = createWheel(element, (event) => wheelHandler(event), {
|
|
44
|
+
requireFocus,
|
|
45
|
+
})
|
|
26
46
|
|
|
27
47
|
return () => instance.destroy()
|
|
28
|
-
}, [node, wheelHandler])
|
|
48
|
+
}, [node, target, wheelHandler, requireFocus])
|
|
29
49
|
|
|
30
50
|
return setNode
|
|
31
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,35 +19,40 @@ export {
|
|
|
19
19
|
export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
|
|
20
20
|
export {
|
|
21
21
|
NumberInput,
|
|
22
|
+
useNumberInputContext,
|
|
22
23
|
type NumberInputProps,
|
|
23
24
|
type NumberInputMethods,
|
|
25
|
+
type NumberInputFieldProps,
|
|
24
26
|
type StepperProps,
|
|
25
27
|
type IncrementStepperProps,
|
|
26
28
|
type DecrementStepperProps,
|
|
27
29
|
} from './components/NumberInput'
|
|
28
30
|
export {
|
|
29
31
|
Piano,
|
|
30
|
-
getNoteRangeArray,
|
|
31
32
|
SHORTCUTS,
|
|
32
33
|
type PianoProps,
|
|
33
34
|
type PianoMethods,
|
|
34
|
-
type
|
|
35
|
-
type
|
|
36
|
-
type
|
|
35
|
+
type KeyState,
|
|
36
|
+
type KeyAttributes,
|
|
37
|
+
type CSSVariables,
|
|
37
38
|
type KeyboardShortcuts,
|
|
38
39
|
} from './components/Piano'
|
|
39
40
|
export {
|
|
40
41
|
PointsEditor,
|
|
42
|
+
usePointsEditorContext,
|
|
41
43
|
clampPoint,
|
|
42
44
|
type PointsEditorProps,
|
|
45
|
+
type PointsEditorContextValue,
|
|
46
|
+
type PointsEditorBackgroundProps,
|
|
47
|
+
type PointsEditorContainerProps,
|
|
43
48
|
type PointProps,
|
|
44
49
|
type PointBaseType,
|
|
45
50
|
} from './components/PointsEditor'
|
|
46
51
|
export {
|
|
47
52
|
Slider,
|
|
48
53
|
useSliderContext,
|
|
49
|
-
type
|
|
50
|
-
type
|
|
54
|
+
type MarksOptionProps,
|
|
55
|
+
type MarksProps,
|
|
51
56
|
type SliderMethods,
|
|
52
57
|
type SliderProps,
|
|
53
58
|
type SliderThumbMethods,
|
|
@@ -61,13 +66,14 @@ export {
|
|
|
61
66
|
type XYPadAreaProps,
|
|
62
67
|
type XYPadThumbProps,
|
|
63
68
|
type XYPadThumbMethods,
|
|
64
|
-
type
|
|
69
|
+
type XY,
|
|
70
|
+
type XYInput,
|
|
65
71
|
} from './components/XYPad'
|
|
66
72
|
|
|
67
73
|
// hooks
|
|
68
74
|
export { useAnimationFrame } from './hooks/useAnimationFrame'
|
|
69
75
|
export { useDrag } from './hooks/useDrag'
|
|
70
|
-
export {
|
|
76
|
+
export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
|
|
71
77
|
export { useEventListener } from './hooks/useEventListener'
|
|
72
78
|
export { useInterval } from './hooks/useInterval'
|
|
73
79
|
export { useLongPress } from './hooks/useLongPress'
|
|
@@ -79,4 +85,4 @@ export {
|
|
|
79
85
|
} from './hooks/useMIDIAccess'
|
|
80
86
|
export { useMIDIInput } from './hooks/useMIDIInput'
|
|
81
87
|
export { useMIDIMessage } from './hooks/useMIDIMessage'
|
|
82
|
-
export { useWheel } from './hooks/useWheel'
|
|
88
|
+
export { useWheel, type UseWheelOptions } from './hooks/useWheel'
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save
|
|
2
|
-
export const drawingState = [
|
|
3
|
-
'strokeStyle',
|
|
4
|
-
'fillStyle',
|
|
5
|
-
'globalAlpha',
|
|
6
|
-
'lineWidth',
|
|
7
|
-
'lineCap',
|
|
8
|
-
'lineJoin',
|
|
9
|
-
'miterLimit',
|
|
10
|
-
'lineDashOffset',
|
|
11
|
-
'shadowOffsetX',
|
|
12
|
-
'shadowOffsetY',
|
|
13
|
-
'shadowBlur',
|
|
14
|
-
'shadowColor',
|
|
15
|
-
'globalCompositeOperation',
|
|
16
|
-
'font',
|
|
17
|
-
'textAlign',
|
|
18
|
-
'textBaseline',
|
|
19
|
-
'direction',
|
|
20
|
-
'imageSmoothingEnabled',
|
|
21
|
-
] as const
|
|
22
|
-
|
|
23
|
-
export type DrawingState = (typeof drawingState)[number]
|
|
24
|
-
export type DrawingStateValue = CanvasRenderingContext2D[DrawingState]
|
|
25
|
-
export type DrawingContext = Pick<CanvasRenderingContext2D, DrawingState>
|
|
26
|
-
|
|
27
|
-
export function isDrawingState(obj: unknown): obj is DrawingState {
|
|
28
|
-
const d: string[] = [...drawingState]
|
|
29
|
-
return typeof obj == 'string' && d.includes(obj)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function setDprConfig(
|
|
33
|
-
canvas: HTMLCanvasElement,
|
|
34
|
-
context: CanvasRenderingContext2D,
|
|
35
|
-
width: number,
|
|
36
|
-
height: number,
|
|
37
|
-
dpr: number,
|
|
38
|
-
) {
|
|
39
|
-
canvas.width = width * dpr
|
|
40
|
-
canvas.height = height * dpr
|
|
41
|
-
// Reset current transformation matrix to the identity matrix
|
|
42
|
-
context.setTransform(1, 0, 0, 1, 0, 0)
|
|
43
|
-
context.scale(dpr, dpr)
|
|
44
|
-
canvas.style.width = `${width}px`
|
|
45
|
-
canvas.style.height = `${height}px`
|
|
46
|
-
}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
|
-
import {
|
|
3
|
-
ComponentPropsWithoutRef,
|
|
4
|
-
forwardRef,
|
|
5
|
-
useCallback,
|
|
6
|
-
useImperativeHandle,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
} from 'react'
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
clamp,
|
|
14
|
-
InputEventOption,
|
|
15
|
-
normalizeValue,
|
|
16
|
-
rawValue,
|
|
17
|
-
stepValue,
|
|
18
|
-
} from '@tremolo-ui/functions'
|
|
19
|
-
|
|
20
|
-
import { useWheel } from '../../hooks/useWheel'
|
|
21
|
-
|
|
22
|
-
import { safeClamp, useNumberInputContext } from './context'
|
|
23
|
-
import { parseValue } from './type'
|
|
24
|
-
|
|
25
|
-
import { NumberInputMethods } from '.'
|
|
26
|
-
|
|
27
|
-
interface InternalProps {
|
|
28
|
-
disabled: boolean
|
|
29
|
-
readonly: boolean
|
|
30
|
-
wheel: InputEventOption | null
|
|
31
|
-
keyboard: InputEventOption | null
|
|
32
|
-
|
|
33
|
-
selectWithFocus?: 'all' | 'number' | 'none'
|
|
34
|
-
blurOnEnter?: boolean
|
|
35
|
-
keepWithinRange?: boolean
|
|
36
|
-
clampValueOnBlur?: boolean
|
|
37
|
-
|
|
38
|
-
onChange?: (value: number, text: string) => void
|
|
39
|
-
onFocus?: (
|
|
40
|
-
value: number,
|
|
41
|
-
text: string,
|
|
42
|
-
event: React.FocusEvent<HTMLInputElement, Element>,
|
|
43
|
-
) => void
|
|
44
|
-
onBlur?: (
|
|
45
|
-
value: number,
|
|
46
|
-
text: string,
|
|
47
|
-
event: React.FocusEvent<HTMLInputElement, Element>,
|
|
48
|
-
) => void
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type InternalPropsWithAttributes = InternalProps &
|
|
52
|
-
Omit<ComponentPropsWithoutRef<'input'>, keyof InternalProps>
|
|
53
|
-
|
|
54
|
-
export const InternalInput = forwardRef<
|
|
55
|
-
NumberInputMethods,
|
|
56
|
-
InternalPropsWithAttributes
|
|
57
|
-
>(
|
|
58
|
-
(
|
|
59
|
-
{
|
|
60
|
-
disabled,
|
|
61
|
-
readonly,
|
|
62
|
-
selectWithFocus,
|
|
63
|
-
blurOnEnter,
|
|
64
|
-
clampValueOnBlur,
|
|
65
|
-
wheel,
|
|
66
|
-
keyboard,
|
|
67
|
-
className,
|
|
68
|
-
onChange,
|
|
69
|
-
onFocus,
|
|
70
|
-
onBlur,
|
|
71
|
-
onKeyDown,
|
|
72
|
-
children: _children,
|
|
73
|
-
...props
|
|
74
|
-
}: InternalPropsWithAttributes,
|
|
75
|
-
ref,
|
|
76
|
-
) => {
|
|
77
|
-
const elmRef = useRef<HTMLInputElement>(null)
|
|
78
|
-
const [editing, setEditing] = useState(false)
|
|
79
|
-
const value = useNumberInputContext((s) => s.value)
|
|
80
|
-
const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
|
|
81
|
-
const min = useNumberInputContext((s) => s.min)
|
|
82
|
-
const max = useNumberInputContext((s) => s.max)
|
|
83
|
-
const step = useNumberInputContext((s) => s.step)
|
|
84
|
-
const units = useNumberInputContext((s) => s.units)
|
|
85
|
-
const digit = useNumberInputContext((s) => s.digit)
|
|
86
|
-
const change = useNumberInputContext((s) => s.change)
|
|
87
|
-
|
|
88
|
-
const error = useMemo(() => {
|
|
89
|
-
if (editing) return false
|
|
90
|
-
const v = valueAsNumber
|
|
91
|
-
return (min ?? -Infinity) > v || v > (max ?? Infinity)
|
|
92
|
-
}, [editing, max, min, valueAsNumber])
|
|
93
|
-
|
|
94
|
-
const updateValueByEvent = useCallback(
|
|
95
|
-
(eventType: InputEventOption[0], x: number) => {
|
|
96
|
-
let newValue
|
|
97
|
-
if (eventType == 'normalized') {
|
|
98
|
-
if (!min || !max) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
'[NumberInput] "min" and "max" are required when InputEventOption[0] is set to "normalized".',
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
const n = normalizeValue(valueAsNumber, min, max)
|
|
104
|
-
newValue = rawValue(n + x, min, max)
|
|
105
|
-
return clamp(stepValue(newValue, step), min, max)
|
|
106
|
-
} else {
|
|
107
|
-
newValue = valueAsNumber + x
|
|
108
|
-
return stepValue(newValue, step)
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
[max, min, step, valueAsNumber],
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
const handleKeyDown = useCallback(
|
|
115
|
-
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
116
|
-
if (!keyboard || !onChange || readonly) return
|
|
117
|
-
const key = event.key
|
|
118
|
-
if (!['ArrowUp', 'ArrowDown'].includes(key)) return
|
|
119
|
-
event.preventDefault()
|
|
120
|
-
const x = key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
|
|
121
|
-
const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)))
|
|
122
|
-
// console.log('key control')
|
|
123
|
-
onChange?.(parsed.rawValue, parsed.formatValue)
|
|
124
|
-
},
|
|
125
|
-
[keyboard, readonly, onChange, updateValueByEvent],
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
const wheelRefCallback = useWheel<HTMLInputElement>((event) => {
|
|
129
|
-
if (!wheel || readonly) return
|
|
130
|
-
event.preventDefault()
|
|
131
|
-
if (!onChange || event.deltaY == 0) return
|
|
132
|
-
const x = Math.sign(event.deltaY) * -wheel[1]
|
|
133
|
-
const parsed = parseValue(String(updateValueByEvent(wheel[0], x)))
|
|
134
|
-
// console.log('wheel control')
|
|
135
|
-
onChange?.(parsed.rawValue, parsed.formatValue)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
useImperativeHandle(ref, () => {
|
|
139
|
-
return {
|
|
140
|
-
focus() {
|
|
141
|
-
elmRef.current?.focus()
|
|
142
|
-
},
|
|
143
|
-
blur() {
|
|
144
|
-
elmRef.current?.blur()
|
|
145
|
-
},
|
|
146
|
-
}
|
|
147
|
-
}, [])
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<input
|
|
151
|
-
ref={(input) => {
|
|
152
|
-
elmRef.current = input
|
|
153
|
-
wheelRefCallback(input)
|
|
154
|
-
}}
|
|
155
|
-
className={clsx('tremolo-number-input', className)}
|
|
156
|
-
value={value}
|
|
157
|
-
min={min}
|
|
158
|
-
max={max}
|
|
159
|
-
step={step}
|
|
160
|
-
type="text"
|
|
161
|
-
aria-disabled={disabled}
|
|
162
|
-
aria-readonly={readonly}
|
|
163
|
-
tabIndex={-1}
|
|
164
|
-
data-error={error}
|
|
165
|
-
onChange={(event) => {
|
|
166
|
-
if (readonly) return
|
|
167
|
-
const v = event.currentTarget.value
|
|
168
|
-
const cursorPos = event.target.selectionStart
|
|
169
|
-
change(v)
|
|
170
|
-
const valueAsNumber = parseValue(v, units, digit).rawValue
|
|
171
|
-
onChange?.(valueAsNumber, v)
|
|
172
|
-
if (cursorPos) {
|
|
173
|
-
event.target.selectionStart = cursorPos
|
|
174
|
-
event.target.selectionEnd = cursorPos
|
|
175
|
-
}
|
|
176
|
-
}}
|
|
177
|
-
onFocus={(event) => {
|
|
178
|
-
setEditing(true)
|
|
179
|
-
const v = event.currentTarget.value
|
|
180
|
-
const parsed = parseValue(v, units, digit)
|
|
181
|
-
if (selectWithFocus == 'all') {
|
|
182
|
-
event.currentTarget.setSelectionRange(0, v.length)
|
|
183
|
-
} else if (selectWithFocus == 'number') {
|
|
184
|
-
event.currentTarget.setSelectionRange(
|
|
185
|
-
0,
|
|
186
|
-
parsed.formatValue.length - parsed.unit.length,
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
onFocus?.(parsed.rawValue, parsed.formatValue, event)
|
|
190
|
-
}}
|
|
191
|
-
onBlur={(event) => {
|
|
192
|
-
setEditing(false)
|
|
193
|
-
// update value
|
|
194
|
-
let v = parseValue(event.currentTarget.value, units, digit).rawValue
|
|
195
|
-
if (clampValueOnBlur) {
|
|
196
|
-
v = safeClamp(v, min, max)
|
|
197
|
-
}
|
|
198
|
-
const parsed = parseValue(String(v), units, digit)
|
|
199
|
-
change(parsed.formatValue)
|
|
200
|
-
if (clampValueOnBlur) {
|
|
201
|
-
onChange?.(parsed.rawValue, parsed.formatValue)
|
|
202
|
-
}
|
|
203
|
-
onBlur?.(parsed.rawValue, parsed.formatValue, event)
|
|
204
|
-
}}
|
|
205
|
-
onKeyDown={(event) => {
|
|
206
|
-
if (blurOnEnter && event.key == 'Enter') {
|
|
207
|
-
event.currentTarget.blur()
|
|
208
|
-
}
|
|
209
|
-
handleKeyDown(event)
|
|
210
|
-
onKeyDown?.(event)
|
|
211
|
-
}}
|
|
212
|
-
{...props}
|
|
213
|
-
/>
|
|
214
|
-
)
|
|
215
|
-
},
|
|
216
|
-
)
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* [unit, scale][]
|
|
3
|
-
* @example
|
|
4
|
-
* [['Hz', 1], ['kHz', 1000]]
|
|
5
|
-
*/
|
|
6
|
-
export type Units = [string, number][]
|
|
7
|
-
|
|
8
|
-
export function selectUnit(units: Units, value: number): [string, number] {
|
|
9
|
-
let i = 0
|
|
10
|
-
for (; i < units.length; i++) {
|
|
11
|
-
if (Math.abs(units[i][1]) > Math.abs(value)) break
|
|
12
|
-
}
|
|
13
|
-
return units[Math.max(0, i - 1)]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function parseValue(
|
|
17
|
-
inputString: string,
|
|
18
|
-
units?: string | Units,
|
|
19
|
-
digit?: number,
|
|
20
|
-
): {
|
|
21
|
-
rawValue: number
|
|
22
|
-
formatValue: string
|
|
23
|
-
unit: string
|
|
24
|
-
} {
|
|
25
|
-
const str = inputString.trim()
|
|
26
|
-
if (!units || typeof units == 'string') {
|
|
27
|
-
const m = str.match(/-?\d+(\.\d+)?/)
|
|
28
|
-
let v = Number(m?.[0] ?? '0')
|
|
29
|
-
v = isNaN(v) ? 0 : v
|
|
30
|
-
return {
|
|
31
|
-
rawValue: v,
|
|
32
|
-
formatValue: (digit != undefined ? v.toFixed(digit) : v) + (units ?? ''),
|
|
33
|
-
unit: units ?? '',
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const unitList = units.map(([u, _s]) => u)
|
|
38
|
-
const scaleList = units.map(([_u, s]) => s)
|
|
39
|
-
let rawValue = 0
|
|
40
|
-
let unit = unitList[0]
|
|
41
|
-
let formatValue =
|
|
42
|
-
(digit != undefined ? rawValue.toFixed(digit) : rawValue) + unit
|
|
43
|
-
const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/)
|
|
44
|
-
if (m) {
|
|
45
|
-
rawValue = Number(m[1]) || 0
|
|
46
|
-
const uIndex = unitList.indexOf(m[3])
|
|
47
|
-
rawValue *= uIndex != -1 ? scaleList[uIndex] : 1
|
|
48
|
-
const [u, scale] = selectUnit(units, rawValue)
|
|
49
|
-
const v = rawValue / scale
|
|
50
|
-
formatValue = (digit != undefined ? v.toFixed(digit) : v) + u
|
|
51
|
-
unit = u
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
rawValue: rawValue,
|
|
55
|
-
formatValue: formatValue,
|
|
56
|
-
unit: unit,
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
|
-
import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'
|
|
3
|
-
|
|
4
|
-
import { usePianoContext } from './context'
|
|
5
|
-
|
|
6
|
-
import { getNoteRangeArray } from '.'
|
|
7
|
-
|
|
8
|
-
export interface KeyLabelProps {
|
|
9
|
-
/**
|
|
10
|
-
* override Piano.label
|
|
11
|
-
*/
|
|
12
|
-
label?: (note: number, index: number) => ReactNode
|
|
13
|
-
|
|
14
|
-
wrapperClassName?: string
|
|
15
|
-
wrapperStyle?: CSSProperties
|
|
16
|
-
|
|
17
|
-
/** @internal */
|
|
18
|
-
__note?: number
|
|
19
|
-
/** @internal */
|
|
20
|
-
__label?: (note: number, index: number) => ReactNode
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function KeyLabel({
|
|
24
|
-
label,
|
|
25
|
-
className,
|
|
26
|
-
wrapperClassName,
|
|
27
|
-
wrapperStyle,
|
|
28
|
-
__note,
|
|
29
|
-
__label,
|
|
30
|
-
...props
|
|
31
|
-
}: KeyLabelProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyLabelProps>) {
|
|
32
|
-
const noteRange = usePianoContext((s) => s.noteRange)
|
|
33
|
-
const noteRangeArray = getNoteRangeArray(noteRange)
|
|
34
|
-
const index = noteRangeArray.indexOf(__note!)
|
|
35
|
-
const content = label
|
|
36
|
-
? label(__note!, index)
|
|
37
|
-
: __label && __label(__note!, index)
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
(content != undefined || content != null) && (
|
|
41
|
-
<div
|
|
42
|
-
className={clsx('tremolo-piano-key-label-wrapper', wrapperClassName)}
|
|
43
|
-
style={wrapperStyle}
|
|
44
|
-
>
|
|
45
|
-
<div className={clsx('tremolo-piano-key-label', className)} {...props}>
|
|
46
|
-
{content}
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
)
|
|
50
|
-
)
|
|
51
|
-
}
|