@tremolo-ui/react 0.5.0 → 0.7.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/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +1669 -541
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1571 -486
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1571 -486
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1640 -538
- package/dist/index.js.map +1 -1
- package/package.json +29 -27
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/DropZone/index.tsx +100 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +144 -0
- package/src/components/Knob/ActiveLine.tsx +7 -6
- package/src/components/Knob/InactiveLine.tsx +9 -10
- package/src/components/Knob/SVGRoot.tsx +5 -3
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +17 -1
- package/src/components/Knob/index.tsx +156 -60
- package/src/components/NumberInput/DecrementStepper.tsx +3 -10
- package/src/components/NumberInput/IncrementStepper.tsx +3 -10
- package/src/components/NumberInput/InputField.tsx +185 -108
- package/src/components/NumberInput/Stepper.tsx +120 -84
- package/src/components/NumberInput/StepperButton.tsx +100 -0
- package/src/components/NumberInput/context.tsx +34 -8
- package/src/components/NumberInput/index.tsx +270 -82
- package/src/components/Piano/index.tsx +363 -200
- package/src/components/PointsEditor/Background.tsx +13 -10
- package/src/components/PointsEditor/Container.tsx +83 -29
- package/src/components/PointsEditor/Point.tsx +241 -79
- package/src/components/PointsEditor/SelectionBox.tsx +48 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -41
- package/src/components/Slider/Marks.tsx +48 -19
- package/src/components/Slider/MarksOption.tsx +64 -45
- package/src/components/Slider/Thumb.tsx +107 -55
- package/src/components/Slider/Track.tsx +66 -62
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +136 -77
- package/src/components/Slider/type.ts +29 -9
- package/src/components/XYPad/Area.tsx +46 -41
- package/src/components/XYPad/Thumb.tsx +131 -64
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +183 -53
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +12 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/components/_util/decimal-digits.ts +23 -0
- package/src/components/_util/visually-hidden.ts +25 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCallbackRef.ts +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +169 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +51 -12
- package/src/hooks/useDragValue.ts +71 -11
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useEventListener.ts +10 -6
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +45 -25
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -501
- package/src/Globals.d.ts +0 -3
- package/src/components/Knob/index.css +0 -81
- package/src/components/NumberInput/index.css +0 -121
- package/src/components/NumberInput/stepperButton.tsx +0 -90
- package/src/components/Piano/index.css +0 -90
- package/src/components/PointsEditor/index.css +0 -35
- package/src/components/Slider/index.css +0 -125
- package/src/components/XYPad/index.css +0 -60
- package/src/components/_util/index.ts +0 -28
- package/src/styles/global.css +0 -19
- /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createContext, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
type InputEventOption,
|
|
5
|
+
type ModifierState,
|
|
6
|
+
type ModifierValue,
|
|
7
|
+
} from '@tremolo-ui/dom'
|
|
8
|
+
import { type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
4
9
|
|
|
5
10
|
export type NumberInputContextValue = {
|
|
6
11
|
value: number
|
|
@@ -21,14 +26,29 @@ export type NumberInputContextValue = {
|
|
|
21
26
|
clampValue: boolean
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
|
-
* The effective scaling for `applyDelta
|
|
25
|
-
*
|
|
29
|
+
* The effective scaling for `applyDelta`, with unbounded ends filled in.
|
|
30
|
+
* Ignores `min` / `max` when `clampValue` is off.
|
|
26
31
|
*/
|
|
27
32
|
range: ValueRange
|
|
33
|
+
/** The effective range for raw input, whose open ends cover finite numbers. */
|
|
34
|
+
rawRange: ValueRange
|
|
28
35
|
|
|
29
|
-
keyboard: InputEventOption | null
|
|
36
|
+
keyboard: ModifierValue<InputEventOption> | null
|
|
30
37
|
/** Pixels of vertical drag on `Stepper` that move the value by one `step`. */
|
|
31
38
|
drag: number | null
|
|
39
|
+
/** How much a `Stepper` drag counts, per modifier key. */
|
|
40
|
+
dragSensitivity: ModifierValue<number>
|
|
41
|
+
/** Whether a `Stepper` drag hides the pointer and reads its movement. */
|
|
42
|
+
pointerLock: boolean
|
|
43
|
+
|
|
44
|
+
/** What `InputField` selects when it takes focus. */
|
|
45
|
+
selectOnFocus: 'all' | 'number' | 'none'
|
|
46
|
+
/** Whether `InputField` shows the plain value while it has focus. */
|
|
47
|
+
unformatOnFocus: boolean
|
|
48
|
+
/** Whether `InputField` puts the caret back after an arrow key steps the value. */
|
|
49
|
+
keepCaretOnStep: boolean
|
|
50
|
+
/** Whether Enter leaves `InputField` as well as committing. */
|
|
51
|
+
blurOnEnter: boolean
|
|
32
52
|
|
|
33
53
|
/** What the input shows: the draft while editing, the formatted value otherwise. */
|
|
34
54
|
text: string
|
|
@@ -44,18 +64,23 @@ export type NumberInputContextValue = {
|
|
|
44
64
|
|
|
45
65
|
/** Typing. Replaces the draft and reports the parsed value, unclamped. */
|
|
46
66
|
setDraft: (text: string) => void
|
|
47
|
-
/** Blur or Enter.
|
|
67
|
+
/** Blur or Enter. Optionally clamps the draft, reports it, and drops it. */
|
|
48
68
|
commitDraft: () => void
|
|
49
69
|
/** Drops the draft and replaces the value: steppers, wheel, keyboard, drag. */
|
|
50
70
|
changeValue: (next: number) => void
|
|
51
71
|
/** Moves the value by one `option` in `direction`, normally +1 or -1. */
|
|
52
|
-
nudge: (
|
|
72
|
+
nudge: (
|
|
73
|
+
direction: number,
|
|
74
|
+
option: ModifierValue<InputEventOption>,
|
|
75
|
+
modifiers?: ModifierState,
|
|
76
|
+
) => void
|
|
53
77
|
|
|
54
78
|
/** `InputField` registers itself here; `Root` focuses it through its methods. */
|
|
55
79
|
inputRef: RefObject<HTMLInputElement | null>
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
const NumberInputContext =
|
|
82
|
+
const NumberInputContext =
|
|
83
|
+
/* @__PURE__ */ createContext<NumberInputContextValue | null>(null)
|
|
59
84
|
|
|
60
85
|
export const NumberInputProvider = NumberInputContext.Provider
|
|
61
86
|
|
|
@@ -81,7 +106,8 @@ export type StepperContextValue = {
|
|
|
81
106
|
draggingRef: RefObject<boolean>
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
const StepperContext =
|
|
109
|
+
const StepperContext =
|
|
110
|
+
/* @__PURE__ */ createContext<StepperContextValue | null>(null)
|
|
85
111
|
|
|
86
112
|
export const StepperProvider = StepperContext.Provider
|
|
87
113
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import {
|
|
3
2
|
ComponentPropsWithoutRef,
|
|
4
|
-
CSSProperties,
|
|
5
3
|
forwardRef,
|
|
6
4
|
ReactNode,
|
|
7
5
|
useCallback,
|
|
@@ -13,17 +11,20 @@ import {
|
|
|
13
11
|
|
|
14
12
|
import {
|
|
15
13
|
applyDelta,
|
|
16
|
-
clamp,
|
|
17
|
-
formatValue,
|
|
18
14
|
InputEventOption,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from '@tremolo-ui/functions'
|
|
15
|
+
ModifierState,
|
|
16
|
+
type ModifierValue,
|
|
17
|
+
selectModifier,
|
|
18
|
+
} from '@tremolo-ui/dom'
|
|
19
|
+
import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
25
20
|
|
|
21
|
+
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
26
22
|
import { useWheel } from '../../hooks/useWheel'
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
25
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
26
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
27
|
+
} from '../../input-event'
|
|
27
28
|
|
|
28
29
|
import { NumberInputProvider } from './context'
|
|
29
30
|
import { DecrementStepper } from './DecrementStepper'
|
|
@@ -38,8 +39,27 @@ export interface NumberInputProps {
|
|
|
38
39
|
*/
|
|
39
40
|
value: number
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Lowest value. Leave it out for no lower bound.
|
|
44
|
+
*
|
|
45
|
+
* It is enforced when a value is committed or stepped, while `clampValue` is
|
|
46
|
+
* on. A value below it carries `data-out-of-range` on `InputField`.
|
|
47
|
+
*/
|
|
41
48
|
min?: number
|
|
49
|
+
/**
|
|
50
|
+
* Highest value. Leave it out for no upper bound.
|
|
51
|
+
*
|
|
52
|
+
* It is enforced when a value is committed or stepped, while `clampValue` is
|
|
53
|
+
* on. A value above it carries `data-out-of-range` on `InputField`.
|
|
54
|
+
*/
|
|
42
55
|
max?: number
|
|
56
|
+
/**
|
|
57
|
+
* Granularity of the value. The steppers, a drag on `Stepper`, the wheel and
|
|
58
|
+
* the arrow keys snap it to multiples of `step`; a typed value is left as it
|
|
59
|
+
* is.
|
|
60
|
+
*
|
|
61
|
+
* @default 1
|
|
62
|
+
*/
|
|
43
63
|
step?: number
|
|
44
64
|
/**
|
|
45
65
|
* How the value is distributed across the travel of a drag or a
|
|
@@ -50,26 +70,20 @@ export interface NumberInputProps {
|
|
|
50
70
|
scale?: Scale
|
|
51
71
|
|
|
52
72
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
73
|
+
* Render the value as text. Plain digits by default.
|
|
74
|
+
*
|
|
75
|
+
* `unitFormat` from `@tremolo-ui/functions` builds this and `parse` together
|
|
76
|
+
* for a unit, as a pair to spread into the root.
|
|
55
77
|
*
|
|
56
78
|
* @example
|
|
57
|
-
*
|
|
58
|
-
* units={[['Hz', 1], ['kHz', 1000]]}
|
|
59
|
-
* units={[['ms', 1], ['s', 1000]]}
|
|
79
|
+
* <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} />
|
|
60
80
|
*/
|
|
61
|
-
|
|
81
|
+
format?: (value: number) => string
|
|
62
82
|
/**
|
|
63
|
-
*
|
|
83
|
+
* Read a value back out of the text. Has to undo `format`.
|
|
64
84
|
*
|
|
65
|
-
*
|
|
66
|
-
* // units={[['Hz', 1], ['kHz', 1000]]} digit={2}
|
|
67
|
-
* // 100 -> 100.00Hz, 1600 -> 1.60kHz
|
|
85
|
+
* Text with no number in it reads as `NaN`, which leaves the value alone.
|
|
68
86
|
*/
|
|
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
87
|
parse?: (text: string) => number
|
|
74
88
|
|
|
75
89
|
/**
|
|
@@ -80,37 +94,133 @@ export interface NumberInputProps {
|
|
|
80
94
|
clampValue?: boolean
|
|
81
95
|
|
|
82
96
|
/**
|
|
83
|
-
*
|
|
84
|
-
* scrolling past the input
|
|
85
|
-
*
|
|
97
|
+
* How much one notch of the wheel moves the value. It only acts while the
|
|
98
|
+
* focus is inside, so that scrolling the page past the input leaves it
|
|
99
|
+
* alone.
|
|
100
|
+
*
|
|
101
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
102
|
+
* range between `min` and `max`. The result is snapped to `step`, except for
|
|
103
|
+
* an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
|
|
104
|
+
* the wheel off.
|
|
105
|
+
*
|
|
106
|
+
* @default ['raw', 1]
|
|
86
107
|
*/
|
|
87
|
-
wheel?: InputEventOption | null
|
|
108
|
+
wheel?: ModifierValue<InputEventOption> | null
|
|
88
109
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
110
|
+
* How much one arrow key press moves the value.
|
|
111
|
+
*
|
|
112
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
113
|
+
* range between `min` and `max`. The result is snapped to `step`, except for
|
|
114
|
+
* an amount set on a modifier key, which is what lets shift move off the
|
|
115
|
+
* grid. `null` turns the arrow keys off.
|
|
116
|
+
*
|
|
117
|
+
* The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
|
|
118
|
+
* the amount to match: 1 would round straight back to where it started, and
|
|
119
|
+
* a development build warns about it.
|
|
120
|
+
*
|
|
121
|
+
* @default { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
91
122
|
*/
|
|
92
|
-
keyboard?: InputEventOption | null
|
|
123
|
+
keyboard?: ModifierValue<InputEventOption> | null
|
|
93
124
|
/**
|
|
94
125
|
* Pixels of vertical drag on `Stepper` that move the value by one `step`.
|
|
95
|
-
*
|
|
126
|
+
* `null` turns the drag off.
|
|
96
127
|
* @default 1
|
|
97
128
|
*/
|
|
98
129
|
drag?: number | null
|
|
99
130
|
|
|
100
131
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
132
|
+
* How much a `Stepper` drag counts, per modifier key.
|
|
133
|
+
*
|
|
134
|
+
* `1` is `drag` pixels per `step`; `0.1` makes the same movement cover a
|
|
135
|
+
* tenth of that. Shift is bound to `0.1` by default, to match what it does
|
|
136
|
+
* on the arrow keys.
|
|
137
|
+
*
|
|
138
|
+
* A modifier entry is not snapped to `step`, which is what lets a finer
|
|
139
|
+
* amount move at all.
|
|
140
|
+
*
|
|
141
|
+
* @default { default: 1, shift: 0.1 }
|
|
142
|
+
*/
|
|
143
|
+
dragSensitivity?: ModifierValue<number>
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Hide the cursor while dragging a `Stepper` and read the pointer movement
|
|
147
|
+
* directly, rather than letting it wander off across the screen.
|
|
148
|
+
*
|
|
149
|
+
* The drag is already relative, so the pointer position carries nothing —
|
|
150
|
+
* but it still runs into the edge of the screen, where the operating system
|
|
151
|
+
* pins it and the coordinates stop changing.
|
|
152
|
+
*
|
|
153
|
+
* Off by default: the browser shows its own notice, Esc takes the lock back,
|
|
154
|
+
* and the request can be refused. A refused request is not an error, and the
|
|
155
|
+
* drag carries on as an ordinary one.
|
|
156
|
+
*
|
|
157
|
+
* @default false
|
|
158
|
+
*/
|
|
159
|
+
pointerLock?: boolean
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Select the text when `InputField` takes focus: `'all'` selects all of it,
|
|
163
|
+
* `'number'` only the leading number, leaving whatever the format appended,
|
|
164
|
+
* and `'none'` leaves the caret where the click put it.
|
|
165
|
+
* @default 'none'
|
|
166
|
+
*/
|
|
167
|
+
selectOnFocus?: 'all' | 'number' | 'none'
|
|
168
|
+
/**
|
|
169
|
+
* Show the plain value while `InputField` has focus, dropping whatever
|
|
170
|
+
* `format` put around it: an input reading `1.23kHz` shows `1230` to be
|
|
171
|
+
* typed over.
|
|
172
|
+
*
|
|
173
|
+
* The number shown is the value itself, not the number inside the formatted
|
|
174
|
+
* text. Those differ whenever the format scales — `1.23` out of `1.23kHz`
|
|
175
|
+
* would read back as 1.23 and lose a factor of a thousand — and it is also
|
|
176
|
+
* why a rounded display no longer becomes the value: `1.6` shown as `2Hz`
|
|
177
|
+
* offers `1.6` for editing, not `2`.
|
|
178
|
+
*
|
|
179
|
+
* @default false
|
|
180
|
+
*/
|
|
181
|
+
unformatOnFocus?: boolean
|
|
182
|
+
/**
|
|
183
|
+
* Put the caret in `InputField` back where it was after an arrow key steps
|
|
184
|
+
* the value.
|
|
185
|
+
*
|
|
186
|
+
* A controlled input whose `value` is replaced drops the caret at the end,
|
|
187
|
+
* so without this the second press of a repeated step always acts on the
|
|
188
|
+
* last digit. With it, the digit under the caret stays under the caret and
|
|
189
|
+
* a column can be held while stepping.
|
|
190
|
+
*
|
|
191
|
+
* The position is measured from the decimal point rather than from either
|
|
192
|
+
* end, so it survives the number growing or shrinking: the caret between
|
|
193
|
+
* `9` and `.9` is still between `10` and `.0`.
|
|
194
|
+
*
|
|
195
|
+
* It only restores the caret. Which digit it sits on does not change the
|
|
196
|
+
* size of the step — that is `keyboard`'s to say.
|
|
197
|
+
*
|
|
198
|
+
* @default false
|
|
199
|
+
*/
|
|
200
|
+
keepCaretOnStep?: boolean
|
|
201
|
+
/**
|
|
202
|
+
* Commit and leave `InputField` when Enter is pressed. Enter commits either
|
|
203
|
+
* way.
|
|
204
|
+
* @default true
|
|
205
|
+
*/
|
|
206
|
+
blurOnEnter?: boolean
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Make the input unchangeable and remove it from the tab order.
|
|
210
|
+
* The parts carry `data-disabled` while it is set.
|
|
104
211
|
*/
|
|
105
212
|
disabled?: boolean
|
|
106
213
|
/**
|
|
107
214
|
* Make the value unchangeable.
|
|
108
|
-
*
|
|
215
|
+
* The parts carry `data-readonly` while it is set.
|
|
109
216
|
*/
|
|
110
217
|
readonly?: boolean
|
|
111
218
|
|
|
112
|
-
|
|
113
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Called with the new value. While the user types, it is called for every
|
|
221
|
+
* entry that reads as a number, unclamped; committing the entry calls it
|
|
222
|
+
* again if clamping changes the value.
|
|
223
|
+
*/
|
|
114
224
|
onChange?: (value: number) => void
|
|
115
225
|
|
|
116
226
|
/**
|
|
@@ -134,10 +244,24 @@ export interface NumberInputMethods {
|
|
|
134
244
|
blur: () => void
|
|
135
245
|
}
|
|
136
246
|
|
|
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
|
+
const defaultFormat = (value: number) => String(value)
|
|
255
|
+
|
|
256
|
+
const defaultParse = (text: string) => {
|
|
257
|
+
const match = text.match(LEADING_NUMBER)
|
|
258
|
+
return match ? Number(match[1]) : NaN
|
|
259
|
+
}
|
|
260
|
+
|
|
137
261
|
type Props = NumberInputProps &
|
|
138
262
|
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
|
|
139
263
|
|
|
140
|
-
export const Root = forwardRef<NumberInputMethods, Props>(
|
|
264
|
+
export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
141
265
|
(
|
|
142
266
|
{
|
|
143
267
|
value,
|
|
@@ -145,14 +269,18 @@ export const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
145
269
|
max,
|
|
146
270
|
step = 1,
|
|
147
271
|
scale = linearScale,
|
|
148
|
-
units,
|
|
149
|
-
digit,
|
|
150
272
|
format: formatProp,
|
|
151
273
|
parse: parseProp,
|
|
152
274
|
clampValue = true,
|
|
153
|
-
wheel =
|
|
154
|
-
keyboard =
|
|
275
|
+
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
276
|
+
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
155
277
|
drag = 1,
|
|
278
|
+
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
279
|
+
pointerLock = false,
|
|
280
|
+
selectOnFocus = 'none',
|
|
281
|
+
unformatOnFocus = false,
|
|
282
|
+
keepCaretOnStep = false,
|
|
283
|
+
blurOnEnter = true,
|
|
156
284
|
disabled = false,
|
|
157
285
|
readonly = false,
|
|
158
286
|
className,
|
|
@@ -170,20 +298,15 @@ export const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
170
298
|
* but the half-finished entry that has no value to be derived from yet.
|
|
171
299
|
*/
|
|
172
300
|
const [draft, setDraft] = useState<string | null>(null)
|
|
301
|
+
const inactive = disabled || readonly
|
|
173
302
|
|
|
174
303
|
// --- interpret props ---
|
|
175
|
-
const format =
|
|
176
|
-
|
|
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
|
-
)
|
|
304
|
+
const format = formatProp ?? defaultFormat
|
|
305
|
+
const parse = parseProp ?? defaultParse
|
|
184
306
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
307
|
+
// Normalized input needs a finite span even when an end is unbounded or
|
|
308
|
+
// the caller opted out of clamping. Safe integers provide one without
|
|
309
|
+
// overflowing the span calculation used by a scale.
|
|
187
310
|
const range: ValueRange = useMemo(
|
|
188
311
|
() => ({
|
|
189
312
|
min: (clampValue ? min : undefined) ?? Number.MIN_SAFE_INTEGER,
|
|
@@ -194,53 +317,108 @@ export const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
194
317
|
[clampValue, min, max, step, scale],
|
|
195
318
|
)
|
|
196
319
|
|
|
320
|
+
// Raw input does not need a finite span for normalization, so its open
|
|
321
|
+
// ends can cover every finite JavaScript number instead of stopping at the
|
|
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],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
// The pipeline range stands in the safe-integer range when an end is
|
|
334
|
+
// open, and there is no travel to sample across that. Probing is skipped
|
|
335
|
+
// rather than run against a range nobody set.
|
|
336
|
+
const probeRange = useMemo(
|
|
337
|
+
() =>
|
|
338
|
+
min !== undefined && max !== undefined && min < max
|
|
339
|
+
? { min, max, step, scale }
|
|
340
|
+
: null,
|
|
341
|
+
[min, max, step, scale],
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
useCheckSteps({
|
|
345
|
+
component: 'NumberInput',
|
|
346
|
+
range: probeRange,
|
|
347
|
+
keyboard,
|
|
348
|
+
wheel,
|
|
349
|
+
format,
|
|
350
|
+
})
|
|
351
|
+
|
|
197
352
|
const text = draft ?? format(value)
|
|
198
|
-
const editing = draft
|
|
353
|
+
const editing = draft !== null
|
|
199
354
|
const outOfRange =
|
|
200
355
|
!editing &&
|
|
201
|
-
((min
|
|
356
|
+
((min !== undefined && value < min) || (max !== undefined && value > max))
|
|
202
357
|
|
|
203
358
|
// --- internal functions ---
|
|
204
359
|
const handleDraft = useCallback(
|
|
205
360
|
(next: string) => {
|
|
206
|
-
if (
|
|
361
|
+
if (inactive) return
|
|
207
362
|
setDraft(next)
|
|
208
363
|
// Deliberately unclamped: clamping here would make "1500" impossible
|
|
209
364
|
// to type into an input whose max is 100.
|
|
210
365
|
const parsed = parse(next)
|
|
211
366
|
if (Number.isFinite(parsed)) onChange?.(parsed)
|
|
212
367
|
},
|
|
213
|
-
[
|
|
368
|
+
[inactive, parse, onChange],
|
|
214
369
|
)
|
|
215
370
|
|
|
216
371
|
const changeValue = useCallback(
|
|
217
372
|
(next: number) => {
|
|
218
|
-
if (
|
|
373
|
+
if (inactive) return
|
|
219
374
|
setDraft(null)
|
|
220
|
-
if (next
|
|
375
|
+
if (next !== value) onChange?.(next)
|
|
221
376
|
},
|
|
222
|
-
[
|
|
377
|
+
[inactive, value, onChange],
|
|
223
378
|
)
|
|
224
379
|
|
|
225
380
|
const commitDraft = useCallback(() => {
|
|
226
|
-
if (draft
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
381
|
+
if (draft === null || inactive) return
|
|
382
|
+
const parsed = parse(draft)
|
|
383
|
+
// Text with no number in it is not a value. Dropping the draft puts the
|
|
384
|
+
// input back to what it was showing, rather than committing a zero the
|
|
385
|
+
// user never typed.
|
|
386
|
+
if (!Number.isFinite(parsed)) {
|
|
387
|
+
setDraft(null)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
let committed = parsed
|
|
391
|
+
if (clampValue && min !== undefined) committed = Math.max(committed, min)
|
|
392
|
+
if (clampValue && max !== undefined) committed = Math.min(committed, max)
|
|
393
|
+
changeValue(committed)
|
|
394
|
+
}, [draft, inactive, parse, clampValue, min, max, changeValue])
|
|
230
395
|
|
|
231
396
|
const nudge = useCallback(
|
|
232
|
-
(
|
|
233
|
-
|
|
397
|
+
(
|
|
398
|
+
direction: number,
|
|
399
|
+
option: ModifierValue<InputEventOption>,
|
|
400
|
+
modifiers?: ModifierState,
|
|
401
|
+
) => {
|
|
402
|
+
const [mode] = selectModifier(option, modifiers).value
|
|
403
|
+
changeValue(
|
|
404
|
+
applyDelta(
|
|
405
|
+
value,
|
|
406
|
+
direction,
|
|
407
|
+
option,
|
|
408
|
+
mode === 'raw' ? rawRange : range,
|
|
409
|
+
modifiers,
|
|
410
|
+
),
|
|
411
|
+
)
|
|
234
412
|
},
|
|
235
|
-
[changeValue, value, range],
|
|
413
|
+
[changeValue, value, rawRange, range],
|
|
236
414
|
)
|
|
237
415
|
|
|
238
416
|
// --- hooks ---
|
|
239
417
|
const wheelRefCallback = useWheel<HTMLDivElement>(
|
|
240
418
|
(event) => {
|
|
241
|
-
if (!wheel ||
|
|
419
|
+
if (!wheel || inactive || event.deltaY === 0) return
|
|
242
420
|
event.preventDefault()
|
|
243
|
-
nudge(-Math.sign(event.deltaY), wheel)
|
|
421
|
+
nudge(-Math.sign(event.deltaY), wheel, event)
|
|
244
422
|
},
|
|
245
423
|
{ requireFocus: true },
|
|
246
424
|
)
|
|
@@ -256,12 +434,19 @@ export const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
256
434
|
readonly,
|
|
257
435
|
clampValue,
|
|
258
436
|
range,
|
|
437
|
+
rawRange,
|
|
259
438
|
keyboard,
|
|
260
439
|
text,
|
|
261
440
|
editing,
|
|
262
441
|
outOfRange,
|
|
263
|
-
|
|
264
|
-
|
|
442
|
+
dragSensitivity,
|
|
443
|
+
pointerLock,
|
|
444
|
+
selectOnFocus,
|
|
445
|
+
unformatOnFocus,
|
|
446
|
+
keepCaretOnStep,
|
|
447
|
+
blurOnEnter,
|
|
448
|
+
atMin: clampValue && min !== undefined && value <= min,
|
|
449
|
+
atMax: clampValue && max !== undefined && value >= max,
|
|
265
450
|
drag,
|
|
266
451
|
format,
|
|
267
452
|
parse,
|
|
@@ -281,11 +466,18 @@ export const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
281
466
|
readonly,
|
|
282
467
|
clampValue,
|
|
283
468
|
range,
|
|
469
|
+
rawRange,
|
|
284
470
|
keyboard,
|
|
285
471
|
text,
|
|
286
472
|
editing,
|
|
287
473
|
outOfRange,
|
|
288
474
|
drag,
|
|
475
|
+
dragSensitivity,
|
|
476
|
+
pointerLock,
|
|
477
|
+
selectOnFocus,
|
|
478
|
+
unformatOnFocus,
|
|
479
|
+
keepCaretOnStep,
|
|
480
|
+
blurOnEnter,
|
|
289
481
|
format,
|
|
290
482
|
parse,
|
|
291
483
|
handleDraft,
|
|
@@ -298,21 +490,21 @@ export const Root = forwardRef<NumberInputMethods, Props>(
|
|
|
298
490
|
useImperativeHandle(forwardedRef, () => {
|
|
299
491
|
return {
|
|
300
492
|
focus() {
|
|
301
|
-
inputRef.current?.focus()
|
|
493
|
+
if (!disabled) inputRef.current?.focus()
|
|
302
494
|
},
|
|
303
495
|
blur() {
|
|
304
496
|
inputRef.current?.blur()
|
|
305
497
|
},
|
|
306
498
|
}
|
|
307
|
-
}, [])
|
|
499
|
+
}, [disabled])
|
|
308
500
|
|
|
309
501
|
return (
|
|
310
502
|
<NumberInputProvider value={context}>
|
|
311
503
|
<div
|
|
312
504
|
ref={wheelRefCallback}
|
|
313
|
-
className={
|
|
314
|
-
|
|
315
|
-
|
|
505
|
+
className={className}
|
|
506
|
+
data-disabled={disabled ? '' : undefined}
|
|
507
|
+
data-readonly={readonly ? '' : undefined}
|
|
316
508
|
style={style}
|
|
317
509
|
{...props}
|
|
318
510
|
>
|
|
@@ -334,8 +526,4 @@ export const NumberInput = {
|
|
|
334
526
|
DecrementStepper,
|
|
335
527
|
}
|
|
336
528
|
|
|
337
|
-
export { useNumberInputContext } from './context'
|
|
338
|
-
export { type NumberInputFieldProps } from './InputField'
|
|
339
|
-
export { type StepperProps } from './Stepper'
|
|
340
|
-
export { type IncrementStepperProps } from './IncrementStepper'
|
|
341
|
-
export { type DecrementStepperProps } from './DecrementStepper'
|
|
529
|
+
export { useNumberInputContext, type NumberInputContextValue } from './context'
|