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 {
spinButtonProps: HTMLAttributes
incrementButtonProps: PressProps
decrementButtonProps: PressProps
}
export function useSpinButton(
props: UseSpinButtonProps,
): UseSpinButtonResult {
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()
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
return
}
const handlers: Record 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 = {
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,
},
}
}