@activecollab/components
Version:
ActiveCollab Components
137 lines (133 loc) • 5.43 kB
JavaScript
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