@tremolo-ui/react 0.7.0 → 0.8.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 +346 -1015
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -221
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +76 -221
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +333 -978
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/DropZone/index.tsx +2 -1
- package/src/components/FileInput/index.tsx +8 -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 +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +19 -16
- package/src/components/NumberInput/InputField.tsx +12 -27
- package/src/components/NumberInput/Stepper.tsx +49 -97
- package/src/components/NumberInput/StepperButton.tsx +3 -4
- package/src/components/NumberInput/context.tsx +2 -1
- package/src/components/NumberInput/index.tsx +40 -71
- package/src/components/Piano/index.tsx +35 -142
- package/src/components/PointsEditor/Container.tsx +15 -26
- package/src/components/PointsEditor/Point.tsx +38 -85
- package/src/components/PointsEditor/context.tsx +5 -56
- package/src/components/PointsEditor/index.tsx +29 -311
- package/src/components/Slider/Marks.tsx +3 -5
- package/src/components/Slider/MarksOption.tsx +7 -18
- package/src/components/Slider/Track.tsx +2 -1
- package/src/components/Slider/index.tsx +29 -28
- package/src/components/XYPad/Area.tsx +27 -49
- package/src/components/XYPad/Thumb.tsx +3 -1
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +33 -46
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useCheckSteps.ts +9 -146
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/index.ts +2 -27
- 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/input-event.ts +0 -32
|
@@ -10,21 +10,23 @@ import {
|
|
|
10
10
|
} from 'react'
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
14
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
15
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
16
|
+
commitNumberInputText,
|
|
14
17
|
InputEventOption,
|
|
15
18
|
ModifierState,
|
|
16
19
|
type ModifierValue,
|
|
17
|
-
|
|
20
|
+
numberInputBounds,
|
|
21
|
+
numberInputRanges,
|
|
22
|
+
nudgeNumberInput,
|
|
23
|
+
parseNumberText,
|
|
24
|
+
wheelDirection,
|
|
18
25
|
} from '@tremolo-ui/dom'
|
|
19
|
-
import { linearScale, type Scale
|
|
26
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
20
27
|
|
|
21
28
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
22
29
|
import { useWheel } from '../../hooks/useWheel'
|
|
23
|
-
import {
|
|
24
|
-
DEFAULT_DRAG_SENSITIVITY,
|
|
25
|
-
DEFAULT_KEYBOARD_OPTIONS,
|
|
26
|
-
DEFAULT_WHEEL_OPTIONS,
|
|
27
|
-
} from '../../input-event'
|
|
28
30
|
|
|
29
31
|
import { NumberInputProvider } from './context'
|
|
30
32
|
import { DecrementStepper } from './DecrementStepper'
|
|
@@ -83,6 +85,9 @@ export interface NumberInputProps {
|
|
|
83
85
|
* Read a value back out of the text. Has to undo `format`.
|
|
84
86
|
*
|
|
85
87
|
* Text with no number in it reads as `NaN`, which leaves the value alone.
|
|
88
|
+
* The default, `parseNumberText` from `@tremolo-ui/dom`, reads the number
|
|
89
|
+
* with the unit around it left out, and is `NaN` for one it cannot read
|
|
90
|
+
* whole: `1,000`, `1:30`, or a number with a unit in front, such as `L 30`.
|
|
86
91
|
*/
|
|
87
92
|
parse?: (text: string) => number
|
|
88
93
|
|
|
@@ -160,7 +165,7 @@ export interface NumberInputProps {
|
|
|
160
165
|
|
|
161
166
|
/**
|
|
162
167
|
* Select the text when `InputField` takes focus: `'all'` selects all of it,
|
|
163
|
-
* `'number'` only the
|
|
168
|
+
* `'number'` only the number, leaving the unit the format put around it,
|
|
164
169
|
* and `'none'` leaves the caret where the click put it.
|
|
165
170
|
* @default 'none'
|
|
166
171
|
*/
|
|
@@ -244,20 +249,8 @@ export interface NumberInputMethods {
|
|
|
244
249
|
blur: () => void
|
|
245
250
|
}
|
|
246
251
|
|
|
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
252
|
const defaultFormat = (value: number) => String(value)
|
|
255
253
|
|
|
256
|
-
const defaultParse = (text: string) => {
|
|
257
|
-
const match = text.match(LEADING_NUMBER)
|
|
258
|
-
return match ? Number(match[1]) : NaN
|
|
259
|
-
}
|
|
260
|
-
|
|
261
254
|
type Props = NumberInputProps &
|
|
262
255
|
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
|
|
263
256
|
|
|
@@ -302,32 +295,15 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
302
295
|
|
|
303
296
|
// --- interpret props ---
|
|
304
297
|
const format = formatProp ?? defaultFormat
|
|
305
|
-
const parse = parseProp ??
|
|
298
|
+
const parse = parseProp ?? parseNumberText
|
|
306
299
|
|
|
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
|
-
}),
|
|
300
|
+
const { normalized: range, raw: rawRange } = useMemo(
|
|
301
|
+
() => numberInputRanges({ min, max, step, scale, clampValue }),
|
|
317
302
|
[clampValue, min, max, step, scale],
|
|
318
303
|
)
|
|
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],
|
|
304
|
+
const ranges = useMemo(
|
|
305
|
+
() => ({ normalized: range, raw: rawRange }),
|
|
306
|
+
[range, rawRange],
|
|
331
307
|
)
|
|
332
308
|
|
|
333
309
|
// The pipeline range stands in the safe-integer range when an end is
|
|
@@ -351,9 +327,9 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
351
327
|
|
|
352
328
|
const text = draft ?? format(value)
|
|
353
329
|
const editing = draft !== null
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
330
|
+
const bounds = numberInputBounds(value, { min, max, clampValue })
|
|
331
|
+
const { atMin, atMax } = bounds
|
|
332
|
+
const outOfRange = !editing && bounds.outOfRange
|
|
357
333
|
|
|
358
334
|
// --- internal functions ---
|
|
359
335
|
const handleDraft = useCallback(
|
|
@@ -379,18 +355,15 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
379
355
|
|
|
380
356
|
const commitDraft = useCallback(() => {
|
|
381
357
|
if (draft === null || inactive) return
|
|
382
|
-
const
|
|
358
|
+
const committed = commitNumberInputText(draft, parse, {
|
|
359
|
+
min,
|
|
360
|
+
max,
|
|
361
|
+
clampValue,
|
|
362
|
+
})
|
|
383
363
|
// 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
|
-
|
|
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)
|
|
364
|
+
// input back to what it was showing.
|
|
365
|
+
if (committed === null) setDraft(null)
|
|
366
|
+
else changeValue(committed)
|
|
394
367
|
}, [draft, inactive, parse, clampValue, min, max, changeValue])
|
|
395
368
|
|
|
396
369
|
const nudge = useCallback(
|
|
@@ -399,26 +372,20 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
399
372
|
option: ModifierValue<InputEventOption>,
|
|
400
373
|
modifiers?: ModifierState,
|
|
401
374
|
) => {
|
|
402
|
-
const [mode] = selectModifier(option, modifiers).value
|
|
403
375
|
changeValue(
|
|
404
|
-
|
|
405
|
-
value,
|
|
406
|
-
direction,
|
|
407
|
-
option,
|
|
408
|
-
mode === 'raw' ? rawRange : range,
|
|
409
|
-
modifiers,
|
|
410
|
-
),
|
|
376
|
+
nudgeNumberInput(value, direction, option, ranges, modifiers),
|
|
411
377
|
)
|
|
412
378
|
},
|
|
413
|
-
[changeValue, value,
|
|
379
|
+
[changeValue, value, ranges],
|
|
414
380
|
)
|
|
415
381
|
|
|
416
382
|
// --- hooks ---
|
|
417
383
|
const wheelRefCallback = useWheel<HTMLDivElement>(
|
|
418
384
|
(event) => {
|
|
419
|
-
|
|
385
|
+
const direction = wheelDirection(event)
|
|
386
|
+
if (!wheel || inactive || direction === null) return
|
|
420
387
|
event.preventDefault()
|
|
421
|
-
nudge(
|
|
388
|
+
nudge(direction, wheel, event)
|
|
422
389
|
},
|
|
423
390
|
{ requireFocus: true },
|
|
424
391
|
)
|
|
@@ -445,8 +412,8 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
445
412
|
unformatOnFocus,
|
|
446
413
|
keepCaretOnStep,
|
|
447
414
|
blurOnEnter,
|
|
448
|
-
atMin
|
|
449
|
-
atMax
|
|
415
|
+
atMin,
|
|
416
|
+
atMax,
|
|
450
417
|
drag,
|
|
451
418
|
format,
|
|
452
419
|
parse,
|
|
@@ -471,6 +438,8 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
471
438
|
text,
|
|
472
439
|
editing,
|
|
473
440
|
outOfRange,
|
|
441
|
+
atMin,
|
|
442
|
+
atMax,
|
|
474
443
|
drag,
|
|
475
444
|
dragSensitivity,
|
|
476
445
|
pointerLock,
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
5
|
ReactNode,
|
|
6
|
-
useCallback,
|
|
7
6
|
useEffect,
|
|
8
7
|
useImperativeHandle,
|
|
9
8
|
useMemo,
|
|
@@ -14,7 +13,10 @@ import {
|
|
|
14
13
|
import {
|
|
15
14
|
blackKeyWidth,
|
|
16
15
|
createPianoInput,
|
|
16
|
+
fitWhiteKeyWidth,
|
|
17
17
|
getNoteRangeArray,
|
|
18
|
+
type KeyboardShortcuts,
|
|
19
|
+
type KeyboardShortcutsScope,
|
|
18
20
|
notePosition,
|
|
19
21
|
pianoWidth,
|
|
20
22
|
type NoteRange,
|
|
@@ -23,33 +25,6 @@ import {
|
|
|
23
25
|
} from '@tremolo-ui/dom'
|
|
24
26
|
import { isWhiteKey, noteKey } from '@tremolo-ui/functions'
|
|
25
27
|
|
|
26
|
-
import { useEventListener } from '../../hooks/useEventListener'
|
|
27
|
-
import { cssLength } from '../_util/css-length'
|
|
28
|
-
|
|
29
|
-
import { KeyboardShortcuts } from './keyboard-shortcuts'
|
|
30
|
-
|
|
31
|
-
import type { CSSVariables } from '../../css-variables'
|
|
32
|
-
|
|
33
|
-
type KeyboardShortcutsScope = 'root' | 'window'
|
|
34
|
-
|
|
35
|
-
function isEditableTarget(target: EventTarget | null) {
|
|
36
|
-
if (!(target instanceof HTMLElement)) return false
|
|
37
|
-
if (target.matches('input, textarea, select')) return true
|
|
38
|
-
|
|
39
|
-
for (let element: HTMLElement | null = target; element;) {
|
|
40
|
-
const contentEditable = element.getAttribute('contenteditable')
|
|
41
|
-
if (contentEditable !== null)
|
|
42
|
-
return contentEditable.toLowerCase() !== 'false'
|
|
43
|
-
element = element.parentElement
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function shortcutKey(event: KeyboardEvent) {
|
|
50
|
-
return event.code || event.key
|
|
51
|
-
}
|
|
52
|
-
|
|
53
28
|
/**
|
|
54
29
|
* What {@link PianoProps.keyProps} may return for one key.
|
|
55
30
|
*
|
|
@@ -57,10 +32,8 @@ function shortcutKey(event: KeyboardEvent) {
|
|
|
57
32
|
* JSX syntax, not on an object type, and selecting on one is the usual way to
|
|
58
33
|
* mark a key out.
|
|
59
34
|
*/
|
|
60
|
-
export type KeyAttributes =
|
|
61
|
-
|
|
62
|
-
CSSVariables<'color' | 'bg' | 'active-color' | 'active-bg'>
|
|
63
|
-
} & Record<`data-${string}`, string | number | boolean | undefined>
|
|
35
|
+
export type KeyAttributes = ComponentPropsWithoutRef<'div'> &
|
|
36
|
+
Record<`data-${string}`, string | number | boolean | undefined>
|
|
64
37
|
|
|
65
38
|
/** What a key is, when {@link PianoProps.label} or `keyProps` is asked about it. */
|
|
66
39
|
export interface KeyState {
|
|
@@ -151,16 +124,9 @@ export interface PianoProps {
|
|
|
151
124
|
*/
|
|
152
125
|
blackKeyHeightRatio?: number
|
|
153
126
|
|
|
154
|
-
/**
|
|
155
|
-
* Height of the keyboard. Sets `--height`; when it is omitted, the height
|
|
156
|
-
* the theme gives stands, and the theme can tell `resizable` apart through
|
|
157
|
-
* `data-resizable`.
|
|
158
|
-
*/
|
|
159
|
-
height?: number | string
|
|
160
|
-
|
|
161
127
|
/**
|
|
162
128
|
* What to draw inside a key. `''`, `null` and `undefined` leave it bare, so
|
|
163
|
-
* a layout with gaps —
|
|
129
|
+
* a layout with gaps — `SHORTCUTS.HOME_ROW_NATURAL`, say — needs no
|
|
164
130
|
* special casing.
|
|
165
131
|
*/
|
|
166
132
|
label?: (note: number, state: KeyState) => ReactNode
|
|
@@ -189,8 +155,6 @@ export interface PianoProps {
|
|
|
189
155
|
onPlayNote?: (note: number, velocity?: number) => void
|
|
190
156
|
/** Called once everything holding a note has let go of it. */
|
|
191
157
|
onStopNote?: (note: number) => void
|
|
192
|
-
|
|
193
|
-
style?: CSSProperties & CSSVariables<'height'>
|
|
194
158
|
}
|
|
195
159
|
|
|
196
160
|
export interface PianoMethods {
|
|
@@ -214,7 +178,6 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
214
178
|
keyGap = 1,
|
|
215
179
|
blackKeyWidthRatio = 0.65,
|
|
216
180
|
blackKeyHeightRatio = 0.6,
|
|
217
|
-
height,
|
|
218
181
|
style,
|
|
219
182
|
className,
|
|
220
183
|
label,
|
|
@@ -234,10 +197,6 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
234
197
|
const [resizedKeyWidth, setResizedKeyWidth] = useState(whiteKeyWidth)
|
|
235
198
|
|
|
236
199
|
const notes = useMemo(() => getNoteRangeArray(noteRange), [noteRange])
|
|
237
|
-
const whiteKeyCount = useMemo(
|
|
238
|
-
() => notes.filter(isWhiteKey).length,
|
|
239
|
-
[notes],
|
|
240
|
-
)
|
|
241
200
|
|
|
242
201
|
const layout: PianoLayout = useMemo(
|
|
243
202
|
() => ({
|
|
@@ -264,21 +223,12 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
264
223
|
layout,
|
|
265
224
|
glissando,
|
|
266
225
|
midiMax,
|
|
226
|
+
keyboardShortcuts,
|
|
227
|
+
keyboardShortcutsScope,
|
|
267
228
|
onPlayNote,
|
|
268
229
|
onStopNote,
|
|
269
230
|
})
|
|
270
231
|
const instanceRef = useRef<PianoInputInstance | null>(null)
|
|
271
|
-
const shortcutNotes = useRef(
|
|
272
|
-
new Map<string, { note: number; source: string }>(),
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
const releaseShortcutNotes = useCallback(() => {
|
|
276
|
-
for (const { note, source } of shortcutNotes.current.values()) {
|
|
277
|
-
instanceRef.current?.noteOff(note, { source })
|
|
278
|
-
}
|
|
279
|
-
shortcutNotes.current.clear()
|
|
280
|
-
}, [])
|
|
281
|
-
|
|
282
232
|
useEffect(() => {
|
|
283
233
|
if (!node) return
|
|
284
234
|
|
|
@@ -286,6 +236,8 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
286
236
|
layout: latest.current.layout,
|
|
287
237
|
glissando: latest.current.glissando,
|
|
288
238
|
midiMax: latest.current.midiMax,
|
|
239
|
+
keyboardShortcuts: latest.current.keyboardShortcuts,
|
|
240
|
+
keyboardShortcutsScope: latest.current.keyboardShortcutsScope,
|
|
289
241
|
onPlayNote: (note, velocity) =>
|
|
290
242
|
latest.current.onPlayNote?.(note, velocity),
|
|
291
243
|
onStopNote: (note) => latest.current.onStopNote?.(note),
|
|
@@ -304,10 +256,27 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
304
256
|
|
|
305
257
|
// Runs after every render.
|
|
306
258
|
useEffect(() => {
|
|
307
|
-
latest.current = {
|
|
308
|
-
|
|
259
|
+
latest.current = {
|
|
260
|
+
layout,
|
|
261
|
+
glissando,
|
|
262
|
+
midiMax,
|
|
263
|
+
keyboardShortcuts,
|
|
264
|
+
keyboardShortcutsScope,
|
|
265
|
+
onPlayNote,
|
|
266
|
+
onStopNote,
|
|
267
|
+
}
|
|
268
|
+
instanceRef.current?.update({
|
|
269
|
+
layout,
|
|
270
|
+
glissando,
|
|
271
|
+
midiMax,
|
|
272
|
+
keyboardShortcuts,
|
|
273
|
+
keyboardShortcutsScope,
|
|
274
|
+
})
|
|
309
275
|
})
|
|
310
276
|
|
|
277
|
+
// By value, so that a range written inline does not reconnect the
|
|
278
|
+
// observer on every render.
|
|
279
|
+
const { first, last } = noteRange
|
|
311
280
|
useEffect(() => {
|
|
312
281
|
if (!resizable || !node) return
|
|
313
282
|
const parent = node.parentElement
|
|
@@ -315,89 +284,16 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
315
284
|
|
|
316
285
|
const resizeObserver = new ResizeObserver(() => {
|
|
317
286
|
setResizedKeyWidth(
|
|
318
|
-
node.clientWidth
|
|
287
|
+
fitWhiteKeyWidth(node.clientWidth, {
|
|
288
|
+
noteRange: { first, last },
|
|
289
|
+
keyGap,
|
|
290
|
+
blackKeyWidthRatio,
|
|
291
|
+
}),
|
|
319
292
|
)
|
|
320
293
|
})
|
|
321
294
|
resizeObserver.observe(parent)
|
|
322
295
|
return () => resizeObserver.disconnect()
|
|
323
|
-
}, [resizable, node,
|
|
324
|
-
|
|
325
|
-
const shortcutKeys = keyboardShortcuts?.keys
|
|
326
|
-
const hasShortcuts = shortcutKeys !== undefined
|
|
327
|
-
// What the held keys were started against, as a value rather than the
|
|
328
|
-
// identity of the array: a caller who writes the keys inline hands over a
|
|
329
|
-
// new array on every render, and releasing on that would stop a note as
|
|
330
|
-
// soon as anything else re-renders — including the state change that
|
|
331
|
-
// playing the note caused.
|
|
332
|
-
const shortcutMapping = shortcutKeys?.join('\u0000')
|
|
333
|
-
|
|
334
|
-
useEffect(
|
|
335
|
-
() => releaseShortcutNotes,
|
|
336
|
-
[
|
|
337
|
-
releaseShortcutNotes,
|
|
338
|
-
shortcutMapping,
|
|
339
|
-
keyboardShortcutsScope,
|
|
340
|
-
noteRange.first,
|
|
341
|
-
noteRange.last,
|
|
342
|
-
],
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
/** The note a shortcut key plays, or null when it has none. */
|
|
346
|
-
function shortcutNote(key: string) {
|
|
347
|
-
if (!shortcutKeys || key === '') return null
|
|
348
|
-
const index = shortcutKeys.indexOf(key)
|
|
349
|
-
const note = noteRange.first + index
|
|
350
|
-
return index === -1 || note > noteRange.last ? null : note
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const shortcutTarget = useMemo(
|
|
354
|
-
() => () => {
|
|
355
|
-
if (!hasShortcuts) return null
|
|
356
|
-
return keyboardShortcutsScope === 'window' ? globalThis.window : node
|
|
357
|
-
},
|
|
358
|
-
[hasShortcuts, keyboardShortcutsScope, node],
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
const focusOutTarget = useMemo(
|
|
362
|
-
() => () =>
|
|
363
|
-
hasShortcuts && keyboardShortcutsScope === 'root' ? node : null,
|
|
364
|
-
[hasShortcuts, keyboardShortcutsScope, node],
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
const windowBlurTarget = useMemo(
|
|
368
|
-
() => () => (hasShortcuts ? globalThis.window : null),
|
|
369
|
-
[hasShortcuts],
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
useEventListener(shortcutTarget, 'keydown', (e) => {
|
|
373
|
-
const key = shortcutKey(e)
|
|
374
|
-
if (e.repeat || shortcutNotes.current.has(key)) return
|
|
375
|
-
if (isEditableTarget(e.target)) return
|
|
376
|
-
|
|
377
|
-
const note = shortcutNote(e.key)
|
|
378
|
-
if (note === null) return
|
|
379
|
-
|
|
380
|
-
const source = `keyboard:${key}`
|
|
381
|
-
shortcutNotes.current.set(key, { note, source })
|
|
382
|
-
instanceRef.current?.noteOn(note, { source })
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
useEventListener(shortcutTarget, 'keyup', (e) => {
|
|
386
|
-
const key = shortcutKey(e)
|
|
387
|
-
const shortcut = shortcutNotes.current.get(key)
|
|
388
|
-
if (!shortcut) return
|
|
389
|
-
|
|
390
|
-
shortcutNotes.current.delete(key)
|
|
391
|
-
instanceRef.current?.noteOff(shortcut.note, { source: shortcut.source })
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
useEventListener(focusOutTarget, 'focusout', (e) => {
|
|
395
|
-
if (e.relatedTarget instanceof Node && node?.contains(e.relatedTarget))
|
|
396
|
-
return
|
|
397
|
-
releaseShortcutNotes()
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
useEventListener(windowBlurTarget, 'blur', releaseShortcutNotes)
|
|
296
|
+
}, [resizable, node, first, last, keyGap, blackKeyWidthRatio])
|
|
401
297
|
|
|
402
298
|
useImperativeHandle(
|
|
403
299
|
forwardedRef,
|
|
@@ -423,7 +319,6 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
423
319
|
{
|
|
424
320
|
// Computed from the layout rather than chosen, so it stays inline.
|
|
425
321
|
width: resizable ? '100%' : pianoWidth(layout),
|
|
426
|
-
'--height': cssLength(height),
|
|
427
322
|
// The keys inside are placed against this box.
|
|
428
323
|
position: 'relative',
|
|
429
324
|
...style,
|
|
@@ -492,5 +387,3 @@ export const Root = /* @__PURE__ */ forwardRef<PianoMethods, Props>(
|
|
|
492
387
|
* Customizable piano component.
|
|
493
388
|
*/
|
|
494
389
|
export const Piano = { Root }
|
|
495
|
-
|
|
496
|
-
export { type KeyboardShortcuts, SHORTCUTS } from './keyboard-shortcuts'
|
|
@@ -1,40 +1,33 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
|
2
2
|
|
|
3
|
+
import { POINT_AXIS, wheelMove } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
3
5
|
import { useComposedRefs } from '../../compose-refs'
|
|
4
6
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
5
7
|
import { useWheel } from '../../hooks/useWheel'
|
|
6
8
|
import { Placement } from '../_util/Placement'
|
|
7
9
|
|
|
8
10
|
import { usePointsEditorContext } from './context'
|
|
9
|
-
import { AXIS } from './Point'
|
|
10
11
|
|
|
11
12
|
type Props = ComponentPropsWithoutRef<'div'>
|
|
12
13
|
|
|
13
14
|
export const Container = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
14
15
|
function Container({ children, className, style, ...props }, forwardedRef) {
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
disabled,
|
|
18
|
-
selectable,
|
|
19
|
-
isPointElement,
|
|
20
|
-
nudgeFocusedPoint,
|
|
21
|
-
beginSelectionBox,
|
|
22
|
-
moveSelectionBox,
|
|
23
|
-
endSelectionBox,
|
|
24
|
-
} = usePointsEditorContext()
|
|
16
|
+
const { containerRef, disabled, selectable, editor } =
|
|
17
|
+
usePointsEditorContext()
|
|
25
18
|
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
26
|
-
axis:
|
|
19
|
+
axis: POINT_AXIS,
|
|
27
20
|
baseElementRef: containerRef,
|
|
28
21
|
// A press that landed on a point belongs to that point. Declining here
|
|
29
22
|
// rather than in onDragStart matters: by then the container would already
|
|
30
23
|
// have taken the pointer capture away from the point.
|
|
31
24
|
shouldStart: (event) =>
|
|
32
|
-
!disabled && !isPointElement(event.target as Element | null),
|
|
25
|
+
!disabled && !editor.isPointElement(event.target as Element | null),
|
|
33
26
|
onDragStart: ([x, y], state) => {
|
|
34
|
-
beginSelectionBox({ x, y }, state.event)
|
|
27
|
+
editor.beginSelectionBox({ x, y }, state.event)
|
|
35
28
|
},
|
|
36
|
-
onChange: ([x, y]) => moveSelectionBox({ x, y }),
|
|
37
|
-
onDragEnd: endSelectionBox,
|
|
29
|
+
onChange: ([x, y]) => editor.moveSelectionBox({ x, y }),
|
|
30
|
+
onDragEnd: () => editor.endSelectionBox(),
|
|
38
31
|
})
|
|
39
32
|
|
|
40
33
|
// One listener for the whole editor rather than one per point: a wheel
|
|
@@ -44,16 +37,12 @@ export const Container = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
44
37
|
useWheel(
|
|
45
38
|
(event) => {
|
|
46
39
|
// Scrolling up moves the point towards y = 0; shift switches to x.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (delta === 0) return
|
|
54
|
-
const axis = horizontal || event.shiftKey ? 'x' : 'y'
|
|
55
|
-
const direction = delta < 0 ? -1 : 1
|
|
56
|
-
if (nudgeFocusedPoint(axis, direction, event)) event.preventDefault()
|
|
40
|
+
const move = wheelMove(event)
|
|
41
|
+
if (!move) return
|
|
42
|
+
const axis = move.axis === 0 ? 'x' : 'y'
|
|
43
|
+
if (editor.nudgeFocusedPoint(axis, move.direction, event)) {
|
|
44
|
+
event.preventDefault()
|
|
45
|
+
}
|
|
57
46
|
},
|
|
58
47
|
{ target: containerRef },
|
|
59
48
|
)
|