@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,92 +1,172 @@
1
- import clsx from 'clsx'
2
1
  import React, {
3
2
  ComponentPropsWithoutRef,
4
3
  CSSProperties,
5
4
  forwardRef,
6
- ReactElement,
5
+ ReactNode,
7
6
  useCallback,
8
7
  useImperativeHandle,
9
8
  useMemo,
10
9
  useRef,
11
10
  } from 'react'
12
11
 
12
+ import type { AxisOptions, XY } from '@tremolo-ui/dom'
13
13
  import {
14
- clamp,
15
- normalizeValue,
16
- rawValue,
17
- stepValue,
14
+ applyDelta,
15
+ linearScale,
18
16
  toFixed,
19
- InputEventOption,
20
- styleHelper,
17
+ type InputEventOption,
18
+ type ModifierValue,
19
+ selectModifier,
21
20
  xor,
21
+ type Scale,
22
22
  } from '@tremolo-ui/functions'
23
23
 
24
- import { useDragWithElement } from '../../hooks/useDragWithElement'
24
+ import { useComposedRefs } from '../../compose-refs'
25
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
26
+ import { useDragValue } from '../../hooks/useDragValue'
25
27
  import { useWheel } from '../../hooks/useWheel'
26
- import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
28
+ import {
29
+ DEFAULT_DRAG_SENSITIVITY,
30
+ DEFAULT_KEYBOARD_OPTIONS,
31
+ DEFAULT_WHEEL_OPTIONS,
32
+ } from '../../input-event'
27
33
 
28
34
  import { SliderProvider } from './context'
29
- import { Scale } from './Scale'
30
- import { ScaleOption } from './ScaleOption'
31
- import { Thumb, SliderThumbMethods, SliderThumbProps } from './Thumb'
32
- import { Track, SliderTrackProps } from './Track'
35
+ import { Marks } from './Marks'
36
+ import { MarksOption } from './MarksOption'
37
+ import { Thumb, SliderThumbMethods } from './Thumb'
38
+ import { Track } from './Track'
33
39
 
34
40
  const defaultExternalStyles: SliderProps['externalStyles'] = {
35
- userSelectNone: true,
36
41
  cursor: 'pointer',
37
42
  }
38
43
 
39
44
  export interface SliderProps {
40
- // required
45
+ /** The current value. The slider shows only this, so update it from `onChange`. */
41
46
  value: number
47
+ /** The value at the start of the travel. */
42
48
  min: number
49
+ /** The value at the end of the travel. */
43
50
  max: number
44
51
 
45
- // optional
52
+ /**
53
+ * Granularity of the value. A drag, the wheel and the arrow keys snap it to
54
+ * multiples of `step`.
55
+ *
56
+ * @default 1
57
+ */
46
58
  step?: number
47
- skew?: number // TODO | SkewFunction
48
59
  /**
49
- * slider orientation
50
- * aria-orientation property is also applied.
60
+ * How the value is distributed across the travel.
61
+ *
62
+ * Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
63
+ * `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
64
+ * `skewScale(n)` for a value that has to match a JUCE parameter.
65
+ *
66
+ * @default linearScale
67
+ */
68
+ scale?: Scale
69
+ /**
70
+ * Run the slider vertically, with the value growing upwards. The range input
71
+ * inside the thumb takes its `aria-orientation` from this.
72
+ *
73
+ * @default false
51
74
  */
52
75
  vertical?: boolean
76
+ /**
77
+ * Grow the value the other way: leftwards, or downwards when `vertical`.
78
+ * The arrow keys follow the direction on screen.
79
+ *
80
+ * @default false
81
+ */
53
82
  reverse?: boolean
54
83
 
55
- /** Global style to apply when dragged */
84
+ /**
85
+ * The cursor to show while dragging. It is set on the dragged element, so it
86
+ * stays while the pointer is outside the slider.
87
+ *
88
+ * @default { cursor: 'pointer' }
89
+ */
56
90
  externalStyles?: {
57
- userSelectNone?: boolean
58
- cursor?: Cursor
91
+ cursor?: CSSProperties['cursor']
59
92
  }
60
93
  /**
61
- * wheel control option
62
- * If null, no event will be triggered
94
+ * How much one notch of the wheel moves the value. It only acts while the
95
+ * focus is inside, so that scrolling the page past the slider leaves it
96
+ * alone.
97
+ *
98
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
99
+ * range between `min` and `max`. The result is snapped to `step`, except for
100
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
101
+ * the wheel off.
102
+ *
103
+ * @default ['raw', 1]
63
104
  */
64
- wheel?: InputEventOption | null
105
+ wheel?: ModifierValue<InputEventOption> | null
65
106
  /**
66
- * keyboard control option
67
- * If null, no event will be triggered
107
+ * How much a drag moves the value, per modifier key.
108
+ *
109
+ * `1` is the pointer position itself, which is what a drag normally is here.
110
+ * **Anything else turns the drag relative**: `0.1` makes the same movement
111
+ * cover a tenth of the travel, so the value stops following the pointer and
112
+ * starts moving a tenth as fast. Shift is bound to `0.1` by default, to
113
+ * match what it does on the arrow keys.
114
+ *
115
+ * Pressing or releasing the key mid-drag does not disturb the value: the
116
+ * travel so far is kept and the new sensitivity applies from there. **The
117
+ * pointer and the value stay apart for the rest of the drag** — snapping
118
+ * them back together on release would move the value nobody asked to move.
119
+ *
120
+ * @default { default: 1, shift: 0.1 }
68
121
  */
69
- keyboard?: InputEventOption | null
122
+ dragSensitivity?: ModifierValue<number>
70
123
 
71
124
  /**
72
- * Only the appearance will change.
73
- * Please consider using with readonly.
74
- * aria-disabled property is also applied.
125
+ * How much one arrow key press moves the value.
126
+ *
127
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
128
+ * range between `min` and `max`. The result is snapped to `step`, except for
129
+ * an amount set on a modifier key, which is what lets shift move off the
130
+ * grid. `null` turns the arrow keys off.
131
+ *
132
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
133
+ * the amount to match: 1 would round straight back to where it started, and
134
+ * a development build warns about it.
135
+ *
136
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
137
+ */
138
+ keyboard?: ModifierValue<InputEventOption> | null
139
+
140
+ /**
141
+ * Make the slider unchangeable and remove it from the tab order.
142
+ * The parts carry `data-disabled` while it is set.
75
143
  */
76
144
  disabled?: boolean
77
145
  /**
78
146
  * Make the value unchangeable.
79
- * aria-readonly property is also applied.
147
+ * The parts carry `data-readonly` while it is set.
80
148
  */
81
149
  readonly?: boolean
82
150
  className?: string
83
151
  style?: CSSProperties
152
+ /** Called with the new value when a drag, the wheel or an arrow key moves it. */
84
153
  onChange?: (value: number) => void
154
+ /** Called when a drag starts, with the value where the track was pressed. */
85
155
  onDragStart?: (value: number) => void
156
+ /** Called when the drag ends, with the value it ended on. */
86
157
  onDragEnd?: (value: number) => void
87
- /** \<SliderThumb /> | \<SliderTrack /> */
88
- children?: ReactElement | ReactElement[]
89
- // ReactElement<typeof SliderThumb, typeof SliderTrack>[]
158
+ /**
159
+ * The slider renders exactly what you compose here; there is no default
160
+ * markup to fall back to.
161
+ *
162
+ * @example
163
+ * <Slider.Root value={value} min={0} max={100} onChange={setValue}>
164
+ * <Slider.Track>
165
+ * <Slider.Thumb aria-label="Level" />
166
+ * </Slider.Track>
167
+ * </Slider.Root>
168
+ */
169
+ children: ReactNode
90
170
  }
91
171
 
92
172
  export interface SliderMethods {
@@ -97,19 +177,26 @@ export interface SliderMethods {
97
177
  type Props = SliderProps &
98
178
  Omit<ComponentPropsWithoutRef<'div'>, keyof SliderProps>
99
179
 
100
- const Root = forwardRef<SliderMethods, Props>(
180
+ /**
181
+ * The wheel only acts while the focus is inside, so that scrolling a page past
182
+ * the control does not change its value.
183
+ */
184
+ const WHEEL_OPTIONS = { requireFocus: true }
185
+
186
+ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
101
187
  (
102
188
  {
103
189
  value,
104
190
  min,
105
191
  max,
106
192
  step = 1,
107
- skew = 1,
193
+ scale = linearScale,
108
194
  vertical = false,
109
195
  reverse = false,
110
196
  externalStyles: _externalStyles,
111
- wheel = ['raw', 1],
112
- keyboard = ['raw', 1],
197
+ wheel = DEFAULT_WHEEL_OPTIONS,
198
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
199
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
113
200
  disabled = false,
114
201
  readonly = false,
115
202
  onChange,
@@ -127,13 +214,13 @@ const Root = forwardRef<SliderMethods, Props>(
127
214
  forwardedRef,
128
215
  ) => {
129
216
  // -- state and ref ---
130
- const trackElementRef = useRef<HTMLDivElement>(null)
217
+ const trackRef = useRef<HTMLDivElement>(null)
131
218
  const thumbRef = useRef<SliderThumbMethods>(null)
132
-
133
219
  // --- interpret props ---
134
220
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
221
+ const inactive = disabled || readonly
135
222
 
136
- const p = toFixed(normalizeValue(value, min, max, skew) * 100)
223
+ const p = toFixed(scale.normalize(value, min, max) * 100)
137
224
  const rev = toFixed(100 - p)
138
225
  // NOTE
139
226
  // normal -> normal (right)
@@ -146,165 +233,143 @@ const Root = forwardRef<SliderMethods, Props>(
146
233
  )
147
234
  const percent = displayReversed ? rev : p
148
235
 
149
- let trackProps: SliderTrackProps = {}
150
- let thumbProps: SliderThumbProps = {}
151
- let scaleComponent: ReactElement | undefined = undefined
152
- if (children != undefined) {
153
- React.Children.forEach(children, (child) => {
154
- if (React.isValidElement(child)) {
155
- if (child.type == Thumb) {
156
- thumbProps = child.props as SliderThumbProps
157
- } else if (child.type == Track) {
158
- trackProps = child.props as SliderTrackProps
159
- } else if (child.type == Scale) {
160
- scaleComponent = child
161
- } else {
162
- throw new Error('only <SliderThumb> or <SliderTrack>')
163
- }
164
- } else {
165
- throw new Error('children is an invalid element.')
166
- }
167
- })
168
- }
169
-
170
236
  // --- internal functions ---
171
- const onDrag = useCallback(
172
- (nx: number, ny: number) => {
173
- if (!onChange || readonly) return
174
- const n = vertical ? ny : nx
175
- const v = rawValue(displayReversed ? 1 - n : n, min, max, skew)
176
- const v2 = clamp(stepValue(v, step), min, max)
177
- onChange(v2)
178
- },
179
- [displayReversed, max, min, onChange, readonly, skew, step, vertical],
237
+ // The pointer is normalized against the track on both axes; only the one
238
+ // the slider runs along is read back. `AxisOptions` extends `ValueRange`,
239
+ // so the same object also describes the scaling for `applyDelta`.
240
+ const axis: AxisOptions = useMemo(
241
+ () => ({ min, max, step, scale, reverse: displayReversed }),
242
+ [min, max, step, scale, displayReversed],
180
243
  )
181
244
 
182
- const updateValueByEvent = useCallback(
183
- (eventType: InputEventOption[0], x: number) => {
184
- let newValue
185
- if (eventType == 'normalized') {
186
- const n = normalizeValue(value, min, max, skew)
187
- newValue = rawValue(n + x, min, max, skew)
188
- } else {
189
- newValue = value + x
190
- }
191
- return clamp(stepValue(newValue, step), min, max)
192
- },
193
- [max, min, skew, step, value],
194
- )
245
+ useCheckSteps({ component: 'Slider', range: axis, keyboard, wheel })
195
246
 
196
247
  const handleKeyDown = useCallback(
197
248
  (event: React.KeyboardEvent<HTMLDivElement>) => {
198
- if (!keyboard || !onChange || readonly) return
199
249
  const key = event.key
200
- if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
201
- event.preventDefault()
202
- let x =
203
- key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
204
- if (reverse) x *= -1
205
- onChange(updateValueByEvent(keyboard[0], x))
206
- }
250
+ if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
251
+ return
252
+ event.preventDefault()
253
+ if (!keyboard || !onChange || inactive) return
254
+ let direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
255
+ if (reverse) direction *= -1
256
+ onChange(applyDelta(value, direction, keyboard, axis, event))
207
257
  },
208
- [keyboard, onChange, readonly, reverse, updateValueByEvent],
258
+ [keyboard, onChange, inactive, reverse, value, axis],
209
259
  )
210
260
 
211
261
  // --- hooks ---
212
- const { refCallback: dragRefCallback } = useDragWithElement<HTMLDivElement>(
213
- {
214
- baseElementRef: trackElementRef,
215
- cursor: readonly ? undefined : externalStyles.cursor,
216
- updateOnPointerDown: true,
217
- onDrag: onDrag,
218
- onDragStart: (nx, ny) => {
219
- if (readonly) return
220
- if (externalStyles.userSelectNone) addUserSelectNone()
221
-
222
- thumbRef.current?.focus()
223
- onDragStart?.(
224
- clamp(
225
- stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
226
- min,
227
- max,
228
- ),
229
- )
230
- },
231
- onDragEnd: (nx, ny) => {
232
- if (readonly) return
262
+ const valueOf = (v: XY<number>) => v[vertical ? 1 : 0]
233
263
 
234
- if (externalStyles.userSelectNone) removeUserSelectNone()
235
-
236
- onDragEnd?.(
237
- clamp(
238
- stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
239
- min,
240
- max,
241
- ),
242
- )
243
- },
264
+ const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
265
+ axis,
266
+ baseElementRef: trackRef,
267
+ cursor: inactive ? undefined : externalStyles.cursor,
268
+ shouldStart: () => !inactive,
269
+ sensitivity: (state) =>
270
+ selectModifier(dragSensitivity, state.event).value,
271
+ updateOnPointerDown: true,
272
+ onChange: (v) => {
273
+ if (inactive) return
274
+ onChange?.(valueOf(v))
244
275
  },
245
- )
276
+ onDragStart: (v) => {
277
+ if (inactive) return
278
+ thumbRef.current?.focus()
279
+ onDragStart?.(valueOf(v))
280
+ },
281
+ onDragEnd: (v) => {
282
+ if (inactive) return
283
+ onDragEnd?.(valueOf(v))
284
+ },
285
+ })
246
286
 
247
287
  const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
248
- if (!wheel || !onChange || readonly) return
288
+ if (!wheel || !onChange || inactive) return
249
289
  event.preventDefault()
250
- let x
251
- if (!vertical && event.deltaX != 0) {
252
- x = event.deltaX > 0 ? wheel[1] : -wheel[1]
290
+ let direction
291
+ if (!vertical && event.deltaX !== 0) {
292
+ direction = event.deltaX > 0 ? 1 : -1
253
293
  } else {
254
- if (event.deltaY == 0) return
255
- x = event.deltaY > 0 ? -wheel[1] : wheel[1]
294
+ if (event.deltaY === 0) return
295
+ direction = event.deltaY > 0 ? -1 : 1
256
296
  }
257
- if (vertical && reverse) x *= -1
258
- onChange(updateValueByEvent(wheel[0], x))
259
- })
297
+ if (reverse) direction *= -1
298
+ onChange(applyDelta(value, direction, wheel, axis, event))
299
+ }, WHEEL_OPTIONS)
300
+
301
+ // Composed once, so React attaches the refs a single time instead of
302
+ // detaching and re-attaching on every render.
303
+ const rootRefCallback = useComposedRefs<HTMLDivElement>(
304
+ dragRefCallback,
305
+ wheelRefCallback,
306
+ )
307
+
308
+ const context = useMemo(
309
+ () => ({
310
+ value,
311
+ min,
312
+ max,
313
+ step,
314
+ scale,
315
+ vertical,
316
+ reverse,
317
+ disabled,
318
+ readonly,
319
+ onChange,
320
+ percent,
321
+ trackRef,
322
+ thumbRef,
323
+ }),
324
+ [
325
+ value,
326
+ min,
327
+ max,
328
+ step,
329
+ scale,
330
+ vertical,
331
+ reverse,
332
+ disabled,
333
+ readonly,
334
+ onChange,
335
+ percent,
336
+ ],
337
+ )
260
338
 
261
339
  useImperativeHandle(forwardedRef, () => {
262
340
  return {
263
341
  focus() {
264
- thumbRef.current?.focus()
342
+ if (!disabled) thumbRef.current?.focus()
265
343
  },
266
344
  blur() {
267
345
  thumbRef.current?.blur()
268
346
  },
269
347
  }
270
- }, [])
348
+ }, [disabled])
271
349
 
272
350
  return (
273
- <SliderProvider
274
- min={min}
275
- max={max}
276
- step={step}
277
- skew={skew}
278
- vertical={vertical}
279
- reverse={reverse}
280
- disabled={disabled}
281
- readonly={readonly}
282
- >
351
+ <SliderProvider value={context}>
352
+ {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the group is the pointer and keyboard event area, while the nested range input carries its control semantics */}
283
353
  <div
284
- className={clsx('tremolo-slider', className)}
285
- ref={(div) => {
286
- wheelRefCallback(div)
287
- dragRefCallback(div)
288
- }}
354
+ className={className}
355
+ ref={rootRefCallback}
356
+ role="group"
357
+ // A press lands on the track or the thumb, neither of which can hold
358
+ // focus, and the browser answers that by clearing the focus to the
359
+ // body — undoing the focus the drag just gave the input. Taking the
360
+ // focus here keeps it inside, and `onFocus` passes it to the input.
289
361
  tabIndex={-1}
290
- role="slider"
291
- aria-valuenow={value}
292
- aria-valuemin={min}
293
- aria-valuemax={max}
294
- aria-orientation={vertical ? 'vertical' : 'horizontal'}
295
- aria-disabled={disabled}
296
- aria-readonly={readonly}
297
- style={{
298
- margin: `calc(${styleHelper(thumbProps?.size ?? 22)} / 2)`, // half thumb size
299
- ...style,
300
- }}
362
+ data-orientation={vertical ? 'vertical' : 'horizontal'}
363
+ data-disabled={disabled ? '' : undefined}
364
+ data-readonly={readonly ? '' : undefined}
365
+ style={style}
301
366
  onPointerDown={onPointerDown}
302
367
  onKeyDown={(event) => {
303
368
  handleKeyDown(event)
304
369
  onKeyDown?.(event)
305
370
  }}
306
371
  onFocus={(event) => {
307
- thumbRef.current?.focus()
372
+ if (!disabled) thumbRef.current?.focus()
308
373
  onFocus?.(event)
309
374
  }}
310
375
  onBlur={(event) => {
@@ -313,25 +378,7 @@ const Root = forwardRef<SliderMethods, Props>(
313
378
  }}
314
379
  {...props}
315
380
  >
316
- <div
317
- style={{
318
- display: 'flex',
319
- flexDirection: vertical ? 'row' : 'column',
320
- width: !vertical ? '100%' : undefined,
321
- height: vertical ? '100%' : undefined,
322
- }}
323
- >
324
- <div className="tremolo-slider-track-wrapper" ref={trackElementRef}>
325
- <Track
326
- __percent={percent}
327
- __thumb={
328
- <Thumb ref={thumbRef} __percent={percent} {...thumbProps} />
329
- }
330
- {...trackProps}
331
- />
332
- </div>
333
- {scaleComponent}
334
- </div>
381
+ {children}
335
382
  </div>
336
383
  </SliderProvider>
337
384
  )
@@ -345,13 +392,13 @@ export const Slider = {
345
392
  Root,
346
393
  Thumb,
347
394
  Track,
348
- Scale,
349
- ScaleOption,
395
+ Marks,
396
+ MarksOption,
350
397
  }
351
398
 
352
- export { useSliderContext } from './context'
399
+ export { useSliderContext, type SliderContextValue } from './context'
353
400
  export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
354
401
  export { type SliderTrackProps } from './Track'
355
- export { type ScaleProps } from './Scale'
356
- export { type ScaleOptionProps } from './ScaleOption'
357
- export { type ScaleOptions, type ScaleType } from './type'
402
+ export { type SliderMarksProps } from './Marks'
403
+ export { type SliderMarksOptionProps } from './MarksOption'
404
+ export { type MarksOptions } from './type'
@@ -1,26 +1,44 @@
1
1
  import { decimalPart, toFixed } from '@tremolo-ui/functions'
2
2
 
3
- export type ScaleType = 'mark' | 'mark-number' | 'number'
4
- export type ScaleOptions = ['step', ScaleType] | [number, ScaleType]
3
+ /**
4
+ * How `Slider.Marks` fills itself in when it is given no children: one option
5
+ * every `per`, or every `step` of the slider. The object form turns off the
6
+ * mark or the label for the whole set; a single option is customized by
7
+ * writing `Slider.MarksOption` out instead.
8
+ */
9
+ export type MarksOptions =
10
+ | 'step'
11
+ | number
12
+ | {
13
+ per: 'step' | number
14
+ mark?: boolean
15
+ label?: boolean
16
+ }
5
17
 
6
18
  export function generateOptionsList(
7
- options: ScaleOptions,
19
+ options: MarksOptions,
8
20
  min: number,
9
21
  max: number,
10
22
  step: number,
11
23
  ) {
24
+ const {
25
+ per,
26
+ mark = true,
27
+ label = true,
28
+ } = typeof options === 'object' ? options : { per: options }
12
29
  const optionsList: {
13
30
  value: number
14
- type: ScaleType
31
+ mark: boolean
32
+ label: boolean
15
33
  }[] = []
16
- const per = options[0] == 'step' ? step : options[0]
17
- const count = Math.floor(max / per) - Math.ceil(min / per) + 1
34
+ const interval = per === 'step' ? step : per
35
+ const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1
18
36
  for (let i = 0; i < count; i++) {
19
37
  const value = toFixed(
20
- per * (Math.ceil(min / per) + i),
21
- decimalPart(per)?.length,
38
+ interval * (Math.ceil(min / interval) + i),
39
+ decimalPart(interval)?.length,
22
40
  )
23
- optionsList.push({ value: value, type: options[1] })
41
+ optionsList.push({ value: value, mark: mark, label: label })
24
42
  }
25
43
  return optionsList
26
44
  }