@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.
Files changed (47) hide show
  1. package/dist/index.cjs +346 -1015
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +76 -221
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +76 -221
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +333 -978
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -3
  10. package/src/components/DropZone/index.tsx +2 -1
  11. package/src/components/FileInput/index.tsx +8 -13
  12. package/src/components/Knob/ActiveLine.tsx +5 -3
  13. package/src/components/Knob/InactiveLine.tsx +6 -4
  14. package/src/components/Knob/SVGRoot.tsx +3 -3
  15. package/src/components/Knob/Thumb.tsx +3 -2
  16. package/src/components/Knob/context.tsx +3 -74
  17. package/src/components/Knob/index.tsx +19 -16
  18. package/src/components/NumberInput/InputField.tsx +12 -27
  19. package/src/components/NumberInput/Stepper.tsx +49 -97
  20. package/src/components/NumberInput/StepperButton.tsx +3 -4
  21. package/src/components/NumberInput/context.tsx +2 -1
  22. package/src/components/NumberInput/index.tsx +40 -71
  23. package/src/components/Piano/index.tsx +35 -142
  24. package/src/components/PointsEditor/Container.tsx +15 -26
  25. package/src/components/PointsEditor/Point.tsx +38 -85
  26. package/src/components/PointsEditor/context.tsx +5 -56
  27. package/src/components/PointsEditor/index.tsx +29 -311
  28. package/src/components/Slider/Marks.tsx +3 -5
  29. package/src/components/Slider/MarksOption.tsx +7 -18
  30. package/src/components/Slider/Track.tsx +2 -1
  31. package/src/components/Slider/index.tsx +29 -28
  32. package/src/components/XYPad/Area.tsx +27 -49
  33. package/src/components/XYPad/Thumb.tsx +3 -1
  34. package/src/components/XYPad/context.tsx +1 -3
  35. package/src/components/XYPad/index.tsx +33 -46
  36. package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
  37. package/src/hooks/_internal/useCheckSteps.ts +9 -146
  38. package/src/hooks/useLongPress.ts +35 -51
  39. package/src/hooks/useMIDIAccess.ts +0 -8
  40. package/src/hooks/useMIDIInput.ts +0 -3
  41. package/src/index.ts +2 -27
  42. package/src/components/Piano/keyboard-shortcuts.ts +0 -61
  43. package/src/components/Slider/type.ts +0 -46
  44. package/src/components/_util/css-length.ts +0 -13
  45. package/src/components/_util/decimal-digits.ts +0 -23
  46. package/src/components/_util/visually-hidden.ts +0 -25
  47. package/src/input-event.ts +0 -32
@@ -10,21 +10,23 @@ import {
10
10
  } from 'react'
11
11
 
12
12
  import {
13
- applyDelta,
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
- selectModifier,
20
+ numberInputBounds,
21
+ numberInputRanges,
22
+ nudgeNumberInput,
23
+ parseNumberText,
24
+ wheelDirection,
18
25
  } from '@tremolo-ui/dom'
19
- import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
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 leading number, leaving whatever the format appended,
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 ?? defaultParse
298
+ const parse = parseProp ?? parseNumberText
306
299
 
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.
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
- // 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],
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 outOfRange =
355
- !editing &&
356
- ((min !== undefined && value < min) || (max !== undefined && value > max))
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 parsed = parse(draft)
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, 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)
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
- applyDelta(
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, rawRange, range],
379
+ [changeValue, value, ranges],
414
380
  )
415
381
 
416
382
  // --- hooks ---
417
383
  const wheelRefCallback = useWheel<HTMLDivElement>(
418
384
  (event) => {
419
- if (!wheel || inactive || event.deltaY === 0) return
385
+ const direction = wheelDirection(event)
386
+ if (!wheel || inactive || direction === null) return
420
387
  event.preventDefault()
421
- nudge(-Math.sign(event.deltaY), wheel, event)
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: clampValue && min !== undefined && value <= min,
449
- atMax: clampValue && max !== undefined && value >= max,
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 = Omit<ComponentPropsWithoutRef<'div'>, 'style'> & {
61
- style?: CSSProperties &
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 — {@link SHORTCUTS.HOME_ROW_NATURAL}, say — needs no
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 = { layout, glissando, midiMax, onPlayNote, onStopNote }
308
- instanceRef.current?.update({ layout, glissando, midiMax })
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 / Math.max(whiteKeyCount, 1) - keyGap,
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, whiteKeyCount, keyGap])
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
- containerRef,
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: 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
- // Browsers turn shift+wheel into horizontal scrolling: `deltaY` comes
48
- // out empty and `deltaX` carries the movement. Reading whichever axis
49
- // moved keeps shift working as the x-axis modifier — and picks up a
50
- // trackpad's own horizontal gesture, which never had a modifier.
51
- const horizontal = event.deltaX !== 0
52
- const delta = horizontal ? event.deltaX : event.deltaY
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
  )