@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,7 +2,11 @@ import { ComponentPropsWithoutRef, ReactNode, CSSProperties } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { useLongPress } from '../../hooks/useLongPress'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useNumberInputContext,
|
|
7
|
+
useNumberInputGesture,
|
|
8
|
+
useStepperContext,
|
|
9
|
+
} from './context'
|
|
6
10
|
|
|
7
11
|
import type { CSSVariables } from '../../css-variables'
|
|
8
12
|
|
|
@@ -37,17 +41,17 @@ export function StepperButton({
|
|
|
37
41
|
variant: 'increment' | 'decrement'
|
|
38
42
|
icon: ReactNode
|
|
39
43
|
}) {
|
|
40
|
-
const { step, disabled,
|
|
44
|
+
const { step, disabled, readOnly, atMin, atMax, nudge } =
|
|
41
45
|
useNumberInputContext()
|
|
42
46
|
const stepper = useStepperContext()
|
|
47
|
+
const gesture = useNumberInputGesture()
|
|
43
48
|
|
|
44
|
-
// Not memoized:
|
|
45
|
-
//
|
|
46
|
-
// calls it through a ref, so a fresh identity per render costs nothing.
|
|
49
|
+
// Not memoized: `useLongPress` only ever calls it through a ref, so a fresh
|
|
50
|
+
// identity per render costs nothing.
|
|
47
51
|
const press = useLongPress(() => {
|
|
48
52
|
// Once the pointer has actually travelled, the drag on `Stepper` owns the
|
|
49
53
|
// value; repeating on top of it would move it twice.
|
|
50
|
-
if (stepper?.
|
|
54
|
+
if (stepper?.moved()) return
|
|
51
55
|
nudge(direction, ['raw', step])
|
|
52
56
|
})
|
|
53
57
|
|
|
@@ -65,10 +69,13 @@ export function StepperButton({
|
|
|
65
69
|
data-disabled={
|
|
66
70
|
disabled || (direction > 0 ? atMax : atMin) ? '' : undefined
|
|
67
71
|
}
|
|
68
|
-
data-readonly={
|
|
72
|
+
data-readonly={readOnly ? '' : undefined}
|
|
69
73
|
style={style}
|
|
70
74
|
onPointerDown={(event) => {
|
|
71
|
-
if (!disabled && !
|
|
75
|
+
if (!disabled && !readOnly) {
|
|
76
|
+
gesture?.press()
|
|
77
|
+
press(event)
|
|
78
|
+
}
|
|
72
79
|
onPointerDown?.(event)
|
|
73
80
|
}}
|
|
74
81
|
{...props}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, RefObject, useContext } from 'react'
|
|
1
|
+
import { createContext, CSSProperties, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type InputEventOption,
|
|
@@ -22,7 +22,7 @@ export type NumberInputContextValue = {
|
|
|
22
22
|
scale: Scale
|
|
23
23
|
|
|
24
24
|
disabled: boolean
|
|
25
|
-
|
|
25
|
+
readOnly: boolean
|
|
26
26
|
clampValue: boolean
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -40,6 +40,8 @@ export type NumberInputContextValue = {
|
|
|
40
40
|
dragSensitivity: ModifierValue<number>
|
|
41
41
|
/** Whether a `Stepper` drag hides the pointer and reads its movement. */
|
|
42
42
|
pointerLock: boolean
|
|
43
|
+
/** The cursor shown while a `Stepper` is dragged. */
|
|
44
|
+
dragCursor: CSSProperties['cursor']
|
|
43
45
|
|
|
44
46
|
/** What `InputField` selects when it takes focus. */
|
|
45
47
|
selectOnFocus: 'all' | 'number' | 'none'
|
|
@@ -101,9 +103,30 @@ export function useNumberInputContext<T>(
|
|
|
101
103
|
return selector ? selector(context) : context
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The change gesture of `Root`, for the parts that change the value. Not on
|
|
108
|
+
* the public context: it is how the parts report, not something to read.
|
|
109
|
+
*/
|
|
110
|
+
export type NumberInputGestureValue = {
|
|
111
|
+
/** A press on a stepper, held until the pointer is released anywhere. */
|
|
112
|
+
press: () => void
|
|
113
|
+
/** An arrow key in `InputField`. */
|
|
114
|
+
key: () => void
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const NumberInputGestureContext =
|
|
118
|
+
/* @__PURE__ */ createContext<NumberInputGestureValue | null>(null)
|
|
119
|
+
|
|
120
|
+
export const NumberInputGestureProvider = NumberInputGestureContext.Provider
|
|
121
|
+
|
|
122
|
+
export function useNumberInputGesture(): NumberInputGestureValue | null {
|
|
123
|
+
return useContext(NumberInputGestureContext)
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
/** Set by `Stepper` once a drag has actually moved, so the steppers stand down. */
|
|
105
127
|
export type StepperContextValue = {
|
|
106
|
-
|
|
128
|
+
/** Whether the drag in progress has moved the value. */
|
|
129
|
+
moved: () => boolean
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
const StepperContext =
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
3
4
|
forwardRef,
|
|
4
5
|
ReactNode,
|
|
6
|
+
Ref,
|
|
5
7
|
useCallback,
|
|
6
8
|
useImperativeHandle,
|
|
7
9
|
useMemo,
|
|
@@ -10,23 +12,30 @@ import {
|
|
|
10
12
|
} from 'react'
|
|
11
13
|
|
|
12
14
|
import {
|
|
13
|
-
|
|
15
|
+
type ChangeSource,
|
|
16
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
17
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
18
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
14
19
|
InputEventOption,
|
|
15
20
|
ModifierState,
|
|
16
21
|
type ModifierValue,
|
|
17
|
-
selectModifier,
|
|
18
22
|
} from '@tremolo-ui/dom'
|
|
19
|
-
import {
|
|
20
|
-
|
|
23
|
+
import {
|
|
24
|
+
commitNumberInputText,
|
|
25
|
+
numberInputBounds,
|
|
26
|
+
numberInputRanges,
|
|
27
|
+
nudgeNumberInput,
|
|
28
|
+
parseNumberText,
|
|
29
|
+
wheelDirection,
|
|
30
|
+
} from '@tremolo-ui/dom/internal'
|
|
31
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
32
|
+
|
|
33
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
34
|
+
import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
|
|
21
35
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
22
36
|
import { useWheel } from '../../hooks/useWheel'
|
|
23
|
-
import {
|
|
24
|
-
DEFAULT_DRAG_SENSITIVITY,
|
|
25
|
-
DEFAULT_KEYBOARD_OPTIONS,
|
|
26
|
-
DEFAULT_WHEEL_OPTIONS,
|
|
27
|
-
} from '../../input-event'
|
|
28
37
|
|
|
29
|
-
import { NumberInputProvider } from './context'
|
|
38
|
+
import { NumberInputGestureProvider, NumberInputProvider } from './context'
|
|
30
39
|
import { DecrementStepper } from './DecrementStepper'
|
|
31
40
|
import { IncrementStepper } from './IncrementStepper'
|
|
32
41
|
import { InputField } from './InputField'
|
|
@@ -83,6 +92,9 @@ export interface NumberInputProps {
|
|
|
83
92
|
* Read a value back out of the text. Has to undo `format`.
|
|
84
93
|
*
|
|
85
94
|
* Text with no number in it reads as `NaN`, which leaves the value alone.
|
|
95
|
+
* The default, `parseNumberText` from `@tremolo-ui/dom`, reads the number
|
|
96
|
+
* with the unit around it left out, and is `NaN` for one it cannot read
|
|
97
|
+
* whole: `1,000`, `1:30`, or a number with a unit in front, such as `L 30`.
|
|
86
98
|
*/
|
|
87
99
|
parse?: (text: string) => number
|
|
88
100
|
|
|
@@ -157,10 +169,17 @@ export interface NumberInputProps {
|
|
|
157
169
|
* @default false
|
|
158
170
|
*/
|
|
159
171
|
pointerLock?: boolean
|
|
172
|
+
/**
|
|
173
|
+
* The cursor to show while dragging a `Stepper`. It is set on the stepper
|
|
174
|
+
* being dragged, so it stays while the pointer is outside it.
|
|
175
|
+
*
|
|
176
|
+
* @default 'ns-resize'
|
|
177
|
+
*/
|
|
178
|
+
dragCursor?: CSSProperties['cursor']
|
|
160
179
|
|
|
161
180
|
/**
|
|
162
181
|
* Select the text when `InputField` takes focus: `'all'` selects all of it,
|
|
163
|
-
* `'number'` only the
|
|
182
|
+
* `'number'` only the number, leaving the unit the format put around it,
|
|
164
183
|
* and `'none'` leaves the caret where the click put it.
|
|
165
184
|
* @default 'none'
|
|
166
185
|
*/
|
|
@@ -214,7 +233,7 @@ export interface NumberInputProps {
|
|
|
214
233
|
* Make the value unchangeable.
|
|
215
234
|
* The parts carry `data-readonly` while it is set.
|
|
216
235
|
*/
|
|
217
|
-
|
|
236
|
+
readOnly?: boolean
|
|
218
237
|
|
|
219
238
|
/**
|
|
220
239
|
* Called with the new value. While the user types, it is called for every
|
|
@@ -222,6 +241,27 @@ export interface NumberInputProps {
|
|
|
222
241
|
* again if clamping changes the value.
|
|
223
242
|
*/
|
|
224
243
|
onChange?: (value: number) => void
|
|
244
|
+
/**
|
|
245
|
+
* Called when a change of the value starts — a press on a stepper, the
|
|
246
|
+
* first wheel notch, arrow key or typed character — with the value before
|
|
247
|
+
* it and what it is made with. A host recording automation can treat the
|
|
248
|
+
* input as touched from here until `onChangeEnd`.
|
|
249
|
+
*/
|
|
250
|
+
onChangeStart?: (value: number, source: ChangeSource) => void
|
|
251
|
+
/**
|
|
252
|
+
* Called when the change ends, with the value it ended on: when the stepper
|
|
253
|
+
* is released, when typed text is committed, or `changeEndDelay` after the
|
|
254
|
+
* last wheel notch or arrow key.
|
|
255
|
+
*/
|
|
256
|
+
onChangeEnd?: (value: number, source: ChangeSource) => void
|
|
257
|
+
/**
|
|
258
|
+
* How long after the last wheel notch, arrow key or typed character the
|
|
259
|
+
* change counts as over, in milliseconds. None of them has an event that
|
|
260
|
+
* says it is done.
|
|
261
|
+
*
|
|
262
|
+
* @default 500
|
|
263
|
+
*/
|
|
264
|
+
changeEndDelay?: number
|
|
225
265
|
|
|
226
266
|
/**
|
|
227
267
|
* The input renders exactly what you compose here; there is no default
|
|
@@ -237,6 +277,13 @@ export interface NumberInputProps {
|
|
|
237
277
|
* </NumberInput.Root>
|
|
238
278
|
*/
|
|
239
279
|
children: ReactNode
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Receives `focus` and `blur`, which act on the input of `InputField` — the
|
|
283
|
+
* element that takes the focus — and do nothing while
|
|
284
|
+
* the input is disabled. `ref` reaches the root element itself.
|
|
285
|
+
*/
|
|
286
|
+
actionsRef?: Ref<NumberInputMethods>
|
|
240
287
|
}
|
|
241
288
|
|
|
242
289
|
export interface NumberInputMethods {
|
|
@@ -244,24 +291,12 @@ export interface NumberInputMethods {
|
|
|
244
291
|
blur: () => void
|
|
245
292
|
}
|
|
246
293
|
|
|
247
|
-
/**
|
|
248
|
-
* A number at the start of the text, and nothing read after it. A half-typed
|
|
249
|
-
* entry still yields the number in front of it, but text with no number is
|
|
250
|
-
* `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
|
|
251
|
-
*/
|
|
252
|
-
const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/
|
|
253
|
-
|
|
254
294
|
const defaultFormat = (value: number) => String(value)
|
|
255
295
|
|
|
256
|
-
const defaultParse = (text: string) => {
|
|
257
|
-
const match = text.match(LEADING_NUMBER)
|
|
258
|
-
return match ? Number(match[1]) : NaN
|
|
259
|
-
}
|
|
260
|
-
|
|
261
296
|
type Props = NumberInputProps &
|
|
262
297
|
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
|
|
263
298
|
|
|
264
|
-
export const Root = /* @__PURE__ */ forwardRef<
|
|
299
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
265
300
|
(
|
|
266
301
|
{
|
|
267
302
|
value,
|
|
@@ -277,16 +312,21 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
277
312
|
drag = 1,
|
|
278
313
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
279
314
|
pointerLock = false,
|
|
315
|
+
dragCursor = 'ns-resize',
|
|
280
316
|
selectOnFocus = 'none',
|
|
281
317
|
unformatOnFocus = false,
|
|
282
318
|
keepCaretOnStep = false,
|
|
283
319
|
blurOnEnter = true,
|
|
284
320
|
disabled = false,
|
|
285
|
-
|
|
321
|
+
readOnly = false,
|
|
286
322
|
className,
|
|
287
323
|
style,
|
|
288
324
|
onChange,
|
|
325
|
+
onChangeStart,
|
|
326
|
+
onChangeEnd,
|
|
327
|
+
changeEndDelay,
|
|
289
328
|
children,
|
|
329
|
+
actionsRef,
|
|
290
330
|
...props
|
|
291
331
|
}: Props,
|
|
292
332
|
forwardedRef,
|
|
@@ -298,36 +338,25 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
298
338
|
* but the half-finished entry that has no value to be derived from yet.
|
|
299
339
|
*/
|
|
300
340
|
const [draft, setDraft] = useState<string | null>(null)
|
|
301
|
-
const inactive = disabled ||
|
|
341
|
+
const inactive = disabled || readOnly
|
|
342
|
+
|
|
343
|
+
const gesture = useChangeGesture(
|
|
344
|
+
value,
|
|
345
|
+
{ onChangeStart, onChangeEnd, changeEndDelay },
|
|
346
|
+
inactive,
|
|
347
|
+
)
|
|
302
348
|
|
|
303
349
|
// --- interpret props ---
|
|
304
350
|
const format = formatProp ?? defaultFormat
|
|
305
|
-
const parse = parseProp ??
|
|
351
|
+
const parse = parseProp ?? parseNumberText
|
|
306
352
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// overflowing the span calculation used by a scale.
|
|
310
|
-
const range: ValueRange = useMemo(
|
|
311
|
-
() => ({
|
|
312
|
-
min: (clampValue ? min : undefined) ?? Number.MIN_SAFE_INTEGER,
|
|
313
|
-
max: (clampValue ? max : undefined) ?? Number.MAX_SAFE_INTEGER,
|
|
314
|
-
step,
|
|
315
|
-
scale,
|
|
316
|
-
}),
|
|
353
|
+
const { normalized: range, raw: rawRange } = useMemo(
|
|
354
|
+
() => numberInputRanges({ min, max, step, scale, clampValue }),
|
|
317
355
|
[clampValue, min, max, step, scale],
|
|
318
356
|
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// safe-integer range.
|
|
323
|
-
const rawRange: ValueRange = useMemo(
|
|
324
|
-
() => ({
|
|
325
|
-
min: (clampValue ? min : undefined) ?? -Number.MAX_VALUE,
|
|
326
|
-
max: (clampValue ? max : undefined) ?? Number.MAX_VALUE,
|
|
327
|
-
step,
|
|
328
|
-
scale,
|
|
329
|
-
}),
|
|
330
|
-
[clampValue, min, max, step, scale],
|
|
357
|
+
const ranges = useMemo(
|
|
358
|
+
() => ({ normalized: range, raw: rawRange }),
|
|
359
|
+
[range, rawRange],
|
|
331
360
|
)
|
|
332
361
|
|
|
333
362
|
// The pipeline range stands in the safe-integer range when an end is
|
|
@@ -351,9 +380,9 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
351
380
|
|
|
352
381
|
const text = draft ?? format(value)
|
|
353
382
|
const editing = draft !== null
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
383
|
+
const bounds = numberInputBounds(value, { min, max, clampValue })
|
|
384
|
+
const { atMin, atMax } = bounds
|
|
385
|
+
const outOfRange = !editing && bounds.outOfRange
|
|
357
386
|
|
|
358
387
|
// --- internal functions ---
|
|
359
388
|
const handleDraft = useCallback(
|
|
@@ -363,35 +392,55 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
363
392
|
// Deliberately unclamped: clamping here would make "1500" impossible
|
|
364
393
|
// to type into an input whose max is 100.
|
|
365
394
|
const parsed = parse(next)
|
|
366
|
-
if (Number.isFinite(parsed))
|
|
395
|
+
if (!Number.isFinite(parsed)) return
|
|
396
|
+
if (parsed !== value) {
|
|
397
|
+
gesture.pulse('keyboard')
|
|
398
|
+
gesture.changed(parsed)
|
|
399
|
+
}
|
|
400
|
+
onChange?.(parsed)
|
|
367
401
|
},
|
|
368
|
-
[inactive, parse, onChange],
|
|
402
|
+
[inactive, parse, onChange, value, gesture],
|
|
369
403
|
)
|
|
370
404
|
|
|
371
405
|
const changeValue = useCallback(
|
|
372
406
|
(next: number) => {
|
|
373
407
|
if (inactive) return
|
|
374
408
|
setDraft(null)
|
|
375
|
-
if (next
|
|
409
|
+
if (next === value) return
|
|
410
|
+
gesture.changed(next)
|
|
411
|
+
onChange?.(next)
|
|
376
412
|
},
|
|
377
|
-
[inactive, value, onChange],
|
|
413
|
+
[inactive, value, onChange, gesture],
|
|
378
414
|
)
|
|
379
415
|
|
|
380
416
|
const commitDraft = useCallback(() => {
|
|
381
417
|
if (draft === null || inactive) return
|
|
382
|
-
const
|
|
418
|
+
const committed = commitNumberInputText(draft, parse, {
|
|
419
|
+
min,
|
|
420
|
+
max,
|
|
421
|
+
clampValue,
|
|
422
|
+
})
|
|
383
423
|
// Text with no number in it is not a value. Dropping the draft puts the
|
|
384
|
-
// input back to what it was showing
|
|
385
|
-
|
|
386
|
-
if (!Number.isFinite(parsed)) {
|
|
424
|
+
// input back to what it was showing.
|
|
425
|
+
if (committed === null) {
|
|
387
426
|
setDraft(null)
|
|
388
427
|
return
|
|
389
428
|
}
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
if (clampValue && max !== undefined) committed = Math.min(committed, max)
|
|
429
|
+
// The typing was a gesture of its own, and committing is where it ends.
|
|
430
|
+
if (committed !== value) gesture.pulse('keyboard')
|
|
393
431
|
changeValue(committed)
|
|
394
|
-
|
|
432
|
+
gesture.end()
|
|
433
|
+
}, [
|
|
434
|
+
draft,
|
|
435
|
+
inactive,
|
|
436
|
+
parse,
|
|
437
|
+
clampValue,
|
|
438
|
+
min,
|
|
439
|
+
max,
|
|
440
|
+
changeValue,
|
|
441
|
+
value,
|
|
442
|
+
gesture,
|
|
443
|
+
])
|
|
395
444
|
|
|
396
445
|
const nudge = useCallback(
|
|
397
446
|
(
|
|
@@ -399,26 +448,21 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
399
448
|
option: ModifierValue<InputEventOption>,
|
|
400
449
|
modifiers?: ModifierState,
|
|
401
450
|
) => {
|
|
402
|
-
const [mode] = selectModifier(option, modifiers).value
|
|
403
451
|
changeValue(
|
|
404
|
-
|
|
405
|
-
value,
|
|
406
|
-
direction,
|
|
407
|
-
option,
|
|
408
|
-
mode === 'raw' ? rawRange : range,
|
|
409
|
-
modifiers,
|
|
410
|
-
),
|
|
452
|
+
nudgeNumberInput(value, direction, option, ranges, modifiers),
|
|
411
453
|
)
|
|
412
454
|
},
|
|
413
|
-
[changeValue, value,
|
|
455
|
+
[changeValue, value, ranges],
|
|
414
456
|
)
|
|
415
457
|
|
|
416
458
|
// --- hooks ---
|
|
417
459
|
const wheelRefCallback = useWheel<HTMLDivElement>(
|
|
418
460
|
(event) => {
|
|
419
|
-
|
|
461
|
+
const direction = wheelDirection(event)
|
|
462
|
+
if (!wheel || inactive || direction === null) return
|
|
420
463
|
event.preventDefault()
|
|
421
|
-
|
|
464
|
+
gesture.pulse('wheel')
|
|
465
|
+
nudge(direction, wheel, event)
|
|
422
466
|
},
|
|
423
467
|
{ requireFocus: true },
|
|
424
468
|
)
|
|
@@ -431,7 +475,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
431
475
|
step,
|
|
432
476
|
scale,
|
|
433
477
|
disabled,
|
|
434
|
-
|
|
478
|
+
readOnly,
|
|
435
479
|
clampValue,
|
|
436
480
|
range,
|
|
437
481
|
rawRange,
|
|
@@ -441,12 +485,13 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
441
485
|
outOfRange,
|
|
442
486
|
dragSensitivity,
|
|
443
487
|
pointerLock,
|
|
488
|
+
dragCursor,
|
|
444
489
|
selectOnFocus,
|
|
445
490
|
unformatOnFocus,
|
|
446
491
|
keepCaretOnStep,
|
|
447
492
|
blurOnEnter,
|
|
448
|
-
atMin
|
|
449
|
-
atMax
|
|
493
|
+
atMin,
|
|
494
|
+
atMax,
|
|
450
495
|
drag,
|
|
451
496
|
format,
|
|
452
497
|
parse,
|
|
@@ -463,7 +508,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
463
508
|
step,
|
|
464
509
|
scale,
|
|
465
510
|
disabled,
|
|
466
|
-
|
|
511
|
+
readOnly,
|
|
467
512
|
clampValue,
|
|
468
513
|
range,
|
|
469
514
|
rawRange,
|
|
@@ -471,9 +516,12 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
471
516
|
text,
|
|
472
517
|
editing,
|
|
473
518
|
outOfRange,
|
|
519
|
+
atMin,
|
|
520
|
+
atMax,
|
|
474
521
|
drag,
|
|
475
522
|
dragSensitivity,
|
|
476
523
|
pointerLock,
|
|
524
|
+
dragCursor,
|
|
477
525
|
selectOnFocus,
|
|
478
526
|
unformatOnFocus,
|
|
479
527
|
keepCaretOnStep,
|
|
@@ -487,7 +535,14 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
487
535
|
],
|
|
488
536
|
)
|
|
489
537
|
|
|
490
|
-
|
|
538
|
+
// Composed once, so React attaches the refs a single time instead of
|
|
539
|
+
// detaching and re-attaching on every render.
|
|
540
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
541
|
+
forwardedRef,
|
|
542
|
+
wheelRefCallback,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
useImperativeHandle(actionsRef, () => {
|
|
491
546
|
return {
|
|
492
547
|
focus() {
|
|
493
548
|
if (!disabled) inputRef.current?.focus()
|
|
@@ -498,18 +553,47 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
498
553
|
}
|
|
499
554
|
}, [disabled])
|
|
500
555
|
|
|
556
|
+
const pressed = useRef(false)
|
|
557
|
+
const gestureContext = useMemo(
|
|
558
|
+
() => ({
|
|
559
|
+
press: () => {
|
|
560
|
+
if (inactive) return
|
|
561
|
+
gesture.hold('pointer')
|
|
562
|
+
// A press on a stepper reaches `Stepper` too; one release will do.
|
|
563
|
+
if (pressed.current) return
|
|
564
|
+
pressed.current = true
|
|
565
|
+
// Released wherever the pointer ends up: the stepper drag captures
|
|
566
|
+
// it, and a press that wanders off the button still ends.
|
|
567
|
+
const release = () => {
|
|
568
|
+
pressed.current = false
|
|
569
|
+
gesture.end()
|
|
570
|
+
window.removeEventListener('pointerup', release)
|
|
571
|
+
window.removeEventListener('pointercancel', release)
|
|
572
|
+
}
|
|
573
|
+
window.addEventListener('pointerup', release)
|
|
574
|
+
window.addEventListener('pointercancel', release)
|
|
575
|
+
},
|
|
576
|
+
key: () => {
|
|
577
|
+
if (!inactive) gesture.pulse('keyboard')
|
|
578
|
+
},
|
|
579
|
+
}),
|
|
580
|
+
[gesture, inactive],
|
|
581
|
+
)
|
|
582
|
+
|
|
501
583
|
return (
|
|
502
584
|
<NumberInputProvider value={context}>
|
|
503
|
-
<
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
585
|
+
<NumberInputGestureProvider value={gestureContext}>
|
|
586
|
+
<div
|
|
587
|
+
ref={rootRefCallback}
|
|
588
|
+
className={className}
|
|
589
|
+
data-disabled={disabled ? '' : undefined}
|
|
590
|
+
data-readonly={readOnly ? '' : undefined}
|
|
591
|
+
style={style}
|
|
592
|
+
{...props}
|
|
593
|
+
>
|
|
594
|
+
{children}
|
|
595
|
+
</div>
|
|
596
|
+
</NumberInputGestureProvider>
|
|
513
597
|
</NumberInputProvider>
|
|
514
598
|
)
|
|
515
599
|
},
|