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