@tremolo-ui/react 0.3.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 +1162 -1554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +592 -278
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +592 -278
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1163 -1555
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- 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 +95 -83
- 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 +117 -52
- 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 +136 -170
- 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 +193 -250
- package/src/hooks/useDrag.ts +41 -76
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +51 -0
- package/src/index.ts +15 -13
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/DragObserver/index.tsx +0 -58
- package/src/components/NumberInput/InternalInput.tsx +0 -221
- 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/WheelObserver/index.tsx +0 -48
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -72
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
|
@@ -1,64 +1,87 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
forwardRef,
|
|
6
|
+
ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react'
|
|
3
13
|
|
|
4
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
applyDelta,
|
|
16
|
+
clamp,
|
|
17
|
+
formatValue,
|
|
18
|
+
InputEventOption,
|
|
19
|
+
linearScale,
|
|
20
|
+
parseValue,
|
|
21
|
+
type Scale,
|
|
22
|
+
type Units,
|
|
23
|
+
type ValueRange,
|
|
24
|
+
} from '@tremolo-ui/functions'
|
|
25
|
+
|
|
26
|
+
import { useWheel } from '../../hooks/useWheel'
|
|
5
27
|
|
|
6
28
|
import { NumberInputProvider } from './context'
|
|
7
29
|
import { DecrementStepper } from './DecrementStepper'
|
|
8
30
|
import { IncrementStepper } from './IncrementStepper'
|
|
9
|
-
import {
|
|
31
|
+
import { InputField } from './InputField'
|
|
10
32
|
import { Stepper } from './Stepper'
|
|
11
|
-
import { Units } from './type'
|
|
12
|
-
|
|
13
|
-
/*
|
|
14
|
-
TODO
|
|
15
|
-
- numberMode: focusが当たった時だけ、parse + type="number"にする
|
|
16
|
-
- skew
|
|
17
|
-
*/
|
|
18
33
|
|
|
19
34
|
export interface NumberInputProps {
|
|
20
|
-
value: number | string
|
|
21
|
-
|
|
22
35
|
/**
|
|
23
|
-
*
|
|
36
|
+
* The value. What the input shows is `format(value)`, except while the user
|
|
37
|
+
* is typing, when their own text stands until it is committed.
|
|
24
38
|
*/
|
|
39
|
+
value: number
|
|
40
|
+
|
|
25
41
|
min?: number
|
|
26
|
-
/**
|
|
27
|
-
* Number.MAX_SAFE_INTEGER
|
|
28
|
-
*/
|
|
29
42
|
max?: number
|
|
30
43
|
step?: number
|
|
44
|
+
/**
|
|
45
|
+
* How the value is distributed across the travel of a drag or a
|
|
46
|
+
* `'normalized'` wheel / keyboard nudge.
|
|
47
|
+
*
|
|
48
|
+
* @default linearScale
|
|
49
|
+
*/
|
|
50
|
+
scale?: Scale
|
|
31
51
|
|
|
32
52
|
/**
|
|
53
|
+
* Unit to display the value in, or a list to pick from by magnitude.
|
|
54
|
+
* Builds the default `format` and `parse`.
|
|
55
|
+
*
|
|
33
56
|
* @example
|
|
34
57
|
* units='Hz'
|
|
35
58
|
* units={[['Hz', 1], ['kHz', 1000]]}
|
|
36
59
|
* units={[['ms', 1], ['s', 1000]]}
|
|
37
60
|
*/
|
|
38
61
|
units?: string | Units
|
|
39
|
-
|
|
40
62
|
/**
|
|
41
|
-
* Digits for
|
|
63
|
+
* Digits after the decimal point, for the default `format`.
|
|
64
|
+
*
|
|
42
65
|
* @example
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* digit={3}
|
|
46
|
-
* results:
|
|
47
|
-
* value=100 -> 100Hz, value=1600 -> 1.60Hz
|
|
66
|
+
* // units={[['Hz', 1], ['kHz', 1000]]} digit={2}
|
|
67
|
+
* // 100 -> 100.00Hz, 1600 -> 1.60kHz
|
|
48
68
|
*/
|
|
49
69
|
digit?: number
|
|
70
|
+
/** Render the value as text. Takes precedence over `units` / `digit`. */
|
|
71
|
+
format?: (value: number) => string
|
|
72
|
+
/** Read a value back out of the text. Takes precedence over `units`. */
|
|
73
|
+
parse?: (text: string) => number
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
keepWithinRange?: boolean
|
|
58
|
-
clampValueOnBlur?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Keep the value within `min` and `max` when it is committed or stepped.
|
|
77
|
+
* Typing is never clamped, so that a value can be entered digit by digit.
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
clampValue?: boolean
|
|
59
81
|
|
|
60
82
|
/**
|
|
61
|
-
*
|
|
83
|
+
* Wheel control option. Only applies while the focus is inside, so that
|
|
84
|
+
* scrolling past the input does not change it.
|
|
62
85
|
* If null, no event will be triggered
|
|
63
86
|
*/
|
|
64
87
|
wheel?: InputEventOption | null
|
|
@@ -67,24 +90,43 @@ export interface NumberInputProps {
|
|
|
67
90
|
* If null, no event will be triggered
|
|
68
91
|
*/
|
|
69
92
|
keyboard?: InputEventOption | null
|
|
93
|
+
/**
|
|
94
|
+
* Pixels of vertical drag on `Stepper` that move the value by one `step`.
|
|
95
|
+
* If null, no event will be triggered
|
|
96
|
+
* @default 1
|
|
97
|
+
*/
|
|
98
|
+
drag?: number | null
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Only the appearance will change.
|
|
102
|
+
* Please consider using with readonly.
|
|
103
|
+
* aria-disabled property is also applied.
|
|
104
|
+
*/
|
|
105
|
+
disabled?: boolean
|
|
106
|
+
/**
|
|
107
|
+
* Make the value unchangeable.
|
|
108
|
+
* aria-readonly property is also applied.
|
|
109
|
+
*/
|
|
110
|
+
readonly?: boolean
|
|
74
111
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
text: string,
|
|
79
|
-
event: React.FocusEvent<HTMLInputElement, Element>,
|
|
80
|
-
) => void
|
|
81
|
-
onBlur?: (
|
|
82
|
-
value: number,
|
|
83
|
-
text: string,
|
|
84
|
-
event: React.FocusEvent<HTMLInputElement, Element>,
|
|
85
|
-
) => void
|
|
112
|
+
className?: string
|
|
113
|
+
style?: CSSProperties
|
|
114
|
+
onChange?: (value: number) => void
|
|
86
115
|
|
|
87
|
-
|
|
116
|
+
/**
|
|
117
|
+
* The input renders exactly what you compose here; there is no default
|
|
118
|
+
* markup to fall back to.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* <NumberInput.Root value={value} min={0} max={100} onChange={setValue}>
|
|
122
|
+
* <NumberInput.InputField />
|
|
123
|
+
* <NumberInput.Stepper>
|
|
124
|
+
* <NumberInput.IncrementStepper />
|
|
125
|
+
* <NumberInput.DecrementStepper />
|
|
126
|
+
* </NumberInput.Stepper>
|
|
127
|
+
* </NumberInput.Root>
|
|
128
|
+
*/
|
|
129
|
+
children: ReactNode
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
export interface NumberInputMethods {
|
|
@@ -93,77 +135,187 @@ export interface NumberInputMethods {
|
|
|
93
135
|
}
|
|
94
136
|
|
|
95
137
|
type Props = NumberInputProps &
|
|
96
|
-
Omit<ComponentPropsWithoutRef<'
|
|
138
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
|
|
97
139
|
|
|
98
|
-
const Root = forwardRef<NumberInputMethods, Props>(
|
|
140
|
+
export const Root = forwardRef<NumberInputMethods, Props>(
|
|
99
141
|
(
|
|
100
142
|
{
|
|
101
143
|
value,
|
|
102
144
|
min,
|
|
103
145
|
max,
|
|
104
146
|
step = 1,
|
|
147
|
+
scale = linearScale,
|
|
105
148
|
units,
|
|
106
|
-
readonly = false,
|
|
107
|
-
disabled = false,
|
|
108
149
|
digit,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
keepWithinRange = true,
|
|
113
|
-
clampValueOnBlur = true,
|
|
150
|
+
format: formatProp,
|
|
151
|
+
parse: parseProp,
|
|
152
|
+
clampValue = true,
|
|
114
153
|
wheel = ['raw', 1],
|
|
115
154
|
keyboard = ['raw', 1],
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
drag = 1,
|
|
156
|
+
disabled = false,
|
|
157
|
+
readonly = false,
|
|
118
158
|
className,
|
|
159
|
+
style,
|
|
119
160
|
onChange,
|
|
120
|
-
onFocus,
|
|
121
|
-
onBlur,
|
|
122
161
|
children,
|
|
123
162
|
...props
|
|
124
163
|
}: Props,
|
|
125
164
|
forwardedRef,
|
|
126
165
|
) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
166
|
+
// --- state and ref ---
|
|
167
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
168
|
+
/**
|
|
169
|
+
* The text being typed. The only state here: it is not a copy of `value`,
|
|
170
|
+
* but the half-finished entry that has no value to be derived from yet.
|
|
171
|
+
*/
|
|
172
|
+
const [draft, setDraft] = useState<string | null>(null)
|
|
173
|
+
|
|
174
|
+
// --- interpret props ---
|
|
175
|
+
const format = useCallback(
|
|
176
|
+
(v: number) =>
|
|
177
|
+
formatProp ? formatProp(v) : formatValue(v, units, digit),
|
|
178
|
+
[formatProp, units, digit],
|
|
179
|
+
)
|
|
180
|
+
const parse = useCallback(
|
|
181
|
+
(t: string) => (parseProp ? parseProp(t) : parseValue(t, units)),
|
|
182
|
+
[parseProp, units],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// An unbounded end, and a range the caller opted out of enforcing, both
|
|
186
|
+
// become the widest range the value pipeline can express.
|
|
187
|
+
const range: ValueRange = useMemo(
|
|
188
|
+
() => ({
|
|
189
|
+
min: (clampValue ? min : undefined) ?? Number.MIN_SAFE_INTEGER,
|
|
190
|
+
max: (clampValue ? max : undefined) ?? Number.MAX_SAFE_INTEGER,
|
|
191
|
+
step,
|
|
192
|
+
scale,
|
|
193
|
+
}),
|
|
194
|
+
[clampValue, min, max, step, scale],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const text = draft ?? format(value)
|
|
198
|
+
const editing = draft != null
|
|
199
|
+
const outOfRange =
|
|
200
|
+
!editing &&
|
|
201
|
+
((min != undefined && value < min) || (max != undefined && value > max))
|
|
202
|
+
|
|
203
|
+
// --- internal functions ---
|
|
204
|
+
const handleDraft = useCallback(
|
|
205
|
+
(next: string) => {
|
|
206
|
+
if (readonly) return
|
|
207
|
+
setDraft(next)
|
|
208
|
+
// Deliberately unclamped: clamping here would make "1500" impossible
|
|
209
|
+
// to type into an input whose max is 100.
|
|
210
|
+
const parsed = parse(next)
|
|
211
|
+
if (Number.isFinite(parsed)) onChange?.(parsed)
|
|
212
|
+
},
|
|
213
|
+
[readonly, parse, onChange],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const changeValue = useCallback(
|
|
217
|
+
(next: number) => {
|
|
218
|
+
if (readonly) return
|
|
219
|
+
setDraft(null)
|
|
220
|
+
if (next != value) onChange?.(next)
|
|
221
|
+
},
|
|
222
|
+
[readonly, value, onChange],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const commitDraft = useCallback(() => {
|
|
226
|
+
if (draft == null || readonly) return
|
|
227
|
+
// `range` is already the widest possible range when clampValue is off.
|
|
228
|
+
changeValue(clamp(parse(draft), range.min, range.max))
|
|
229
|
+
}, [draft, readonly, parse, range, changeValue])
|
|
230
|
+
|
|
231
|
+
const nudge = useCallback(
|
|
232
|
+
(direction: number, option: InputEventOption) => {
|
|
233
|
+
changeValue(applyDelta(value, direction, option, range))
|
|
234
|
+
},
|
|
235
|
+
[changeValue, value, range],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// --- hooks ---
|
|
239
|
+
const wheelRefCallback = useWheel<HTMLDivElement>(
|
|
240
|
+
(event) => {
|
|
241
|
+
if (!wheel || readonly || event.deltaY == 0) return
|
|
242
|
+
event.preventDefault()
|
|
243
|
+
nudge(-Math.sign(event.deltaY), wheel)
|
|
244
|
+
},
|
|
245
|
+
{ requireFocus: true },
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const context = useMemo(
|
|
249
|
+
() => ({
|
|
250
|
+
value,
|
|
251
|
+
min,
|
|
252
|
+
max,
|
|
253
|
+
step,
|
|
254
|
+
scale,
|
|
255
|
+
disabled,
|
|
256
|
+
readonly,
|
|
257
|
+
clampValue,
|
|
258
|
+
range,
|
|
259
|
+
keyboard,
|
|
260
|
+
text,
|
|
261
|
+
editing,
|
|
262
|
+
outOfRange,
|
|
263
|
+
atMin: clampValue && min != undefined && value <= min,
|
|
264
|
+
atMax: clampValue && max != undefined && value >= max,
|
|
265
|
+
drag,
|
|
266
|
+
format,
|
|
267
|
+
parse,
|
|
268
|
+
setDraft: handleDraft,
|
|
269
|
+
commitDraft,
|
|
270
|
+
changeValue,
|
|
271
|
+
nudge,
|
|
272
|
+
inputRef,
|
|
273
|
+
}),
|
|
274
|
+
[
|
|
275
|
+
value,
|
|
276
|
+
min,
|
|
277
|
+
max,
|
|
278
|
+
step,
|
|
279
|
+
scale,
|
|
280
|
+
disabled,
|
|
281
|
+
readonly,
|
|
282
|
+
clampValue,
|
|
283
|
+
range,
|
|
284
|
+
keyboard,
|
|
285
|
+
text,
|
|
286
|
+
editing,
|
|
287
|
+
outOfRange,
|
|
288
|
+
drag,
|
|
289
|
+
format,
|
|
290
|
+
parse,
|
|
291
|
+
handleDraft,
|
|
292
|
+
commitDraft,
|
|
293
|
+
changeValue,
|
|
294
|
+
nudge,
|
|
295
|
+
],
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
useImperativeHandle(forwardedRef, () => {
|
|
299
|
+
return {
|
|
300
|
+
focus() {
|
|
301
|
+
inputRef.current?.focus()
|
|
302
|
+
},
|
|
303
|
+
blur() {
|
|
304
|
+
inputRef.current?.blur()
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
}, [])
|
|
308
|
+
|
|
130
309
|
return (
|
|
131
|
-
<NumberInputProvider
|
|
132
|
-
value={String(value)}
|
|
133
|
-
min={min}
|
|
134
|
-
max={max}
|
|
135
|
-
step={step}
|
|
136
|
-
units={units}
|
|
137
|
-
digit={digit}
|
|
138
|
-
readonly={readonly}
|
|
139
|
-
keepWithinRange={keepWithinRange}
|
|
140
|
-
onChange={onChange}
|
|
141
|
-
>
|
|
310
|
+
<NumberInputProvider value={context}>
|
|
142
311
|
<div
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
data-stepper={!!children}
|
|
150
|
-
data-variant={variant}
|
|
312
|
+
ref={wheelRefCallback}
|
|
313
|
+
className={clsx('tremolo-number-input', className)}
|
|
314
|
+
aria-disabled={disabled}
|
|
315
|
+
aria-readonly={readonly}
|
|
316
|
+
style={style}
|
|
317
|
+
{...props}
|
|
151
318
|
>
|
|
152
|
-
<InternalInput
|
|
153
|
-
ref={forwardedRef}
|
|
154
|
-
readonly={readonly}
|
|
155
|
-
disabled={disabled}
|
|
156
|
-
selectWithFocus={selectWithFocus}
|
|
157
|
-
blurOnEnter={blurOnEnter}
|
|
158
|
-
clampValueOnBlur={clampValueOnBlur}
|
|
159
|
-
wheel={wheel}
|
|
160
|
-
keyboard={keyboard}
|
|
161
|
-
className={className}
|
|
162
|
-
onChange={onChange}
|
|
163
|
-
onFocus={onFocus}
|
|
164
|
-
onBlur={onBlur}
|
|
165
|
-
{...props}
|
|
166
|
-
/>
|
|
167
319
|
{children}
|
|
168
320
|
</div>
|
|
169
321
|
</NumberInputProvider>
|
|
@@ -176,12 +328,14 @@ const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
176
328
|
*/
|
|
177
329
|
export const NumberInput = {
|
|
178
330
|
Root,
|
|
331
|
+
InputField,
|
|
179
332
|
Stepper,
|
|
180
333
|
IncrementStepper,
|
|
181
334
|
DecrementStepper,
|
|
182
335
|
}
|
|
183
336
|
|
|
337
|
+
export { useNumberInputContext } from './context'
|
|
338
|
+
export { type NumberInputFieldProps } from './InputField'
|
|
184
339
|
export { type StepperProps } from './Stepper'
|
|
185
340
|
export { type IncrementStepperProps } from './IncrementStepper'
|
|
186
341
|
export { type DecrementStepperProps } from './DecrementStepper'
|
|
187
|
-
export { type Units } from './type'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import {
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useCallback,
|
|
7
|
+
} from 'react'
|
|
8
|
+
|
|
9
|
+
import { useLongPress } from '../../hooks/useLongPress'
|
|
10
|
+
|
|
11
|
+
import { useNumberInputContext, useStepperContext } from './context'
|
|
12
|
+
|
|
13
|
+
export interface StepperButtonProps {
|
|
14
|
+
className?: string
|
|
15
|
+
style?: CSSProperties
|
|
16
|
+
/** Replaces the default arrow. Size it with the `--stepper-icon-size` variable. */
|
|
17
|
+
children?: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Props = StepperButtonProps &
|
|
21
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof StepperButtonProps>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
25
|
+
* in which way they move the value and which arrow they draw.
|
|
26
|
+
*/
|
|
27
|
+
export function StepperButton({
|
|
28
|
+
direction,
|
|
29
|
+
variant,
|
|
30
|
+
icon,
|
|
31
|
+
className,
|
|
32
|
+
style,
|
|
33
|
+
children,
|
|
34
|
+
onPointerDown,
|
|
35
|
+
...props
|
|
36
|
+
}: Props & {
|
|
37
|
+
direction: 1 | -1
|
|
38
|
+
variant: 'increment' | 'decrement'
|
|
39
|
+
icon: ReactNode
|
|
40
|
+
}) {
|
|
41
|
+
const { step, readonly, atMin, atMax, nudge } = useNumberInputContext()
|
|
42
|
+
const stepper = useStepperContext()
|
|
43
|
+
|
|
44
|
+
const press = useLongPress(
|
|
45
|
+
useCallback(() => {
|
|
46
|
+
// Once the pointer has actually travelled, the drag on `Stepper` owns the
|
|
47
|
+
// value; repeating on top of it would move it twice.
|
|
48
|
+
if (stepper?.draggingRef.current) return
|
|
49
|
+
nudge(direction, ['raw', step])
|
|
50
|
+
}, [stepper, nudge, direction, step]),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
|
|
55
|
+
<div
|
|
56
|
+
className={clsx(`tremolo-number-input-${variant}-stepper`, className)}
|
|
57
|
+
role="button"
|
|
58
|
+
tabIndex={-1}
|
|
59
|
+
// A bare arrow has no accessible name of its own. Overridable, since a
|
|
60
|
+
// caller may need it in their own language.
|
|
61
|
+
aria-label={direction > 0 ? 'Increment' : 'Decrement'}
|
|
62
|
+
aria-disabled={direction > 0 ? atMax : atMin}
|
|
63
|
+
aria-readonly={readonly}
|
|
64
|
+
style={style}
|
|
65
|
+
onPointerDown={(event) => {
|
|
66
|
+
if (!readonly) press()
|
|
67
|
+
onPointerDown?.(event)
|
|
68
|
+
}}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{children ?? icon}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function StepperArrow({ up }: { up: boolean }) {
|
|
77
|
+
return (
|
|
78
|
+
<svg
|
|
79
|
+
className="tremolo-number-input-stepper-icon"
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
strokeWidth="2"
|
|
84
|
+
strokeLinecap="round"
|
|
85
|
+
strokeLinejoin="round"
|
|
86
|
+
>
|
|
87
|
+
<polyline points={up ? '18 15 12 9 6 15' : '6 9 12 15 18 9'} />
|
|
88
|
+
</svg>
|
|
89
|
+
)
|
|
90
|
+
}
|