@tremolo-ui/react 0.2.1 → 0.4.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.
@@ -1,19 +1,39 @@
1
- import { useRef, useCallback, RefObject, useState } from 'react'
1
+ import { RefObject, useCallback, useEffect, useRef, useState } from 'react'
2
2
 
3
+ import { createDrag } from '@tremolo-ui/dom'
3
4
  import { normalizeValue } from '@tremolo-ui/functions'
4
5
 
5
- import { useEventListener } from './useEventListener'
6
- import { useRefCallbackEvent } from './useRefCallbackEvent'
6
+ import { useCallbackRef } from './useCallbackRef'
7
7
 
8
8
  interface UseDragWithElement<T extends Element> {
9
9
  baseElementRef: RefObject<T | null>
10
+ /**
11
+ * Report the position on pointer down, before any movement.
12
+ *
13
+ * Enable it where the pointer position *is* the value, so that a plain click
14
+ * jumps to it. Leave it off where the element being dragged is an object in
15
+ * its own right, so that grabbing its edge does not shift it under the cursor.
16
+ *
17
+ * @default false
18
+ */
19
+ updateOnPointerDown?: boolean
20
+ /** CSS cursor to show while dragging. Applied to the element itself. */
21
+ cursor?: string
10
22
  onDrag: (normalizedX: number, normalizedY: number) => void
11
23
  onDragStart?: (normalizedX: number, normalizedY: number) => void
12
24
  onDragEnd?: (normalizedX: number, normalizedY: number) => void
13
25
  }
14
26
 
27
+ /**
28
+ * Track a pointer drag, reporting the position normalized against
29
+ * the bounding rect of `baseElementRef`.
30
+ *
31
+ * @returns a ref callback for the element that starts the drag, and whether a drag is in progress
32
+ */
15
33
  export function useDragWithElement<T extends Element>({
16
34
  baseElementRef,
35
+ updateOnPointerDown = false,
36
+ cursor,
17
37
  onDrag,
18
38
  onDragStart,
19
39
  onDragEnd,
@@ -22,51 +42,59 @@ export function useDragWithElement<T extends Element>({
22
42
  const normalizedX = useRef(0)
23
43
  const normalizedY = useRef(0)
24
44
 
25
- const handleDrag = useCallback(
26
- (event: MouseEvent | React.PointerEvent<T> | TouchEvent) => {
27
- if (!baseElementRef.current || !dragging) {
28
- return
29
- }
30
- const isTouch = event instanceof TouchEvent
31
- if (isTouch && event.cancelable) event.preventDefault()
32
- const { left, top, right, bottom } =
33
- baseElementRef.current.getBoundingClientRect()
34
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX
35
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY
36
- const nx = normalizeValue(mouseX, left, right)
37
- const ny = normalizeValue(mouseY, top, bottom)
38
- normalizedX.current = nx
39
- normalizedY.current = ny
40
- onDrag(nx, ny)
45
+ const dragHandler = useCallbackRef(onDrag)
46
+ const dragStartHandler = useCallbackRef(onDragStart)
47
+ const dragEndHandler = useCallbackRef(onDragEnd)
48
+
49
+ const update = useCallback(
50
+ (clientX: number, clientY: number) => {
51
+ const base = baseElementRef.current
52
+ if (!base) return false
53
+ const { left, top, right, bottom } = base.getBoundingClientRect()
54
+ normalizedX.current = normalizeValue(clientX, left, right)
55
+ normalizedY.current = normalizeValue(clientY, top, bottom)
56
+ return true
41
57
  },
42
- [onDrag, baseElementRef, dragging],
58
+ [baseElementRef],
43
59
  )
44
60
 
45
- const refHandler = useRefCallbackEvent(
46
- 'touchmove',
47
- handleDrag,
48
- { passive: false },
49
- [onDrag],
50
- )
61
+ // See useDrag for why the node is held in state.
62
+ const [node, setNode] = useState<Element | null>(null)
51
63
 
52
- const pointerDownHandler = useCallback(
53
- (event: React.PointerEvent<T>) => {
54
- // dragging.current = true
55
- setDragging(true)
56
- handleDrag(event)
57
- onDragStart?.(normalizedX.current, normalizedY.current)
58
- },
59
- [handleDrag, onDragStart],
60
- )
64
+ useEffect(() => {
65
+ if (!node) return
61
66
 
62
- useEventListener(globalThis.window, 'pointermove', handleDrag)
67
+ const instance = createDrag(node, {
68
+ cursor,
69
+ onDragStart: ({ clientX, clientY }) => {
70
+ setDragging(true)
71
+ const updated = update(clientX, clientY)
72
+ if (updated && updateOnPointerDown) {
73
+ dragHandler(normalizedX.current, normalizedY.current)
74
+ }
75
+ dragStartHandler(normalizedX.current, normalizedY.current)
76
+ },
77
+ onDrag: ({ clientX, clientY }) => {
78
+ if (update(clientX, clientY)) {
79
+ dragHandler(normalizedX.current, normalizedY.current)
80
+ }
81
+ },
82
+ onDragEnd: () => {
83
+ setDragging(false)
84
+ dragEndHandler(normalizedX.current, normalizedY.current)
85
+ },
86
+ })
63
87
 
64
- useEventListener(globalThis.window, 'pointerup', () => {
65
- if (!dragging) return
66
- // dragging.current = false
67
- setDragging(false)
68
- onDragEnd?.(normalizedX.current, normalizedY.current)
69
- })
88
+ return () => instance.destroy()
89
+ }, [
90
+ node,
91
+ update,
92
+ updateOnPointerDown,
93
+ cursor,
94
+ dragHandler,
95
+ dragStartHandler,
96
+ dragEndHandler,
97
+ ])
70
98
 
71
- return { refHandler, pointerDownHandler, dragging }
99
+ return { refCallback: setNode, dragging }
72
100
  }
@@ -1,39 +1,32 @@
1
- import { useCallback, useEffect, useState } from 'react'
1
+ import { useEffect, useMemo, useSyncExternalStore } from 'react'
2
2
 
3
- /** @private */
4
- export const PERMISSION_DENIED = 'PERMISSION_DENIED'
5
- /** @private */
6
- export const NOT_SUPPORTED = 'NOT_SUPPORTED'
3
+ import { createMIDIAccess } from '@tremolo-ui/dom'
7
4
 
8
- /** @private */
9
- export type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED
5
+ export {
6
+ NOT_SUPPORTED,
7
+ PERMISSION_DENIED,
8
+ type MIDIAccessError,
9
+ } from '@tremolo-ui/dom'
10
10
 
11
11
  /**
12
12
  * Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
13
13
  */
14
14
  export function useMIDIAccess(requestOnMount = true) {
15
- const [midiAccess, setMidiAccess] = useState<MIDIAccess | null>(null)
16
- const [error, setError] = useState<MIDIAccessError | null>(null)
15
+ const instance = useMemo(() => createMIDIAccess(), [])
17
16
 
18
- const request = useCallback(() => {
19
- if (navigator.requestMIDIAccess) {
20
- navigator
21
- .requestMIDIAccess()
22
- .then((access) => {
23
- setMidiAccess(access)
24
- setError(null)
25
- })
26
- .catch(() => {
27
- setError(PERMISSION_DENIED)
28
- })
29
- } else {
30
- setError(NOT_SUPPORTED)
31
- }
32
- }, [])
17
+ const { midiAccess, error } = useSyncExternalStore(
18
+ instance.subscribe,
19
+ instance.getState,
20
+ instance.getServerState,
21
+ )
33
22
 
34
23
  useEffect(() => {
35
- if (requestOnMount) request()
36
- }, [requestOnMount, request])
24
+ if (requestOnMount) instance.request()
25
+ }, [instance, requestOnMount])
37
26
 
38
- return { request, midiAccess, error }
27
+ useEffect(() => {
28
+ return () => instance.destroy()
29
+ }, [instance])
30
+
31
+ return { request: instance.request, midiAccess, error }
39
32
  }
@@ -1,18 +1,6 @@
1
- import { useCallback } from 'react'
1
+ import { useEffect } from 'react'
2
2
 
3
- import { useMIDIMessage } from './useMIDIMessage'
4
-
5
- const MIDI_EVENT_TO_NUMBER = {
6
- NOTE_ON: 0x90,
7
- NOTE_OFF: 0x80,
8
- PITCH_BEND: 0xe0,
9
- }
10
-
11
- // const NUMBER_TO_MIDI_EVENT: Record<number, string> = {
12
- // 0x90: 'NOTE_ON',
13
- // 0x80: 'NOTE_OFF',
14
- // 0xe0: 'PITCH_BEND',
15
- // }
3
+ import { createMIDIInput } from '@tremolo-ui/dom'
16
4
 
17
5
  /**
18
6
  * Hooks for handling note on/off events. To be used with useMIDIAccess. Internally uses useMIDIMessage.
@@ -23,27 +11,12 @@ export function useMIDIInput(
23
11
  onNoteOffEvent?: (note: number) => void,
24
12
  onPitchBendEvent?: (msb: number, lsb: number) => void,
25
13
  ) {
26
- const handleMIDIMessage = useCallback(
27
- (event: MIDIMessageEvent) => {
28
- if (!event.data) return
29
- // event.data[0] ... command
30
- // event.data[1] ... note, MSB (Most Significant Byte)
31
- // event.data[2] ... velocity, LSB (Least Significant Byte)
32
- const kind = event.data[0] & 0xf0
33
-
34
- if (
35
- kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||
36
- (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)
37
- ) {
38
- onNoteOffEvent?.(event.data[1])
39
- } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {
40
- onNoteOnEvent?.(event.data[1], event.data[2])
41
- } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {
42
- onPitchBendEvent?.(event.data[1], event.data[2])
43
- }
44
- },
45
- [onNoteOffEvent, onNoteOnEvent, onPitchBendEvent],
46
- )
47
-
48
- useMIDIMessage(midiAccess, handleMIDIMessage)
14
+ useEffect(() => {
15
+ const instance = createMIDIInput(midiAccess, {
16
+ onNoteOnEvent,
17
+ onNoteOffEvent,
18
+ onPitchBendEvent,
19
+ })
20
+ return () => instance.destroy()
21
+ }, [midiAccess, onNoteOnEvent, onNoteOffEvent, onPitchBendEvent])
49
22
  }
@@ -1,5 +1,7 @@
1
1
  import { useEffect } from 'react'
2
2
 
3
+ import { createMIDIMessage } from '@tremolo-ui/dom'
4
+
3
5
  /**
4
6
  * Hooks for when you want to process MIDI events in more detail than useMIDIInput.
5
7
  */
@@ -8,16 +10,7 @@ export function useMIDIMessage(
8
10
  onMIDIMessage: (event: MIDIMessageEvent) => void,
9
11
  ) {
10
12
  useEffect(() => {
11
- if (!midiAccess) return
12
-
13
- for (const input of midiAccess.inputs.values()) {
14
- input.addEventListener('midimessage', onMIDIMessage)
15
- }
16
-
17
- return () => {
18
- for (const input of midiAccess.inputs.values()) {
19
- input.removeEventListener('midimessage', onMIDIMessage)
20
- }
21
- }
13
+ const instance = createMIDIMessage(midiAccess, onMIDIMessage)
14
+ return () => instance.destroy()
22
15
  }, [midiAccess, onMIDIMessage])
23
16
  }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ import { createWheel } from '@tremolo-ui/dom'
4
+
5
+ import { useCallbackRef } from './useCallbackRef'
6
+
7
+ /**
8
+ * Listen to wheel events on an element.
9
+ *
10
+ * The listener is not passive, so the handler may call `preventDefault()`.
11
+ *
12
+ * @returns a ref callback to attach to the element
13
+ */
14
+ export function useWheel<T extends Element>(
15
+ onWheel: (event: WheelEvent) => void,
16
+ ): (node: T | null) => void {
17
+ const wheelHandler = useCallbackRef(onWheel)
18
+
19
+ // See useDrag for why the node is held in state.
20
+ const [node, setNode] = useState<T | null>(null)
21
+
22
+ useEffect(() => {
23
+ if (!node) return
24
+
25
+ const instance = createWheel(node, (event) => wheelHandler(event))
26
+
27
+ return () => instance.destroy()
28
+ }, [node, wheelHandler])
29
+
30
+ return setNode
31
+ }
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@ export {
16
16
  type InitFunction,
17
17
  type RelativeSizingProps,
18
18
  } from './components/AnimationCanvas'
19
- export { DragObserver, type DragObserverProps } from './components/DragObserver'
20
19
  export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
21
20
  export {
22
21
  NumberInput,
@@ -55,10 +54,6 @@ export {
55
54
  type SliderThumbProps,
56
55
  type SliderTrackProps,
57
56
  } from './components/Slider'
58
- export {
59
- WheelObserver,
60
- type WheelObserverProps,
61
- } from './components/WheelObserver'
62
57
  export {
63
58
  XYPad,
64
59
  type XYPadProps,
@@ -84,3 +79,4 @@ export {
84
79
  } from './hooks/useMIDIAccess'
85
80
  export { useMIDIInput } from './hooks/useMIDIInput'
86
81
  export { useMIDIMessage } from './hooks/useMIDIMessage'
82
+ export { useWheel } from './hooks/useWheel'
@@ -1,58 +0,0 @@
1
- import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
2
-
3
- import { useDrag } from '../../hooks/useDrag'
4
- import { Override } from '../_util/type'
5
-
6
- export interface DragObserverProps<T extends ElementType> {
7
- /**
8
- * React.ElementType
9
- * @default div
10
- */
11
- as?: T
12
-
13
- /**
14
- * Threshold at which the onDrag event fires.
15
- * Prevents onDrag events from firing, for example, when double-clicking.
16
- */
17
- threshold?: number
18
-
19
- children?: ReactNode
20
- onDrag?: (x: number, y: number, deltaX: number, deltaY: number) => void
21
- onDragStart?: () => void
22
- onDragEnd?: () => void
23
- }
24
-
25
- export function DragObserver<T extends ElementType = 'div'>(
26
- props: Override<DragObserverProps<T>, ComponentPropsWithoutRef<T>>,
27
- ) {
28
- const {
29
- as: Component = 'div',
30
- threshold = 1,
31
- onDrag,
32
- onDragStart,
33
- onDragEnd,
34
- children,
35
- onPointerDown,
36
- ...attributes
37
- } = props
38
-
39
- const [refCallback, pointerDownHandler] = useDrag({
40
- threshold: threshold,
41
- onDrag: onDrag,
42
- onDragStart: onDragStart,
43
- onDragEnd: onDragEnd,
44
- })
45
-
46
- return (
47
- <Component
48
- ref={refCallback}
49
- onPointerDown={(e) => {
50
- pointerDownHandler(e)
51
- onPointerDown?.(e)
52
- }}
53
- {...attributes}
54
- >
55
- {children}
56
- </Component>
57
- )
58
- }
@@ -1,48 +0,0 @@
1
- import { ComponentProps, ElementType, ReactNode } from 'react'
2
-
3
- import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
4
- import { composeRefs } from '../_util/composeRefs'
5
- import { Override } from '../_util/type'
6
-
7
- export interface WheelObserverProps<T extends ElementType> {
8
- /**
9
- * React.ElementType
10
- * @default div
11
- */
12
- as?: T
13
-
14
- children?: ReactNode
15
- onWheel?: (event: WheelEvent) => void
16
- }
17
-
18
- /**
19
- * @example
20
- * <WheelObserver
21
- * onWheel={(event) => {
22
- * event.preventDefault()
23
- * console.log(event.deltaY)
24
- * }
25
- * >
26
- * <div>Wheel here</div>
27
- * </WheelObserver>
28
- */
29
- export function WheelObserver<T extends ElementType = 'div'>(
30
- props: Override<WheelObserverProps<T>, ComponentProps<T>>,
31
- ) {
32
- const { as: Component = 'div', children, onWheel, ref, ...attributes } = props
33
-
34
- const wheelRefCallback = useRefCallbackEvent(
35
- 'wheel',
36
- (event) => {
37
- if (onWheel) onWheel(event)
38
- },
39
- { passive: false },
40
- [onWheel],
41
- )
42
-
43
- return (
44
- <Component ref={composeRefs(ref, wheelRefCallback)} {...attributes}>
45
- {children}
46
- </Component>
47
- )
48
- }