forked from use-platform/use-platform
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseSpinButton.ts
More file actions
134 lines (117 loc) · 3.47 KB
/
useSpinButton.ts
File metadata and controls
134 lines (117 loc) · 3.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { HTMLAttributes, KeyboardEvent, useCallback, useEffect, useRef } from 'react'
import { PressProps } from '../../interactions/press'
const STEP_DELAY = 400
const STEP_TIMEOUT = 70
export interface UseSpinButtonProps {
min?: number
max?: number
value?: number
textValue?: string
disabled?: boolean
readOnly?: boolean
required?: boolean
onIncrement?: () => void
onExtraIncrement?: () => void
onDecrement?: () => void
onExtraDecrement?: () => void
onIncrementToMax?: () => void
onDecrementToMin?: () => void
}
export interface UseSpinButtonResult<T extends HTMLElement> {
spinButtonProps: HTMLAttributes<T>
incrementButtonProps: PressProps<HTMLElement>
decrementButtonProps: PressProps<HTMLElement>
}
export function useSpinButton<T extends HTMLElement = HTMLElement>(
props: UseSpinButtonProps,
): UseSpinButtonResult<T> {
const {
value = NaN,
min = NaN,
max = NaN,
textValue = Number.isFinite(value) ? value.toString() : '',
disabled,
readOnly,
required,
onIncrement,
onExtraIncrement,
onDecrement,
onExtraDecrement,
onDecrementToMin,
onIncrementToMax,
} = props
const isInteractive = !(readOnly || disabled)
const timerRef = useRef<number>()
const onKeyDown = useCallback(
(event: KeyboardEvent<T>) => {
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
return
}
const handlers: Record<string, (() => void) | undefined> = {
ArrowUp: onIncrement,
// fallback to increment
PageUp: onExtraIncrement ?? onIncrement,
ArrowDown: onDecrement,
// fallback to decrement
PageDown: onExtraDecrement ?? onDecrement,
End: onIncrementToMax,
Home: onDecrementToMin,
}
const handler = handlers[event.key]
if (handler) {
event.preventDefault()
handler()
}
},
[
onIncrement,
onExtraIncrement,
onIncrementToMax,
onDecrement,
onExtraDecrement,
onDecrementToMin,
],
)
const resetTimer = useCallback(() => clearTimeout(timerRef.current), [])
const pressStartHandler = useCallback((callback?: () => void) => {
function repeat(delay: number) {
callback?.()
timerRef.current = window.setTimeout(repeat, delay, STEP_TIMEOUT)
}
repeat(STEP_DELAY)
}, [])
const onIncrementPressStart = useCallback(() => {
pressStartHandler(onIncrement)
}, [pressStartHandler, onIncrement])
const onDecrementPressStart = useCallback(() => {
pressStartHandler(onDecrement)
}, [pressStartHandler, onDecrement])
useEffect(() => resetTimer, [resetTimer])
const spinButtonProps: HTMLAttributes<T> = {
role: 'spinbutton',
'aria-valuenow': Number.isFinite(value) ? value : undefined,
// TODO: localize message
'aria-valuetext': textValue === '' ? 'Empty' : textValue,
'aria-valuemin': Number.isFinite(min) ? min : undefined,
'aria-valuemax': Number.isFinite(max) ? max : undefined,
'aria-disabled': disabled || undefined,
'aria-readonly': readOnly || undefined,
'aria-required': required || undefined,
}
if (isInteractive) {
spinButtonProps.onKeyDown = onKeyDown
}
return {
spinButtonProps,
incrementButtonProps: {
disabled: !isInteractive,
onPressStart: onIncrementPressStart,
onPressEnd: resetTimer,
},
decrementButtonProps: {
disabled: !isInteractive,
onPressStart: onDecrementPressStart,
onPressEnd: resetTimer,
},
}
}