Spaces:
Sleeping
Sleeping
| /** Pressable with pointer-down scale feedback. */ | |
| import { animate } from "motion"; | |
| import type { ComponentChildren, JSX } from "preact"; | |
| import { useRef } from "preact/hooks"; | |
| import { prefersReducedMotion, springs } from "../motion/springs"; | |
| type PressableProps = { | |
| children: ComponentChildren; | |
| className?: string; | |
| disabled?: boolean; | |
| onClick?: JSX.MouseEventHandler<HTMLButtonElement>; | |
| type?: "button" | "submit"; | |
| ariaLabel?: string; | |
| ariaPressed?: boolean; | |
| }; | |
| export function Pressable({ | |
| children, | |
| className = "", | |
| disabled = false, | |
| onClick, | |
| type = "button", | |
| ariaLabel, | |
| ariaPressed, | |
| }: PressableProps) { | |
| const ref = useRef<HTMLButtonElement>(null); | |
| const active = useRef(false); | |
| const setScale = (value: number) => { | |
| const el = ref.current; | |
| if (!el || disabled) return; | |
| if (prefersReducedMotion()) { | |
| el.style.transform = "none"; | |
| el.style.opacity = value < 1 ? "0.85" : "1"; | |
| return; | |
| } | |
| animate(el, { scale: value }, springs.press); | |
| }; | |
| const onPointerDown = (event: PointerEvent) => { | |
| if (disabled || event.button !== 0) return; | |
| active.current = true; | |
| ref.current?.setPointerCapture(event.pointerId); | |
| setScale(0.98); | |
| }; | |
| const endPress = () => { | |
| if (!active.current) return; | |
| active.current = false; | |
| setScale(1); | |
| }; | |
| return ( | |
| <button | |
| ref={ref} | |
| type={type} | |
| disabled={disabled} | |
| className={`pressable ${className}`.trim()} | |
| aria-label={ariaLabel} | |
| aria-pressed={ariaPressed} | |
| onPointerDown={onPointerDown} | |
| onPointerUp={endPress} | |
| onPointerCancel={endPress} | |
| onPointerLeave={endPress} | |
| onClick={onClick} | |
| > | |
| {children} | |
| </button> | |
| ); | |
| } | |