UNPKG

@activecollab/components

Version:

ActiveCollab Components

137 lines (133 loc) 5.43 kB
import { useCallback, useRef } from "react"; import { applyResize, isResizeKey, keyboardResize, makeStart, measurePercent } from "./resizePolicy"; /** * The one place pointer and keyboard input meet the resize policy. * * `StackedCard`'s built-in grip reports raw deltas and knows no policy — enough * for a host that just tracks a width. This hook is the full contract: it * captures the gesture start, funnels both inputs through the pure policy in * `resizePolicy.ts`, streams preview events and fires exactly ONE commit per * gesture. It returns the props to spread on the grip, including the live * separator ARIA. * * Preview versus commit (spec §7e): the continuous stream is live feedback * only. A host creates an undo entry on the COMMIT, never on a preview — and a * gesture that never changed the size commits nothing at all, so a bare click * on the grip is not a change. * * The hook never touches the card's content: the intrinsic minimum is read * through `contentMin`, which may be a getter so a card that measures its own * footer can answer at the moment it is asked. */ /** The props to spread on the grip — `StackedCard`'s `resizeHandleProps`. */ const sameSize = (a, b) => a.w === b.w && a.h === b.h; export const useStackedCardResize = _ref => { let size = _ref.size, policy = _ref.policy, cardId = _ref.cardId, contentMin = _ref.contentMin, _ref$axis = _ref.axis, axis = _ref$axis === void 0 ? "both" : _ref$axis, _ref$scale = _ref.scale, scale = _ref$scale === void 0 ? 1 : _ref$scale, _ref$label = _ref.label, label = _ref$label === void 0 ? "Resize card" : _ref$label, onResize = _ref.onResize, onResizeCommit = _ref.onResizeCommit; // The pointer gesture's anchor: the size + content-min captured on pointer // down, plus the pointer origin the delta is measured from. const pointerStart = useRef(null); // Mirrors the latest emitted size so pointer-up / key-up can commit exactly // what preview last showed, without a stale closure. const latest = useRef(size); latest.current = size; // Whether this gesture actually moved the card. A bare click, or a Home on an // axis with no floor, leaves it false and commits nothing. const dirty = useRef(false); // Read through refs so the handlers below can stay stable across renders // while still seeing the current policy, axis and zoom. const policyRef = useRef(policy); policyRef.current = policy; const axisRef = useRef(axis); axisRef.current = axis; const scaleRef = useRef(scale); scaleRef.current = scale; const contentMinRef = useRef(contentMin); contentMinRef.current = contentMin; const readContentMin = useCallback(() => { const source = contentMinRef.current; return typeof source === "function" ? source() : source; }, []); const emitPreview = useCallback((next, source) => { // Nothing changed — not a preview, and not something to commit later. if (sameSize(next, latest.current)) return; latest.current = next; dirty.current = true; onResize(next, source); }, [onResize]); const handlePointerDown = useCallback(e => { pointerStart.current = { start: makeStart(latest.current, readContentMin()), x: e.clientX, y: e.clientY }; dirty.current = false; e.target.setPointerCapture == null || e.target.setPointerCapture(e.pointerId); e.preventDefault(); }, [readContentMin]); const handlePointerMove = useCallback(e => { const ps = pointerStart.current; if (!ps) return; const z = scaleRef.current || 1; const next = applyResize(ps.start, { dx: (e.clientX - ps.x) / z, dy: (e.clientY - ps.y) / z }, policyRef.current, // Shift held during a free-form drag = temporary proportional lock. { constrain: e.shiftKey, axis: axisRef.current }); emitPreview(next, "pointer"); }, [emitPreview]); const endPointer = useCallback(() => { if (!pointerStart.current) return; pointerStart.current = null; if (dirty.current) onResizeCommit(latest.current, "pointer"); dirty.current = false; }, [onResizeCommit]); const handleKeyDown = useCallback(e => { const next = keyboardResize(e.key, latest.current, policyRef.current, readContentMin(), { shiftKey: e.shiftKey, axis: axisRef.current }); if (!next) return; // The key is ours even when it resolves to no movement (already pinned at // a bound), so the browser must not also scroll the surface. e.preventDefault(); emitPreview(next, "keyboard"); }, [emitPreview, readContentMin]); const handleKeyUp = useCallback(e => { if (!isResizeKey(e.key)) return; // The gesture end for the keyboard is the key release: one commit per // hold, mirroring pointer-up after a stream of moves. if (dirty.current) onResizeCommit(latest.current, "keyboard"); dirty.current = false; }, [onResizeCommit]); return { role: "separator", tabIndex: 0, "aria-label": label, "aria-controls": cardId, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": measurePercent(size, policy, readContentMin()), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: endPointer, onLostPointerCapture: endPointer, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }; }; //# sourceMappingURL=useStackedCardResize.js.map