@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,42 +1,61 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, CSSProperties, ReactElement } from 'react'
1
+ import {
2
+ ComponentPropsWithoutRef,
3
+ CSSProperties,
4
+ forwardRef,
5
+ ReactNode,
6
+ } from 'react'
7
+
8
+ import { useComposedRefs } from '../../compose-refs'
9
+ import { cssLength } from '../_util/css-length'
10
+ import { Placement } from '../_util/Placement'
11
+
12
+ import { useXYPadContext } from './context'
3
13
 
4
14
  export interface XYPadAreaProps {
15
+ /** Sets `--width`; the size the theme gives it stands when omitted. */
5
16
  width?: number | string
17
+ /** Sets `--height`; the size the theme gives it stands when omitted. */
6
18
  height?: number | string
19
+ /** Sets `--color`, for the theme to colour the area with. */
7
20
  color?: string
8
21
  className?: string
9
22
  style?: CSSProperties
10
- children?: ReactElement
11
-
12
- /** inherit */
13
- __thumb?: ReactElement
23
+ /** `<XYPad.Thumb />` goes here. */
24
+ children?: ReactNode
14
25
  }
15
26
 
16
- export function Area({
17
- width = 120,
18
- height = 120,
19
- color,
20
- children,
21
- className,
22
- style,
23
- __thumb,
24
- ...props
25
- }: XYPadAreaProps &
26
- Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>) {
27
- return (
28
- <div
29
- className={clsx('tremolo-xy-pad-area', className)}
30
- style={{
31
- ...{ '--color': color },
32
- width: width,
33
- height: height,
34
- ...style,
35
- }}
36
- {...props}
37
- >
38
- {children}
39
- {__thumb}
40
- </div>
41
- )
42
- }
27
+ type Props = XYPadAreaProps &
28
+ Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>
29
+
30
+ export const Area = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
31
+ function Area(
32
+ { width, height, color, children, className, style, ...props },
33
+ forwardedRef,
34
+ ) {
35
+ const { areaRef } = useXYPadContext()
36
+
37
+ // The area is what the pointer position is normalized against, so the
38
+ // context ref is composed with any ref the caller passed.
39
+ const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, areaRef)
40
+
41
+ return (
42
+ <div
43
+ ref={composedRef}
44
+ className={className}
45
+ style={
46
+ {
47
+ '--color': color,
48
+ '--width': cssLength(width),
49
+ '--height': cssLength(height),
50
+ // The thumb inside is placed against this box.
51
+ position: 'relative',
52
+ ...style,
53
+ } as CSSProperties
54
+ }
55
+ {...props}
56
+ >
57
+ <Placement name="XYPad.Area">{children}</Placement>
58
+ </div>
59
+ )
60
+ },
61
+ )
@@ -1,5 +1,5 @@
1
- import clsx from 'clsx'
2
1
  import {
2
+ AriaAttributes,
3
3
  ComponentPropsWithoutRef,
4
4
  CSSProperties,
5
5
  forwardRef,
@@ -8,24 +8,43 @@ import {
8
8
  useRef,
9
9
  } from 'react'
10
10
 
11
- export interface XYPadThumbProps {
12
- size?: number | string
13
- width?: number | string
14
- height?: number | string
11
+ import { useCheckPlacement } from '../_util/Placement'
12
+ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
15
13
 
16
- color?: string
14
+ import { toXY, useXYPadContext, type XYInput } from './context'
17
15
 
18
- wrapperClassName?: string
19
- wrapperStyle?: CSSProperties
16
+ export interface XYPadThumbProps {
17
+ /**
18
+ * Sets `--color`, for the theme to colour the thumb with. The size is not
19
+ * set here but by `--thumb-size` on `XYPad.Root`, so that the root can leave
20
+ * the matching room around the area.
21
+ */
22
+ color?: string
20
23
 
24
+ className?: string
25
+ style?: CSSProperties
26
+ /**
27
+ * Rendered inside the thumb. The thumb is one element either way, so what
28
+ * is passed here is decoration on top of it rather than a replacement for
29
+ * it — `className` and `style` are how its own appearance is changed.
30
+ */
21
31
  children?: ReactNode
22
32
 
23
- /** @internal */
24
- __disabled?: boolean
25
- /** @internal */
26
- __readonly?: boolean
27
- /** @internal */
28
- __css?: CSSProperties
33
+ /**
34
+ * The accessible name of each axis, as `[x, y]`. There are two range inputs
35
+ * inside the thumb, so each takes a name of its own; a single string names
36
+ * them both, which is rarely what you want.
37
+ */
38
+ 'aria-label'?: XYInput<AriaAttributes['aria-label']>
39
+ /** The ids of what labels each axis, as `[x, y]` or one for both. */
40
+ 'aria-labelledby'?: XYInput<AriaAttributes['aria-labelledby']>
41
+ /** The ids of what describes each axis, as `[x, y]` or one for both. */
42
+ 'aria-describedby'?: XYInput<AriaAttributes['aria-describedby']>
43
+ /**
44
+ * What the value of each axis means, when the number does not say it, as
45
+ * `[x, y]` or one for both.
46
+ */
47
+ 'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
29
48
  }
30
49
 
31
50
  export interface XYPadThumbMethods {
@@ -36,66 +55,108 @@ export interface XYPadThumbMethods {
36
55
  type Props = XYPadThumbProps &
37
56
  Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadThumbProps>
38
57
 
39
- export const Thumb = forwardRef<XYPadThumbMethods, Props>(
40
- (
58
+ export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
59
+ function Thumb(
41
60
  {
42
- size,
43
- width = 22,
44
- height = 22,
45
61
  color,
46
62
  children,
47
- wrapperClassName,
48
- wrapperStyle,
49
63
  className,
50
64
  style,
51
- __disabled,
52
- __readonly,
53
- __css,
65
+ 'aria-label': ariaLabel,
66
+ 'aria-labelledby': ariaLabelledby,
67
+ 'aria-describedby': ariaDescribedby,
68
+ 'aria-valuetext': ariaValuetext,
54
69
  ...props
55
- }: Props,
56
- ref,
57
- ) => {
58
- const wrapperRef = useRef<HTMLDivElement>(null)
70
+ },
71
+ forwardedRef,
72
+ ) {
73
+ const xInputRef = useRef<HTMLInputElement>(null)
74
+ const yInputRef = useRef<HTMLInputElement>(null)
75
+ const {
76
+ value,
77
+ min,
78
+ max,
79
+ step,
80
+ disabled,
81
+ readonly,
82
+ onChange,
83
+ percent,
84
+ thumbRef,
85
+ } = useXYPadContext()
86
+
87
+ // The thumb is positioned against the area.
88
+ useCheckPlacement('XYPad.Thumb', 'XYPad.Area')
59
89
 
60
- useImperativeHandle(ref, () => {
61
- return {
62
- focus() {
63
- wrapperRef.current?.focus()
64
- },
65
- blur() {
66
- wrapperRef.current?.blur()
67
- },
68
- }
69
- }, [])
90
+ // Per axis, since the thumb holds one input for each.
91
+ const labels = toXY(ariaLabel)
92
+ const labelledby = toXY(ariaLabelledby)
93
+ const describedby = toXY(ariaDescribedby)
94
+ const valueText = toXY(ariaValuetext)
95
+
96
+ const methods = () => ({
97
+ focus() {
98
+ if (!disabled) xInputRef.current?.focus()
99
+ },
100
+ blur() {
101
+ xInputRef.current?.blur()
102
+ yInputRef.current?.blur()
103
+ },
104
+ })
105
+
106
+ useImperativeHandle(forwardedRef, methods, [disabled])
107
+ // Root focuses the thumb when a drag starts, wherever the user placed it.
108
+ useImperativeHandle(thumbRef, methods, [disabled])
70
109
 
71
110
  return (
72
111
  <div
73
- className={clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName)}
112
+ className={className}
113
+ data-disabled={disabled ? '' : undefined}
114
+ data-readonly={readonly ? '' : undefined}
115
+ {...props}
74
116
  style={{
75
- ...__css,
76
- ...wrapperStyle,
117
+ ...{ '--color': color },
118
+ // Not a style but the mechanics of the position below: the thumb is
119
+ // placed by a percentage of the area, which needs it out of flow and
120
+ // measured from its own centre.
121
+ position: 'absolute',
122
+ translate: 'var(--translate, -50% -50%)',
123
+ zIndex: 100,
124
+ ...style,
125
+ // Where the thumb sits is the component's decision, not a style: a
126
+ // `left` from the caller would take it off the area, so it is written
127
+ // after theirs.
128
+ left: `${percent[0]}%`,
129
+ top: `${percent[1]}%`,
77
130
  }}
78
- {...props}
79
131
  >
80
- {children ? (
81
- children
82
- ) : (
83
- // default thumb
84
- <div
85
- ref={wrapperRef}
86
- className={clsx('tremolo-xy-pad-thumb', className)}
87
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
88
- tabIndex={0}
89
- aria-disabled={__disabled}
90
- aria-readonly={__readonly}
91
- style={{
92
- ...{ '--color': color },
93
- width: size ?? width,
94
- height: size ?? height,
95
- ...style,
132
+ {([0, 1] as const).map((axis) => (
133
+ <VisuallyHiddenRangeInput
134
+ key={axis}
135
+ ref={axis === 0 ? xInputRef : yInputRef}
136
+ data-axis={axis === 0 ? 'x' : 'y'}
137
+ value={value[axis]}
138
+ min={min[axis]}
139
+ max={max[axis]}
140
+ step={step[axis]}
141
+ disabled={disabled}
142
+ aria-readonly={readonly}
143
+ aria-orientation={axis === 0 ? 'horizontal' : 'vertical'}
144
+ aria-label={labels[axis] ?? (axis === 0 ? 'x' : 'y')}
145
+ aria-labelledby={labelledby[axis]}
146
+ aria-describedby={describedby[axis]}
147
+ aria-valuetext={valueText[axis]}
148
+ onChange={(event) => {
149
+ if (readonly) {
150
+ event.currentTarget.value = String(value[axis])
151
+ return
152
+ }
153
+ const next = [...value] as [number, number]
154
+ next[axis] = event.currentTarget.valueAsNumber
155
+ onChange?.(next)
96
156
  }}
97
- ></div>
98
- )}
157
+ />
158
+ ))}
159
+ {children}
99
160
  </div>
100
161
  )
101
162
  },
@@ -0,0 +1,49 @@
1
+ import { createContext, RefObject, useContext } from 'react'
2
+
3
+ import { toXY, type XY, type XYInput } from '@tremolo-ui/dom'
4
+ import type { Scale } from '@tremolo-ui/functions'
5
+
6
+ import type { XYPadThumbMethods } from './Thumb'
7
+
8
+ export { toXY, type XY, type XYInput }
9
+
10
+ export type XYPadContextValue = {
11
+ value: XY<number>
12
+ min: XY<number>
13
+ max: XY<number>
14
+ step: XY<number>
15
+ /** How the value of each axis is distributed across the travel. */
16
+ scale: XY<Scale>
17
+ reverse: XY<boolean>
18
+ disabled: boolean
19
+ readonly: boolean
20
+ onChange?: (value: XY<number>) => void
21
+
22
+ /**
23
+ * Position of the thumb, 0-100 per axis, already accounting for `reverse`.
24
+ */
25
+ percent: XY<number>
26
+
27
+ /** `Area` registers its element here; `Root` normalizes the pointer against it. */
28
+ areaRef: RefObject<HTMLDivElement | null>
29
+ /** `Thumb` registers itself here; `Root` focuses it when a drag starts. */
30
+ thumbRef: RefObject<XYPadThumbMethods | null>
31
+ }
32
+
33
+ const XYPadContext = /* @__PURE__ */ createContext<XYPadContextValue | null>(
34
+ null,
35
+ )
36
+
37
+ export const XYPadProvider = XYPadContext.Provider
38
+
39
+ /**
40
+ * Everything here is derived during render, so there is no state to keep in
41
+ * sync: `value` comes from the props of `Root` and the rest follows from it.
42
+ */
43
+ export function useXYPadContext(): XYPadContextValue
44
+ export function useXYPadContext<T>(selector: (state: XYPadContextValue) => T): T
45
+ export function useXYPadContext<T>(selector?: (state: XYPadContextValue) => T) {
46
+ const context = useContext(XYPadContext)
47
+ if (!context) throw new Error('Missing XYPadContext.Provider in the tree')
48
+ return selector ? selector(context) : context
49
+ }