@tremolo-ui/react 0.4.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.
Files changed (90) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +2272 -1650
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1461 -475
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1461 -475
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2258 -1648
  16. package/dist/index.js.map +1 -1
  17. package/package.json +32 -31
  18. package/src/components/AnimationCanvas/index.tsx +162 -194
  19. package/src/components/Knob/ActiveLine.tsx +9 -8
  20. package/src/components/Knob/InactiveLine.tsx +11 -14
  21. package/src/components/Knob/SVGRoot.tsx +10 -17
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +73 -89
  24. package/src/components/Knob/index.tsx +227 -105
  25. package/src/components/NumberInput/DecrementStepper.tsx +21 -50
  26. package/src/components/NumberInput/IncrementStepper.tsx +22 -50
  27. package/src/components/NumberInput/InputField.tsx +206 -0
  28. package/src/components/NumberInput/Stepper.tsx +150 -21
  29. package/src/components/NumberInput/StepperButton.tsx +95 -0
  30. package/src/components/NumberInput/context.tsx +104 -149
  31. package/src/components/NumberInput/index.tsx +461 -110
  32. package/src/components/Piano/index.tsx +413 -263
  33. package/src/components/Piano/keyboard-shortcuts.ts +61 -0
  34. package/src/components/PointsEditor/Background.tsx +19 -4
  35. package/src/components/PointsEditor/Container.tsx +92 -22
  36. package/src/components/PointsEditor/Point.tsx +293 -53
  37. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  38. package/src/components/PointsEditor/context.tsx +97 -75
  39. package/src/components/PointsEditor/index.tsx +497 -62
  40. package/src/components/Slider/Marks.tsx +84 -0
  41. package/src/components/Slider/MarksOption.tsx +115 -0
  42. package/src/components/Slider/Thumb.tsx +99 -54
  43. package/src/components/Slider/Track.tsx +73 -62
  44. package/src/components/Slider/context.tsx +40 -53
  45. package/src/components/Slider/index.tsx +237 -190
  46. package/src/components/Slider/type.ts +27 -9
  47. package/src/components/XYPad/Area.tsx +52 -33
  48. package/src/components/XYPad/Thumb.tsx +121 -60
  49. package/src/components/XYPad/context.tsx +49 -0
  50. package/src/components/XYPad/index.tsx +338 -246
  51. package/src/components/_util/Placement.tsx +68 -0
  52. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  53. package/src/components/_util/css-length.ts +13 -0
  54. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  55. package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
  56. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  57. package/src/hooks/useAnimationFrame.ts +17 -7
  58. package/src/hooks/useDrag.ts +39 -9
  59. package/src/hooks/useDragValue.ts +194 -0
  60. package/src/hooks/useEventListener.ts +11 -7
  61. package/src/hooks/useLongPress.ts +42 -8
  62. package/src/hooks/useMIDIAccess.ts +82 -11
  63. package/src/hooks/useMIDIInput.ts +45 -13
  64. package/src/hooks/useMIDIMessage.ts +23 -5
  65. package/src/hooks/useWheel.ts +29 -9
  66. package/src/index.ts +50 -27
  67. package/src/input-event.ts +32 -0
  68. package/dist/index.css +0 -536
  69. package/src/Globals.d.ts +0 -3
  70. package/src/components/AnimationCanvas/canvas.ts +0 -46
  71. package/src/components/Knob/index.css +0 -77
  72. package/src/components/NumberInput/InternalInput.tsx +0 -216
  73. package/src/components/NumberInput/index.css +0 -185
  74. package/src/components/NumberInput/type.ts +0 -58
  75. package/src/components/Piano/KeyLabel.tsx +0 -51
  76. package/src/components/Piano/context.tsx +0 -73
  77. package/src/components/Piano/index.css +0 -90
  78. package/src/components/Piano/key.tsx +0 -205
  79. package/src/components/Piano/keyboardShortcuts.ts +0 -31
  80. package/src/components/PointsEditor/index.css +0 -30
  81. package/src/components/Slider/Scale.tsx +0 -52
  82. package/src/components/Slider/ScaleOption.tsx +0 -103
  83. package/src/components/Slider/index.css +0 -112
  84. package/src/components/XYPad/index.css +0 -48
  85. package/src/components/_util/index.ts +0 -28
  86. package/src/components/_util/type.ts +0 -1
  87. package/src/hooks/useDragWithElement.ts +0 -100
  88. package/src/hooks/usePianoDrag.ts +0 -75
  89. package/src/hooks/useRefCallbackEvent.ts +0 -34
  90. package/src/styles/global.css +0 -19
@@ -1,205 +0,0 @@
1
- import clsx from 'clsx'
2
- import React, {
3
- CSSProperties,
4
- ComponentPropsWithoutRef,
5
- forwardRef,
6
- useImperativeHandle,
7
- useState,
8
- ReactElement,
9
- } from 'react'
10
-
11
- import { usePianoContext } from './context'
12
- import { KeyLabel, KeyLabelProps } from './KeyLabel'
13
-
14
- export interface KeyProps {
15
- noteNumber: number
16
-
17
- width?: number
18
- height?: number | `${number}%`
19
-
20
- bg?: string
21
- color?: string
22
- activeBg?: string
23
- activeColor?: string
24
-
25
- style?: CSSProperties
26
-
27
- /**
28
- * \<KeyLabel />
29
- */
30
- children?: ReactElement
31
-
32
- /** @internal */
33
- __width?: number
34
- }
35
-
36
- export interface KeyMethods {
37
- play: (velocity?: number) => void
38
- stop: () => void
39
- played: () => boolean
40
- }
41
-
42
- type Props = KeyProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyProps>
43
- type ImplProps = Props & { keyType: 'black' | 'white' }
44
-
45
- export const defaultWhiteKeyWidth = 40
46
- export const defaultBlackKeyWidth = 26
47
-
48
- const KeyImpl = forwardRef<KeyMethods, ImplProps>(
49
- (
50
- {
51
- keyType,
52
- noteNumber,
53
- width,
54
- height,
55
- bg,
56
- color,
57
- activeBg,
58
- activeColor,
59
- className,
60
- style,
61
- children,
62
- __width,
63
- ...props
64
- }: ImplProps,
65
- ref,
66
- ) => {
67
- const [played, setPlayed] = useState(false)
68
- const midiMax = usePianoContext((s) => s.midiMax)
69
- const fill = usePianoContext((s) => s.fill)
70
- // const glissando = usePianoContext(s => s.glissando)
71
- const onPlayNote = usePianoContext((s) => s.onPlayNote)
72
- const onStopNote = usePianoContext((s) => s.onStopNote)
73
- const label = usePianoContext((s) => s.label)
74
- const notePosition = usePianoContext((s) => s.notePosition)
75
- const position = notePosition(noteNumber)
76
-
77
- const disabled = noteNumber > midiMax
78
- const colors = {
79
- '--bg': bg,
80
- '--color': color,
81
- '--active-bg': activeBg,
82
- '--active-color': activeColor,
83
- }
84
-
85
- useImperativeHandle(ref, () => {
86
- return {
87
- play(velocity) {
88
- if (disabled) return
89
- setPlayed(true)
90
- if (onPlayNote) onPlayNote(noteNumber, velocity)
91
- },
92
- stop() {
93
- setPlayed(false)
94
- if (onStopNote) onStopNote(noteNumber)
95
- },
96
- played() {
97
- return played
98
- },
99
- }
100
- }, [disabled, noteNumber, onPlayNote, onStopNote, played])
101
-
102
- let keyLabelProps: KeyLabelProps = {}
103
- if (children != undefined) {
104
- React.Children.forEach(children, (child) => {
105
- if (React.isValidElement(child)) {
106
- if (child.type == KeyLabel) {
107
- keyLabelProps = child.props as KeyLabelProps
108
- } else {
109
- throw new Error('only <KeyLabel>')
110
- }
111
- } else {
112
- throw new Error('children is an invalid element.')
113
- }
114
- })
115
- }
116
-
117
- return (
118
- <div
119
- className={clsx(`tremolo-piano-${keyType}-key`, className)}
120
- aria-disabled={disabled}
121
- data-active={played}
122
- style={{
123
- ...colors,
124
- left: position,
125
- width: fill ? __width : width,
126
- height: height,
127
- ...style,
128
- }}
129
- // onPointerDown={() => {
130
- // if (disabled) return
131
- // setPlayed(true)
132
- // if (onPlayNote) onPlayNote(noteNumber)
133
- // }}
134
- // onPointerEnter={() => {
135
- // if (disabled) return
136
- // if (glissando) {
137
- // setPlayed(true)
138
- // if (onPlayNote) onPlayNote(noteNumber)
139
- // }
140
- // }}
141
- // onPointerUp={() => {
142
- // if (disabled) return
143
- // setPlayed(false)
144
- // if (onStopNote) onStopNote(noteNumber)
145
- // }}
146
- // onPointerLeave={() => {
147
- // if (disabled) return
148
- // if (glissando) {
149
- // setPlayed(false)
150
- // if (onStopNote) onStopNote(noteNumber)
151
- // }
152
- // }}
153
- {...props}
154
- >
155
- <KeyLabel label={label} __note={noteNumber} {...keyLabelProps} />
156
- </div>
157
- )
158
- },
159
- )
160
-
161
- export const WhiteKey = forwardRef<KeyMethods, Props>(
162
- (
163
- {
164
- width = defaultWhiteKeyWidth,
165
- height = '100%',
166
- onContextMenu = (e) => e.preventDefault(),
167
- ...props
168
- }: Props,
169
- ref,
170
- ) => {
171
- return (
172
- <KeyImpl
173
- ref={ref}
174
- keyType="white"
175
- width={width}
176
- height={height}
177
- onContextMenu={onContextMenu}
178
- {...props}
179
- />
180
- )
181
- },
182
- )
183
-
184
- export const BlackKey = forwardRef<KeyMethods, Props>(
185
- (
186
- {
187
- width = defaultBlackKeyWidth,
188
- height = '60%',
189
- onContextMenu = (e) => e.preventDefault(),
190
- ...props
191
- }: Props,
192
- ref,
193
- ) => {
194
- return (
195
- <KeyImpl
196
- ref={ref}
197
- keyType="black"
198
- width={width}
199
- height={height}
200
- onContextMenu={onContextMenu}
201
- {...props}
202
- />
203
- )
204
- },
205
- )
@@ -1,31 +0,0 @@
1
- export type KeyboardShortcuts = {
2
- keys: string[]
3
- // TODO
4
- flags?: {
5
- naturalOnly?: boolean
6
- }
7
- }
8
-
9
- export const SHORTCUTS = {
10
- HOME_ROW: {
11
- keys: [
12
- 'a',
13
- 'w',
14
- 's',
15
- 'e',
16
- 'd',
17
- 'f',
18
- 't',
19
- 'g',
20
- 'y',
21
- 'h',
22
- 'u',
23
- 'j',
24
- 'k',
25
- 'o',
26
- 'l',
27
- 'p',
28
- ';',
29
- ],
30
- },
31
- }
@@ -1,30 +0,0 @@
1
- .tremolo-points-editor {
2
- position: relative;
3
- }
4
-
5
- .tremolo-points-editor-background {
6
- position: absolute;
7
- inset: 0;
8
- z-index: 0;
9
- }
10
-
11
- .tremolo-points-editor-container {
12
- position: absolute;
13
- inset: 0;
14
- z-index: 10;
15
- }
16
-
17
- .tremolo-points-editor-point {
18
- --color: oklch(59.5% 0.17548 266.373);
19
-
20
- position: absolute;
21
- border-radius: 50%;
22
- outline: none;
23
- cursor: grab;
24
- background-color: var(--color);
25
- translate: -50% -50%;
26
-
27
- &[data-dragging='true'] {
28
- cursor: inherit;
29
- }
30
- }
@@ -1,52 +0,0 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
-
4
- import { xor } from '@tremolo-ui/functions'
5
-
6
- import { useSliderContext } from './context'
7
- import { ScaleOption } from './ScaleOption'
8
- import { generateOptionsList, ScaleOptions } from './type'
9
-
10
- export interface ScaleProps {
11
- gap?: number | string
12
- options?: ScaleOptions
13
- children?: ReactNode
14
- }
15
-
16
- export function Scale({
17
- gap = 6,
18
- options,
19
- children,
20
- className,
21
- style,
22
- ...props
23
- }: ScaleProps & Omit<ComponentPropsWithoutRef<'div'>, keyof ScaleProps>) {
24
- const min = useSliderContext((s) => s.min)
25
- const max = useSliderContext((s) => s.max)
26
- const step = useSliderContext((s) => s.step)
27
- const vertical = useSliderContext((s) => s.vertical)
28
- const reverse = useSliderContext((s) => s.reverse)
29
-
30
- const optionsList = options
31
- ? generateOptionsList(options, min, max, step)
32
- : []
33
- if (xor(vertical, reverse)) optionsList.reverse()
34
-
35
- return (
36
- <div
37
- className={clsx('tremolo-slider-scale', className)}
38
- style={{
39
- marginLeft: vertical ? gap : undefined,
40
- marginTop: !vertical ? gap : undefined,
41
- ...style,
42
- }}
43
- {...props}
44
- >
45
- {options
46
- ? optionsList.map(({ value, type }, index) => {
47
- return <ScaleOption key={index} value={value} type={type} />
48
- })
49
- : children}
50
- </div>
51
- )
52
- }
@@ -1,103 +0,0 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, CSSProperties, useCallback } from 'react'
3
-
4
- import { toFixed, xor, normalizeValue } from '@tremolo-ui/functions'
5
-
6
- import { useSliderContext } from './context'
7
- import { ScaleType } from './type'
8
-
9
- export interface ScaleOptionProps {
10
- // required
11
- value: number
12
-
13
- // optional
14
- type?: ScaleType
15
- /** Display text instead of value. */
16
- label?: string
17
- /** mark thickness */
18
- thickness?: number | string
19
- /** mark length */
20
- length?: number | string
21
- /** Gap between mark and label. */
22
- gap?: number | string
23
- labelWidth?: number | string
24
- classes?: {
25
- mark?: string
26
- label?: string
27
- }
28
- styles?: {
29
- mark?: CSSProperties
30
- label?: CSSProperties
31
- }
32
- }
33
-
34
- export function ScaleOption({
35
- value,
36
- type = 'mark-number',
37
- label,
38
- thickness = 1,
39
- length = '0.5rem',
40
- gap = 2,
41
- labelWidth,
42
- classes,
43
- styles,
44
- className,
45
- style,
46
- ...props
47
- }: ScaleOptionProps &
48
- Omit<ComponentPropsWithoutRef<'div'>, keyof ScaleOptionProps>) {
49
- const min = useSliderContext((s) => s.min)
50
- const max = useSliderContext((s) => s.max)
51
- const skew = useSliderContext((s) => s.skew)
52
- const vertical = useSliderContext((s) => s.vertical)
53
- const reverse = useSliderContext((s) => s.reverse)
54
-
55
- const calcPercent = useCallback(
56
- (rawValue: number) => {
57
- return toFixed(
58
- xor(vertical, reverse)
59
- ? 100 - normalizeValue(rawValue, min, max, skew) * 100
60
- : normalizeValue(rawValue, min, max, skew) * 100,
61
- )
62
- },
63
- [vertical, reverse, max, min, skew],
64
- )
65
-
66
- return (
67
- <div
68
- className={clsx('tremolo-slider-scale-option', className)}
69
- style={{
70
- left: !vertical ? `${calcPercent(value)}%` : undefined,
71
- top: vertical ? `${calcPercent(value)}%` : undefined,
72
- ...style,
73
- }}
74
- data-vertical={vertical}
75
- {...props}
76
- >
77
- {type != 'number' && (
78
- <div
79
- className={clsx('tremolo-slider-scale-option-mark', classes?.mark)}
80
- style={{
81
- width: !vertical ? thickness : length,
82
- height: vertical ? thickness : length,
83
- marginBottom: !vertical ? gap : undefined,
84
- marginRight: vertical ? gap : undefined,
85
- ...styles?.mark,
86
- }}
87
- data-vertical={vertical}
88
- ></div>
89
- )}
90
- {type != 'mark' && (
91
- <div
92
- className={clsx('tremolo-slider-scale-option-label', classes?.label)}
93
- style={{
94
- width: labelWidth,
95
- ...styles?.label,
96
- }}
97
- >
98
- {label || value}
99
- </div>
100
- )}
101
- </div>
102
- )
103
- }
@@ -1,112 +0,0 @@
1
- .tremolo-slider {
2
- display: inline-block;
3
- cursor: pointer;
4
- outline: 0;
5
- -webkit-tap-highlight-color: transparent;
6
-
7
- &[aria-readonly='true'] {
8
- cursor: not-allowed;
9
- }
10
- }
11
-
12
- .tremolo-slider-thumb-wrapper {
13
- position: absolute;
14
- translate: -50% -50%;
15
- z-index: 100;
16
- }
17
-
18
- .tremolo-slider-thumb {
19
- --color: oklch(59.5% 0.17548 266.373);
20
-
21
- background: var(--color);
22
- border-radius: 50%;
23
- outline: none;
24
-
25
- &[aria-disabled='true'] {
26
- background: oklch(50.462% 0.03307 270.021);
27
- }
28
-
29
- &[aria-readonly='false']:focus {
30
- box-shadow: 0px 0px 0px 3px
31
- color-mix(in srgb, var(--color) 20%, transparent);
32
- }
33
- }
34
-
35
- .tremolo-slider-track {
36
- --active: oklch(69.212% 0.12903 267.633);
37
- --inactive: oklch(95% 0 0);
38
-
39
- position: relative;
40
-
41
- &[data-vertical='false'] {
42
- min-width: 50px;
43
- }
44
-
45
- &[data-vertical='true'] {
46
- min-height: 50px;
47
- }
48
-
49
- &:hover {
50
- --active: oklch(64.276% 0.15244 266.932);
51
- --inactive: #e0e0e0;
52
- }
53
-
54
- &[aria-disabled='true'] {
55
- --active: oklch(75.327% 0.01237 264.547);
56
- --inactive: oklch(89.755% 0.0001 271.152);
57
- }
58
- }
59
-
60
- .tremolo-slider-scale {
61
- display: block;
62
- position: relative;
63
- }
64
-
65
- .tremolo-slider-scale-option {
66
- display: flex;
67
- color: oklch(25.197% 0.00003 271.152);
68
- flex-direction: column;
69
- justify-content: center;
70
- align-items: center;
71
- position: absolute;
72
- translate: -50% 0;
73
- z-index: 10;
74
-
75
- &[data-vertical='true'] {
76
- flex-direction: row;
77
- translate: 0 -50%;
78
- }
79
- }
80
-
81
- .tremolo-slider-scale-option-mark {
82
- background-color: currentColor;
83
- }
84
-
85
- .tremolo-slider-scale-option-label {
86
- text-align: right;
87
- }
88
-
89
- :where(.dark, [data-theme='dark']) {
90
- .tremolo-slider-thumb {
91
- --color: oklch(64.5% 0.17548 266.373);
92
-
93
- &:hover {
94
- --color: oklch(67.5% 0.17548 266.373);
95
- }
96
- }
97
-
98
- .tremolo-slider-track {
99
- --active: oklch(59.5% 0.17548 266.373);
100
- --inactive: oklch(41.7% 0 0);
101
-
102
- &:hover {
103
- --active: oklch(62.5% 0.17548 266.373);
104
- --inactive: oklch(44.7% 0 0);
105
- }
106
-
107
- &[aria-disabled='true'] {
108
- --active: oklch(75.327% 0.01237 264.547);
109
- --inactive: oklch(89.755% 0.0001 271.152);
110
- }
111
- }
112
- }
@@ -1,48 +0,0 @@
1
- .tremolo-xy-pad {
2
- display: inline-block;
3
- cursor: pointer;
4
- }
5
-
6
- .tremolo-xy-pad-area {
7
- --color: oklch(95% 0 0);
8
-
9
- position: relative;
10
- background-color: var(--color);
11
- }
12
-
13
- .tremolo-xy-pad-thumb-wrapper {
14
- position: absolute;
15
- translate: -50% -50%;
16
- z-index: 100;
17
- }
18
-
19
- .tremolo-xy-pad-thumb {
20
- --color: oklch(59.5% 0.17548 266.373);
21
-
22
- border-radius: 50%;
23
- outline: none;
24
- background-color: var(--color);
25
-
26
- &[aria-disabled='true'] {
27
- background-color: oklch(50.462% 0.03307 270.021);
28
- }
29
-
30
- &[aria-readonly='false']:focus {
31
- box-shadow: 0px 0px 0px 3px
32
- color-mix(in srgb, var(--color) 20%, transparent);
33
- }
34
- }
35
-
36
- :where(.dark, [data-theme='dark']) {
37
- .tremolo-xy-pad-area {
38
- --color: oklch(41.7% 0 0);
39
- }
40
-
41
- .tremolo-xy-pad-thumb {
42
- --color: oklch(64.5% 0.17548 266.373);
43
-
44
- &:hover {
45
- --color: oklch(67.5% 0.17548 266.373);
46
- }
47
- }
48
- }
@@ -1,28 +0,0 @@
1
- export function addUserSelectNone() {
2
- window.document.body.classList.add('tremolo-user-select-none')
3
- }
4
-
5
- export function removeUserSelectNone() {
6
- window.document.body.classList.remove('tremolo-user-select-none')
7
- }
8
-
9
- const cursorStyles = {
10
- grabbing: 'tremolo-cursor-grabbing',
11
- grab: 'tremolo-cursor-grab',
12
- pointer: 'tremolo-cursor-pointer',
13
- move: 'tremolo-cursor-move',
14
- none: 'tremolo-cursor-none',
15
- }
16
-
17
- export type Cursor = keyof typeof cursorStyles
18
-
19
- export function setCursorStyle(type: Cursor) {
20
- resetCursorStyle()
21
- window.document.body.classList.add(cursorStyles[type])
22
- }
23
-
24
- export function resetCursorStyle() {
25
- Object.values(cursorStyles).forEach((s) => {
26
- window.document.body.classList.remove(s)
27
- })
28
- }
@@ -1 +0,0 @@
1
- export type Override<T, U> = T & Omit<U, keyof T>
@@ -1,100 +0,0 @@
1
- import { RefObject, useCallback, useEffect, useRef, useState } from 'react'
2
-
3
- import { createDrag } from '@tremolo-ui/dom'
4
- import { normalizeValue } from '@tremolo-ui/functions'
5
-
6
- import { useCallbackRef } from './useCallbackRef'
7
-
8
- interface UseDragWithElement<T extends Element> {
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
22
- onDrag: (normalizedX: number, normalizedY: number) => void
23
- onDragStart?: (normalizedX: number, normalizedY: number) => void
24
- onDragEnd?: (normalizedX: number, normalizedY: number) => void
25
- }
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
- */
33
- export function useDragWithElement<T extends Element>({
34
- baseElementRef,
35
- updateOnPointerDown = false,
36
- cursor,
37
- onDrag,
38
- onDragStart,
39
- onDragEnd,
40
- }: UseDragWithElement<T>) {
41
- const [dragging, setDragging] = useState(false)
42
- const normalizedX = useRef(0)
43
- const normalizedY = useRef(0)
44
-
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
57
- },
58
- [baseElementRef],
59
- )
60
-
61
- // See useDrag for why the node is held in state.
62
- const [node, setNode] = useState<Element | null>(null)
63
-
64
- useEffect(() => {
65
- if (!node) return
66
-
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
- })
87
-
88
- return () => instance.destroy()
89
- }, [
90
- node,
91
- update,
92
- updateOnPointerDown,
93
- cursor,
94
- dragHandler,
95
- dragStartHandler,
96
- dragEndHandler,
97
- ])
98
-
99
- return { refCallback: setNode, dragging }
100
- }