use-press
FC-0009pressprimitivev1.0.0Apple-gedragslaag voor tapbare elementen: touch-down zonder vertraging, hit-marge, wegslepen-annuleert/terugslepen-herstelt; activatie via onPress.
houd mij ingedrukt
bevestigd: 0× — sleep eens weg (annuleert) en terug (herstelt), of laat nét naast het vlak los: de hit-marge vangt het op.
primitives/ui/press.ts
/**
* usePress — de Apple-gedragslaag onder elk tapbaar element (WWDC18):
*
* - Licht op bij touch-down/mousedown, ZONDER vertraging. De bevestiging
* volgt pas bij touch-up.
* - Extra hit-marge: iets naast het element loslaten telt nog als tap.
* - Wegslepen annuleert (pressed uit), terugslepen binnen de marge herstelt.
*
* Activatie loopt via `onPress`, niet via onClick: dankzij pointer-capture
* blijft de pers gevolgd buiten het element, en beslist de marge — niet de
* browser — of de tap doorgaat. Toetsenbord activeert via de native click
* (e.detail === 0), zodat Enter/Spatie exact het platformgedrag houden.
*/
"use client";
import { useCallback, useRef, useState } from "react";
import type * as React from "react";
export interface UsePressOptions {
onPress?: () => void;
disabled?: boolean;
/** Marge (px) rond het element waarbinnen de pers geldig blijft. */
hitMargin?: number;
}
export interface PressHandlers {
onPointerDown: (e: React.PointerEvent<HTMLElement>) => void;
onPointerMove: (e: React.PointerEvent<HTMLElement>) => void;
onPointerUp: (e: React.PointerEvent<HTMLElement>) => void;
onPointerCancel: (e: React.PointerEvent<HTMLElement>) => void;
onClick: (e: React.MouseEvent<HTMLElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
onKeyUp: (e: React.KeyboardEvent<HTMLElement>) => void;
}
export interface UsePressResult {
/** Visuele pers-staat — bind aan `data-pressed`. */
pressed: boolean;
pressProps: PressHandlers;
}
const DEFAULT_HIT_MARGIN = 24;
interface ActivePress {
pointerId: number;
rect: DOMRect;
}
export function usePress(options: UsePressOptions = {}): UsePressResult {
const { onPress, disabled = false, hitMargin = DEFAULT_HIT_MARGIN } = options;
const [pressed, setPressed] = useState(false);
const active = useRef<ActivePress | null>(null);
const within = useCallback(
(e: { clientX: number; clientY: number }): boolean => {
const rect = active.current?.rect;
if (!rect) return false;
return (
e.clientX >= rect.left - hitMargin &&
e.clientX <= rect.right + hitMargin &&
e.clientY >= rect.top - hitMargin &&
e.clientY <= rect.bottom + hitMargin
);
},
[hitMargin],
);
const onPointerDown = useCallback(
(e: React.PointerEvent<HTMLElement>) => {
if (disabled || !e.isPrimary) return;
e.currentTarget.setPointerCapture(e.pointerId);
active.current = { pointerId: e.pointerId, rect: e.currentTarget.getBoundingClientRect() };
setPressed(true);
},
[disabled],
);
const onPointerMove = useCallback(
(e: React.PointerEvent<HTMLElement>) => {
if (active.current?.pointerId !== e.pointerId) return;
setPressed(within(e)); /* wegslepen annuleert, terugslepen herstelt */
},
[within],
);
const finish = useCallback(
(e: React.PointerEvent<HTMLElement>, activate: boolean) => {
if (active.current?.pointerId !== e.pointerId) return;
const hit = activate && within(e);
active.current = null;
setPressed(false);
if (hit && !disabled) onPress?.();
},
[disabled, onPress, within],
);
const onPointerUp = useCallback(
(e: React.PointerEvent<HTMLElement>) => finish(e, true),
[finish],
);
const onPointerCancel = useCallback(
(e: React.PointerEvent<HTMLElement>) => finish(e, false),
[finish],
);
/* Toetsenbord: native click met detail 0 (Enter/Spatie op een button). */
const onClick = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
if (disabled) return;
if (e.detail === 0) onPress?.();
},
[disabled, onPress],
);
const onKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLElement>) => {
if (disabled) return;
if (e.key === " " || e.key === "Enter") setPressed(true);
},
[disabled],
);
const onKeyUp = useCallback((_e: React.KeyboardEvent<HTMLElement>) => {
setPressed(false);
}, []);
return {
pressed,
pressProps: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onClick, onKeyDown, onKeyUp },
};
}