@tremolo-ui/react 0.6.0 → 0.7.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.
- package/dist/index.cjs +252 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +550 -133
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +550 -133
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +218 -39
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/DropZone/index.tsx +100 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +144 -0
- package/src/components/Knob/index.tsx +8 -5
- package/src/components/NumberInput/DecrementStepper.tsx +2 -13
- package/src/components/NumberInput/IncrementStepper.tsx +2 -14
- package/src/components/NumberInput/InputField.tsx +5 -11
- package/src/components/NumberInput/Stepper.tsx +4 -19
- package/src/components/NumberInput/StepperButton.tsx +13 -8
- package/src/components/NumberInput/context.tsx +6 -7
- package/src/components/NumberInput/index.tsx +4 -13
- package/src/components/Piano/index.tsx +10 -13
- package/src/components/PointsEditor/Background.tsx +2 -8
- package/src/components/PointsEditor/Container.tsx +2 -8
- package/src/components/PointsEditor/Point.tsx +7 -5
- package/src/components/PointsEditor/SelectionBox.tsx +2 -8
- package/src/components/PointsEditor/context.tsx +6 -6
- package/src/components/PointsEditor/index.tsx +9 -11
- package/src/components/Slider/Marks.tsx +6 -9
- package/src/components/Slider/MarksOption.tsx +7 -2
- package/src/components/Slider/Thumb.tsx +9 -15
- package/src/components/Slider/Track.tsx +6 -13
- package/src/components/Slider/index.tsx +5 -12
- package/src/components/Slider/type.ts +4 -2
- package/src/components/XYPad/Area.tsx +5 -10
- package/src/components/XYPad/Thumb.tsx +5 -11
- package/src/components/XYPad/index.tsx +3 -5
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
- package/src/components/_util/decimal-digits.ts +23 -0
- package/src/components/_util/visually-hidden.ts +25 -0
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCheckSteps.ts +2 -3
- package/src/hooks/useDrag.ts +13 -4
- package/src/hooks/useDragValue.ts +17 -2
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +12 -9
- package/src/input-event.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef, forwardRef
|
|
1
|
+
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useComposedRefs } from '../../compose-refs'
|
|
4
4
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
@@ -8,13 +8,7 @@ import { Placement } from '../_util/Placement'
|
|
|
8
8
|
import { usePointsEditorContext } from './context'
|
|
9
9
|
import { AXIS } from './Point'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
/** `<PointsEditor.Point />` goes here. */
|
|
13
|
-
children?: ReactNode
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type Props = PointsEditorContainerProps &
|
|
17
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorContainerProps>
|
|
11
|
+
type Props = ComponentPropsWithoutRef<'div'>
|
|
18
12
|
|
|
19
13
|
export const Container = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
20
14
|
function Container({ children, className, style, ...props }, forwardedRef) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
3
|
CSSProperties,
|
|
4
|
-
ReactNode,
|
|
5
4
|
useCallback,
|
|
6
5
|
useEffect,
|
|
7
6
|
useId,
|
|
@@ -11,12 +10,12 @@ import {
|
|
|
11
10
|
|
|
12
11
|
import {
|
|
13
12
|
applyDelta,
|
|
14
|
-
clamp,
|
|
15
13
|
type InputEventOption,
|
|
16
14
|
type ModifierState,
|
|
17
15
|
type ModifierValue,
|
|
18
16
|
selectModifier,
|
|
19
|
-
} from '@tremolo-ui/
|
|
17
|
+
} from '@tremolo-ui/dom'
|
|
18
|
+
import { clamp } from '@tremolo-ui/functions'
|
|
20
19
|
|
|
21
20
|
import { useComposedRefs } from '../../compose-refs'
|
|
22
21
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
@@ -26,6 +25,8 @@ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
|
26
25
|
|
|
27
26
|
import { type PointRegistration, usePointsEditorContext } from './context'
|
|
28
27
|
|
|
28
|
+
import type { CSSVariables } from '../../css-variables'
|
|
29
|
+
|
|
29
30
|
export type PointBaseType = { x: number; y: number }
|
|
30
31
|
|
|
31
32
|
export function clampPoint(
|
|
@@ -45,8 +46,6 @@ export interface PointsEditorPointProps<T extends PointBaseType> {
|
|
|
45
46
|
* growing downwards.
|
|
46
47
|
*/
|
|
47
48
|
value: T
|
|
48
|
-
/** Drawn inside the point: the theme's own dot stands when it is left out. */
|
|
49
|
-
children?: ReactNode
|
|
50
49
|
/**
|
|
51
50
|
* How the selection refers to this point. One is generated when it is left
|
|
52
51
|
* out, which lasts as long as the point is mounted — give your own if the
|
|
@@ -94,6 +93,9 @@ export interface PointsEditorPointProps<T extends PointBaseType> {
|
|
|
94
93
|
onDragStart?: (value: PointBaseType) => void
|
|
95
94
|
/** Called when that drag ends, with where the point is. */
|
|
96
95
|
onDragEnd?: (value: PointBaseType) => void
|
|
96
|
+
|
|
97
|
+
style?: CSSProperties &
|
|
98
|
+
CSSVariables<'width' | 'height' | 'color' | 'translate'>
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef, CSSProperties
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useCheckPlacement } from '../_util/Placement'
|
|
4
4
|
|
|
5
5
|
import { usePointsEditorContext } from './context'
|
|
6
6
|
|
|
7
|
-
export interface PointsEditorSelectionBoxProps {
|
|
8
|
-
/** Drawn inside the box, which is only there while a drag is running. */
|
|
9
|
-
children?: ReactNode
|
|
10
|
-
}
|
|
11
|
-
|
|
12
7
|
/**
|
|
13
8
|
* The box a drag on empty space draws, and what it covers is selected.
|
|
14
9
|
*
|
|
@@ -20,8 +15,7 @@ export function SelectionBox({
|
|
|
20
15
|
className,
|
|
21
16
|
style,
|
|
22
17
|
...props
|
|
23
|
-
}:
|
|
24
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorSelectionBoxProps>) {
|
|
18
|
+
}: ComponentPropsWithoutRef<'div'>) {
|
|
25
19
|
useCheckPlacement('PointsEditor.SelectionBox', 'PointsEditor.Container')
|
|
26
20
|
|
|
27
21
|
const selectionBox = usePointsEditorContext((s) => s.selectionBox)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createContext, CSSProperties, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '@tremolo-ui/
|
|
3
|
+
import {
|
|
4
|
+
type InputEventOption,
|
|
5
|
+
type ModifierState,
|
|
6
|
+
type ModifierValue,
|
|
7
|
+
type SelectionBoxRect,
|
|
8
|
+
} from '@tremolo-ui/dom'
|
|
9
9
|
|
|
10
10
|
import type { PointBaseType } from './Point'
|
|
11
11
|
|
|
@@ -12,19 +12,16 @@ import {
|
|
|
12
12
|
} from 'react'
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
+
applyDelta,
|
|
15
16
|
createSelectionBox,
|
|
17
|
+
type InputEventOption,
|
|
18
|
+
type ModifierState,
|
|
19
|
+
type ModifierValue,
|
|
16
20
|
type SelectionBoxInstance,
|
|
17
21
|
type SelectionBoxRect,
|
|
18
22
|
type XY,
|
|
19
23
|
} from '@tremolo-ui/dom'
|
|
20
|
-
import {
|
|
21
|
-
applyDelta,
|
|
22
|
-
clamp,
|
|
23
|
-
type InputEventOption,
|
|
24
|
-
type ModifierState,
|
|
25
|
-
type ModifierValue,
|
|
26
|
-
toPrecision,
|
|
27
|
-
} from '@tremolo-ui/functions'
|
|
24
|
+
import { clamp, toPrecision } from '@tremolo-ui/functions'
|
|
28
25
|
|
|
29
26
|
import { DEFAULT_DRAG_SENSITIVITY } from '../../input-event'
|
|
30
27
|
import { cssLength } from '../_util/css-length'
|
|
@@ -35,6 +32,8 @@ import { type PointRegistration, PointsEditorProvider } from './context'
|
|
|
35
32
|
import { AXIS, Point, type PointBaseType } from './Point'
|
|
36
33
|
import { SelectionBox } from './SelectionBox'
|
|
37
34
|
|
|
35
|
+
import type { CSSVariables } from '../../css-variables'
|
|
36
|
+
|
|
38
37
|
/** One array for every editor with selection turned off, so memos hold still. */
|
|
39
38
|
const EMPTY: readonly string[] = []
|
|
40
39
|
|
|
@@ -213,6 +212,8 @@ export interface PointsEditorProps {
|
|
|
213
212
|
* </PointsEditor.Root>
|
|
214
213
|
*/
|
|
215
214
|
children: ReactNode
|
|
215
|
+
|
|
216
|
+
style?: CSSProperties & CSSVariables<'width' | 'height'>
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
type Props = PointsEditorProps &
|
|
@@ -554,9 +555,6 @@ export {
|
|
|
554
555
|
usePointsEditorContext,
|
|
555
556
|
type PointsEditorContextValue,
|
|
556
557
|
} from './context'
|
|
557
|
-
export { type PointsEditorBackgroundProps } from './Background'
|
|
558
|
-
export { type PointsEditorContainerProps } from './Container'
|
|
559
|
-
export { type PointsEditorSelectionBoxProps } from './SelectionBox'
|
|
560
558
|
export {
|
|
561
559
|
clampPoint,
|
|
562
560
|
type PointBaseType,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef, CSSProperties
|
|
2
|
-
|
|
3
|
-
import { xor } from '@tremolo-ui/functions'
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
4
2
|
|
|
5
3
|
import { cssLength } from '../_util/css-length'
|
|
6
4
|
import { Placement } from '../_util/Placement'
|
|
@@ -9,6 +7,8 @@ import { useSliderContext } from './context'
|
|
|
9
7
|
import { MarksOption } from './MarksOption'
|
|
10
8
|
import { generateOptionsList, MarksOptions } from './type'
|
|
11
9
|
|
|
10
|
+
import type { CSSVariables } from '../../css-variables'
|
|
11
|
+
|
|
12
12
|
export interface SliderMarksProps {
|
|
13
13
|
/**
|
|
14
14
|
* Space between the marks and the track. Sets `--gap`; the theme's own
|
|
@@ -24,11 +24,8 @@ export interface SliderMarksProps {
|
|
|
24
24
|
* `'step'` makes one per step, which is a great many for a fine `step`.
|
|
25
25
|
*/
|
|
26
26
|
options?: MarksOptions
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* is set.
|
|
30
|
-
*/
|
|
31
|
-
children?: ReactNode
|
|
27
|
+
|
|
28
|
+
style?: CSSProperties & CSSVariables<'gap'>
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
export function Marks({
|
|
@@ -49,7 +46,7 @@ export function Marks({
|
|
|
49
46
|
const optionsList = options
|
|
50
47
|
? generateOptionsList(options, min, max, step)
|
|
51
48
|
: []
|
|
52
|
-
if (
|
|
49
|
+
if (vertical !== reverse) optionsList.reverse()
|
|
53
50
|
|
|
54
51
|
return (
|
|
55
52
|
<div
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties, useCallback } from 'react'
|
|
2
2
|
|
|
3
|
-
import { toFixed
|
|
3
|
+
import { toFixed } from '@tremolo-ui/functions'
|
|
4
4
|
|
|
5
5
|
import { cssLength } from '../_util/css-length'
|
|
6
6
|
import { useCheckPlacement } from '../_util/Placement'
|
|
7
7
|
|
|
8
8
|
import { useSliderContext } from './context'
|
|
9
9
|
|
|
10
|
+
import type { CSSVariables } from '../../css-variables'
|
|
11
|
+
|
|
10
12
|
export interface SliderMarksOptionProps {
|
|
11
13
|
/** Where the mark sits, on the same scale as the thumb. */
|
|
12
14
|
value: number
|
|
@@ -39,6 +41,9 @@ export interface SliderMarksOptionProps {
|
|
|
39
41
|
mark?: CSSProperties
|
|
40
42
|
label?: CSSProperties
|
|
41
43
|
}
|
|
44
|
+
|
|
45
|
+
style?: CSSProperties &
|
|
46
|
+
CSSVariables<'thickness' | 'length' | 'gap' | 'label-width' | 'translate'>
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export function MarksOption({
|
|
@@ -68,7 +73,7 @@ export function MarksOption({
|
|
|
68
73
|
(value: number) => {
|
|
69
74
|
// The marks have to sit on the same curve the thumb runs along.
|
|
70
75
|
const percent = scale.normalize(value, min, max) * 100
|
|
71
|
-
return toFixed(
|
|
76
|
+
return toFixed(vertical !== reverse ? 100 - percent : percent)
|
|
72
77
|
},
|
|
73
78
|
[vertical, reverse, max, min, scale],
|
|
74
79
|
)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AriaAttributes,
|
|
3
2
|
ComponentPropsWithoutRef,
|
|
4
|
-
CSSProperties,
|
|
5
3
|
forwardRef,
|
|
6
|
-
ReactNode,
|
|
7
4
|
useImperativeHandle,
|
|
8
5
|
useRef,
|
|
6
|
+
CSSProperties,
|
|
9
7
|
} from 'react'
|
|
10
8
|
|
|
11
9
|
import { useCheckPlacement } from '../_util/Placement'
|
|
@@ -13,6 +11,8 @@ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
|
13
11
|
|
|
14
12
|
import { useSliderContext } from './context'
|
|
15
13
|
|
|
14
|
+
import type { CSSVariables } from '../../css-variables'
|
|
15
|
+
|
|
16
16
|
export interface SliderThumbProps {
|
|
17
17
|
/**
|
|
18
18
|
* Sets `--color`, for the theme to colour the thumb with. The size is not
|
|
@@ -21,18 +21,7 @@ export interface SliderThumbProps {
|
|
|
21
21
|
*/
|
|
22
22
|
color?: string
|
|
23
23
|
|
|
24
|
-
|
|
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
|
-
*/
|
|
31
|
-
children?: ReactNode
|
|
32
|
-
'aria-label'?: AriaAttributes['aria-label']
|
|
33
|
-
'aria-labelledby'?: AriaAttributes['aria-labelledby']
|
|
34
|
-
'aria-describedby'?: AriaAttributes['aria-describedby']
|
|
35
|
-
'aria-valuetext'?: AriaAttributes['aria-valuetext']
|
|
24
|
+
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
export interface SliderThumbMethods {
|
|
@@ -50,6 +39,11 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
50
39
|
children,
|
|
51
40
|
className,
|
|
52
41
|
style,
|
|
42
|
+
// Pulled out of `props` so that they land on the range input rather than
|
|
43
|
+
// on the thumb: the input is the control, it takes the focus, and a name
|
|
44
|
+
// on a plain div would be ignored. No declaration in `SliderThumbProps`
|
|
45
|
+
// is needed for this — they are already on the div attributes it is
|
|
46
|
+
// merged with, with the same types.
|
|
53
47
|
'aria-label': ariaLabel,
|
|
54
48
|
'aria-labelledby': ariaLabelledby,
|
|
55
49
|
'aria-describedby': ariaDescribedby,
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
|
-
forwardRef,
|
|
5
|
-
ReactNode,
|
|
6
|
-
} from 'react'
|
|
7
|
-
|
|
8
|
-
import { xor } from '@tremolo-ui/functions'
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
|
|
9
2
|
|
|
10
3
|
import { useComposedRefs } from '../../compose-refs'
|
|
11
4
|
import { cssLength } from '../_util/css-length'
|
|
@@ -13,6 +6,8 @@ import { Placement } from '../_util/Placement'
|
|
|
13
6
|
|
|
14
7
|
import { useSliderContext } from './context'
|
|
15
8
|
|
|
9
|
+
import type { CSSVariables } from '../../css-variables'
|
|
10
|
+
|
|
16
11
|
export interface SliderTrackProps {
|
|
17
12
|
/**
|
|
18
13
|
* How long the track is along the axis the slider runs. Sets `--length`;
|
|
@@ -27,10 +22,8 @@ export interface SliderTrackProps {
|
|
|
27
22
|
/** Colour of the rest of the track. Sets `--inactive`. */
|
|
28
23
|
inactive?: string
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/** `<Slider.Thumb />` goes here. */
|
|
33
|
-
children?: ReactNode
|
|
25
|
+
style?: CSSProperties &
|
|
26
|
+
CSSVariables<'length' | 'thickness' | 'active' | 'inactive' | 'percent'>
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
type Props = SliderTrackProps &
|
|
@@ -65,7 +58,7 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
65
58
|
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
66
59
|
// Which end the value grows from. `percent` is already the position on
|
|
67
60
|
// screen, so this only says which side of it is the filled one.
|
|
68
|
-
data-flipped={
|
|
61
|
+
data-flipped={vertical !== reverse ? '' : undefined}
|
|
69
62
|
style={
|
|
70
63
|
{
|
|
71
64
|
'--active': active,
|
|
@@ -9,17 +9,15 @@ import React, {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react'
|
|
11
11
|
|
|
12
|
-
import type { AxisOptions, XY } from '@tremolo-ui/dom'
|
|
13
12
|
import {
|
|
14
13
|
applyDelta,
|
|
15
|
-
|
|
16
|
-
toFixed,
|
|
14
|
+
type AxisOptions,
|
|
17
15
|
type InputEventOption,
|
|
18
16
|
type ModifierValue,
|
|
19
17
|
selectModifier,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from '@tremolo-ui/functions'
|
|
18
|
+
type XY,
|
|
19
|
+
} from '@tremolo-ui/dom'
|
|
20
|
+
import { linearScale, type Scale, toFixed } from '@tremolo-ui/functions'
|
|
23
21
|
|
|
24
22
|
import { useComposedRefs } from '../../compose-refs'
|
|
25
23
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
@@ -147,8 +145,6 @@ export interface SliderProps {
|
|
|
147
145
|
* The parts carry `data-readonly` while it is set.
|
|
148
146
|
*/
|
|
149
147
|
readonly?: boolean
|
|
150
|
-
className?: string
|
|
151
|
-
style?: CSSProperties
|
|
152
148
|
/** Called with the new value when a drag, the wheel or an arrow key moves it. */
|
|
153
149
|
onChange?: (value: number) => void
|
|
154
150
|
/** Called when a drag starts, with the value where the track was pressed. */
|
|
@@ -227,10 +223,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
227
223
|
// vertical -> rev (up)
|
|
228
224
|
// reverse -> rev (left)
|
|
229
225
|
// vertical & reverse -> normal (down)
|
|
230
|
-
const displayReversed =
|
|
231
|
-
() => xor(vertical, reverse),
|
|
232
|
-
[vertical, reverse],
|
|
233
|
-
)
|
|
226
|
+
const displayReversed = vertical !== reverse
|
|
234
227
|
const percent = displayReversed ? rev : p
|
|
235
228
|
|
|
236
229
|
// --- internal functions ---
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toFixed } from '@tremolo-ui/functions'
|
|
2
|
+
|
|
3
|
+
import { decimalDigits } from '../_util/decimal-digits'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* How `Slider.Marks` fills itself in when it is given no children: one option
|
|
@@ -36,7 +38,7 @@ export function generateOptionsList(
|
|
|
36
38
|
for (let i = 0; i < count; i++) {
|
|
37
39
|
const value = toFixed(
|
|
38
40
|
interval * (Math.ceil(min / interval) + i),
|
|
39
|
-
|
|
41
|
+
decimalDigits(interval),
|
|
40
42
|
)
|
|
41
43
|
optionsList.push({ value: value, mark: mark, label: label })
|
|
42
44
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
|
-
forwardRef,
|
|
5
|
-
ReactNode,
|
|
6
|
-
} from 'react'
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
|
|
7
2
|
|
|
8
3
|
import { useComposedRefs } from '../../compose-refs'
|
|
9
4
|
import { cssLength } from '../_util/css-length'
|
|
@@ -11,6 +6,8 @@ import { Placement } from '../_util/Placement'
|
|
|
11
6
|
|
|
12
7
|
import { useXYPadContext } from './context'
|
|
13
8
|
|
|
9
|
+
import type { CSSVariables } from '../../css-variables'
|
|
10
|
+
|
|
14
11
|
export interface XYPadAreaProps {
|
|
15
12
|
/** Sets `--width`; the size the theme gives it stands when omitted. */
|
|
16
13
|
width?: number | string
|
|
@@ -18,10 +15,8 @@ export interface XYPadAreaProps {
|
|
|
18
15
|
height?: number | string
|
|
19
16
|
/** Sets `--color`, for the theme to colour the area with. */
|
|
20
17
|
color?: string
|
|
21
|
-
|
|
22
|
-
style?: CSSProperties
|
|
23
|
-
/** `<XYPad.Thumb />` goes here. */
|
|
24
|
-
children?: ReactNode
|
|
18
|
+
|
|
19
|
+
style?: CSSProperties & CSSVariables<'width' | 'height' | 'color'>
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
type Props = XYPadAreaProps &
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AriaAttributes,
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
|
-
CSSProperties,
|
|
5
4
|
forwardRef,
|
|
6
|
-
ReactNode,
|
|
7
5
|
useImperativeHandle,
|
|
8
6
|
useRef,
|
|
7
|
+
CSSProperties,
|
|
9
8
|
} from 'react'
|
|
10
9
|
|
|
11
10
|
import { useCheckPlacement } from '../_util/Placement'
|
|
@@ -13,6 +12,8 @@ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
|
13
12
|
|
|
14
13
|
import { toXY, useXYPadContext, type XYInput } from './context'
|
|
15
14
|
|
|
15
|
+
import type { CSSVariables } from '../../css-variables'
|
|
16
|
+
|
|
16
17
|
export interface XYPadThumbProps {
|
|
17
18
|
/**
|
|
18
19
|
* Sets `--color`, for the theme to colour the thumb with. The size is not
|
|
@@ -21,15 +22,6 @@ export interface XYPadThumbProps {
|
|
|
21
22
|
*/
|
|
22
23
|
color?: string
|
|
23
24
|
|
|
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
|
-
*/
|
|
31
|
-
children?: ReactNode
|
|
32
|
-
|
|
33
25
|
/**
|
|
34
26
|
* The accessible name of each axis, as `[x, y]`. There are two range inputs
|
|
35
27
|
* inside the thumb, so each takes a name of its own; a single string names
|
|
@@ -45,6 +37,8 @@ export interface XYPadThumbProps {
|
|
|
45
37
|
* `[x, y]` or one for both.
|
|
46
38
|
*/
|
|
47
39
|
'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
|
|
40
|
+
|
|
41
|
+
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
export interface XYPadThumbMethods {
|
|
@@ -10,17 +10,15 @@ import {
|
|
|
10
10
|
useRef,
|
|
11
11
|
} from 'react'
|
|
12
12
|
|
|
13
|
-
import type { AxisOptions } from '@tremolo-ui/dom'
|
|
14
13
|
import {
|
|
15
14
|
applyDelta,
|
|
16
|
-
|
|
17
|
-
toFixed,
|
|
15
|
+
type AxisOptions,
|
|
18
16
|
InputEventOption,
|
|
19
17
|
ModifierState,
|
|
20
18
|
type ModifierValue,
|
|
21
19
|
selectModifier,
|
|
22
|
-
|
|
23
|
-
} from '@tremolo-ui/functions'
|
|
20
|
+
} from '@tremolo-ui/dom'
|
|
21
|
+
import { linearScale, type Scale, toFixed } from '@tremolo-ui/functions'
|
|
24
22
|
|
|
25
23
|
import { useComposedRefs } from '../../compose-refs'
|
|
26
24
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef,
|
|
1
|
+
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
position: 'absolute',
|
|
5
|
-
width: 1,
|
|
6
|
-
height: 1,
|
|
7
|
-
padding: 0,
|
|
8
|
-
margin: -1,
|
|
9
|
-
overflow: 'hidden',
|
|
10
|
-
clip: 'rect(0, 0, 0, 0)',
|
|
11
|
-
clipPath: 'inset(50%)',
|
|
12
|
-
whiteSpace: 'nowrap',
|
|
13
|
-
border: 0,
|
|
14
|
-
pointerEvents: 'none',
|
|
15
|
-
}
|
|
3
|
+
import { visuallyHiddenStyle } from './visually-hidden'
|
|
16
4
|
|
|
17
5
|
type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'style'>
|
|
18
6
|
|
|
@@ -20,5 +8,5 @@ export const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef<
|
|
|
20
8
|
HTMLInputElement,
|
|
21
9
|
Props
|
|
22
10
|
>((props, ref) => (
|
|
23
|
-
<input {...props} ref={ref} type="range" style={
|
|
11
|
+
<input {...props} ref={ref} type="range" style={visuallyHiddenStyle} />
|
|
24
12
|
))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many digits a number carries after the decimal point.
|
|
3
|
+
*
|
|
4
|
+
* `Slider.Marks` builds its labels by multiplying an interval, which leaves
|
|
5
|
+
* binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
|
|
6
|
+
* count is what tells `toFixed` how far to round that back.
|
|
7
|
+
*
|
|
8
|
+
* The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
|
|
9
|
+
* which has no decimal point at all, so reading the text after the point
|
|
10
|
+
* reports no digits and the interval rounds away to whole numbers.
|
|
11
|
+
*/
|
|
12
|
+
export function decimalDigits(x: number): number {
|
|
13
|
+
if (!Number.isFinite(x)) return 0
|
|
14
|
+
|
|
15
|
+
const text = String(x)
|
|
16
|
+
const e = text.indexOf('e')
|
|
17
|
+
if (e === -1) return text.split('.')[1]?.length ?? 0
|
|
18
|
+
|
|
19
|
+
const fraction = text.slice(0, e).split('.')[1]?.length ?? 0
|
|
20
|
+
// A negative exponent pushes the point further right, a positive one pulls
|
|
21
|
+
// it back past the digits that are there.
|
|
22
|
+
return Math.max(0, fraction - Number(text.slice(e + 1)))
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Take an element out of sight while leaving it in the accessibility tree and
|
|
5
|
+
* in the tab order.
|
|
6
|
+
*
|
|
7
|
+
* `display: none` and `visibility: hidden` would remove it from both, and the
|
|
8
|
+
* native control underneath a headless component is what carries the ARIA and
|
|
9
|
+
* the keyboard behaviour. `pointer-events: none` is safe because nothing is
|
|
10
|
+
* ever clicked here directly: a `<label>` forwards its click, and a drag is
|
|
11
|
+
* handled by the part that is visible.
|
|
12
|
+
*/
|
|
13
|
+
export const visuallyHiddenStyle: CSSProperties = {
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
width: 1,
|
|
16
|
+
height: 1,
|
|
17
|
+
padding: 0,
|
|
18
|
+
margin: -1,
|
|
19
|
+
overflow: 'hidden',
|
|
20
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
21
|
+
clipPath: 'inset(50%)',
|
|
22
|
+
whiteSpace: 'nowrap',
|
|
23
|
+
border: 0,
|
|
24
|
+
pointerEvents: 'none',
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS custom properties, for a `style` object.
|
|
3
|
+
*
|
|
4
|
+
* React's `CSSProperties` has no key starting with `--`, so
|
|
5
|
+
* `style={{ '--color': 'red' }}` is refused as an unknown property even though
|
|
6
|
+
* the browser takes it. Every part accepts this alongside `CSSProperties`,
|
|
7
|
+
* which is how a variable the component or a theme reads is set inline.
|
|
8
|
+
*
|
|
9
|
+
* `T` names the custom properties the part itself writes or reads, without the
|
|
10
|
+
* leading `--`, so that an editor can suggest them. Any other `--` name is
|
|
11
|
+
* still accepted — a theme reads variables of its own, such as the colours of
|
|
12
|
+
* each kind of key.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* CSSVariables<'thickness' | 'length'>
|
|
16
|
+
* // { '--thickness'?: string | number, '--length'?: string | number,
|
|
17
|
+
* // [name: `--${string}`]: string | number | undefined }
|
|
18
|
+
*/
|
|
19
|
+
export type CSSVariables<T extends string = never> = {
|
|
20
|
+
[K in T as `--${K}`]?: string | number
|
|
21
|
+
} & {
|
|
22
|
+
[K in `--${string}`]?: string | number
|
|
23
|
+
}
|
|
@@ -2,12 +2,11 @@ import { useEffect } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
applyDelta,
|
|
5
|
-
linearScale,
|
|
6
5
|
type InputEventOption,
|
|
7
6
|
type ModifierState,
|
|
8
7
|
type ModifierValue,
|
|
9
|
-
|
|
10
|
-
} from '@tremolo-ui/functions'
|
|
8
|
+
} from '@tremolo-ui/dom'
|
|
9
|
+
import { linearScale, type ValueRange } from '@tremolo-ui/functions'
|
|
11
10
|
|
|
12
11
|
/** Positions probed across the travel. The ends are left out so that the
|
|
13
12
|
* clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
|
package/src/hooks/useDrag.ts
CHANGED
|
@@ -4,10 +4,14 @@ import { createDrag, type DragState } from '@tremolo-ui/dom'
|
|
|
4
4
|
|
|
5
5
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
export interface UseDragOptions {
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Pixels the pointer has to travel before the drag counts as one.
|
|
10
|
+
*
|
|
11
|
+
* A click moves the pointer by a pixel or two, so without a threshold a
|
|
12
|
+
* double click reports a drag between the two presses.
|
|
13
|
+
*
|
|
14
|
+
* @default 1
|
|
11
15
|
*/
|
|
12
16
|
threshold?: number
|
|
13
17
|
|
|
@@ -24,6 +28,9 @@ interface UseDragProps {
|
|
|
24
28
|
pointerLock?: boolean
|
|
25
29
|
|
|
26
30
|
/**
|
|
31
|
+
* Called on every move once the drag has started, with the pointer position
|
|
32
|
+
* relative to the element and how far it moved since the last call.
|
|
33
|
+
*
|
|
27
34
|
* @param state the whole drag, for anything the four numbers leave out —
|
|
28
35
|
* the pointer event and its modifier keys, most of all.
|
|
29
36
|
*/
|
|
@@ -34,7 +41,9 @@ interface UseDragProps {
|
|
|
34
41
|
deltaY: number,
|
|
35
42
|
state: DragState,
|
|
36
43
|
) => void
|
|
44
|
+
/** Called once the pointer has moved past `threshold`, not on pointerdown. */
|
|
37
45
|
onDragStart?: (state: DragState) => void
|
|
46
|
+
/** Called when the pointer is released, only if the drag ever started. */
|
|
38
47
|
onDragEnd?: (state: DragState) => void
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -50,7 +59,7 @@ export function useDrag<T extends Element>({
|
|
|
50
59
|
onDrag,
|
|
51
60
|
onDragStart,
|
|
52
61
|
onDragEnd,
|
|
53
|
-
}:
|
|
62
|
+
}: UseDragOptions): (node: T | null) => void {
|
|
54
63
|
const dragHandler = useCallbackRef(onDrag)
|
|
55
64
|
const dragStartHandler = useCallbackRef(onDragStart)
|
|
56
65
|
const dragEndHandler = useCallbackRef(onDragEnd)
|