@activecollab/components
Version:
ActiveCollab Components
178 lines (175 loc) • 8.68 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
const _excluded = ["selected", "disabled", "pending", "isDragSource", "isDragPreview", "entered", "forceHover", "resizable", "resizeAxis", "resizeLabel", "priorityColor", "onResizeStart", "onResizeDelta", "onResizeCommit", "resizeHandleProps", "className", "children"];
import React, { useCallback, useRef } from "react";
import classnames from "classnames";
import { STACKED_CARD_CONTROL_CLASS } from "./constants";
import { StackedCardResizeHandleGlyph } from "./Glyphs";
import { DEFAULT_KEYBOARD_STEP, SHIFT_STEP_MULTIPLIER, isResizeKey } from "./resizePolicy";
import { StyledStackedCard, StyledStackedCardResizeHandle } from "./Styles";
/**
* StackedCard — the hollow shell for cards on drag-and-drop surfaces.
*
* This is NOT the base for cards in lists. It is the card for surfaces where
* dragging is a primary interaction: column boards and canvases. Content is
* laid out top to bottom in `StackedCardRow`s, which makes every card a
* portrait presentation of its data.
*
* ── The rule the whole subsystem hangs on ──────────────────────────────────
* THE SHELL STAYS HOLLOW. There is no `contentType` enum here, no per-type
* prop soup, no `if task …`. The shell owns only VISUAL STATES (selected,
* disabled, pending, priority, drag source/preview, entered) and one intent
* callback (`onResizeDelta`). Content is passed as `children` and composes the
* layout primitives. Typed cards — an ImageCard, a LinkCard, a TaskCard — are
* facades built on top of it in the consuming app, and adding one requires
* ZERO changes in this file.
*
* ── What the shell does NOT do ────────────────────────────────────────────
* It implements no drag-and-drop, no selection bookkeeping, no resize policy
* and no keyboard navigation. The card reports; the host decides. Drag
* choreography (the drag layer, the landing indicator, the drop) belongs to
* the host surface, which then feeds the result back as `isDragSource` /
* `isDragPreview`. The shell forwards its ref and spreads DOM props, so a host
* can attach `tabIndex`, ARIA, pointer and keyboard handlers to it.
*
* ── Resize ────────────────────────────────────────────────────────────────
* The grip is opt-in (`resizable`) and reports intent as a gesture: one
* `onResizeStart`, a stream of `onResizeDelta` previews measured FROM THE
* GESTURE START, and one `onResizeCommit` at the end — and only if the gesture
* moved anything. A host applies previews for live feedback and records an undo
* entry on the commit alone (spec §7e).
*
* Because the deltas are gesture-relative rather than incremental, a host can
* resolve every event against the size it snapshotted on start, and never has
* to keep a running accumulator. That is what makes a slow, few-pixels-at-a-time
* drag keep growing instead of stalling under a snap step.
*
* The grip itself is a focusable `role="separator"` and handles arrow keys
* (Shift for a coarse nudge). For the full policy — bounds, proportional mode,
* step snapping, content minimums, Home/End and the live `aria-valuenow` — use
* `useStackedCardResize` and spread its result through `resizeHandleProps`;
* those handlers then own the grip and the three callbacks above stay quiet.
*
* ── Theming ───────────────────────────────────────────────────────────────
* Colours resolve through `--sc-*` custom properties with built-in fallbacks:
* `--sc-selection-ring` (selection / focus / entered) and the cover scrim
* family `--sc-scrim`, `--sc-scrim-strong`, `--sc-scrim-fg`. Declare any of
* them on an ancestor to retint a whole surface.
*/
export const StackedCard = /*#__PURE__*/React.forwardRef((_ref, ref) => {
let selected = _ref.selected,
disabled = _ref.disabled,
pending = _ref.pending,
isDragSource = _ref.isDragSource,
isDragPreview = _ref.isDragPreview,
entered = _ref.entered,
forceHover = _ref.forceHover,
resizable = _ref.resizable,
_ref$resizeAxis = _ref.resizeAxis,
resizeAxis = _ref$resizeAxis === void 0 ? "both" : _ref$resizeAxis,
_ref$resizeLabel = _ref.resizeLabel,
resizeLabel = _ref$resizeLabel === void 0 ? "Resize card" : _ref$resizeLabel,
priorityColor = _ref.priorityColor,
onResizeStart = _ref.onResizeStart,
onResizeDelta = _ref.onResizeDelta,
onResizeCommit = _ref.onResizeCommit,
resizeHandleProps = _ref.resizeHandleProps,
className = _ref.className,
children = _ref.children,
rest = _objectWithoutPropertiesLoose(_ref, _excluded);
/**
* One live gesture. `x`/`y` anchor a pointer drag; `dx`/`dy` carry the
* running total a keyboard gesture has nudged. `moved` gates the commit, so
* a click that never travelled is not a change.
*/
const gesture = useRef(null);
const emitDelta = useCallback((dx, dy) => {
const g = gesture.current;
if (!g) return;
if (dx !== 0 || dy !== 0) g.moved = true;
onResizeDelta == null || onResizeDelta(dx, dy, g.source);
}, [onResizeDelta]);
const endGesture = useCallback(() => {
const g = gesture.current;
if (!g) return;
gesture.current = null;
if (g.moved) onResizeCommit == null || onResizeCommit(g.source);
}, [onResizeCommit]);
const handlePointerDown = useCallback(e => {
gesture.current = {
source: "pointer",
x: e.clientX,
y: e.clientY,
dx: 0,
dy: 0,
moved: false
};
e.target.setPointerCapture == null || e.target.setPointerCapture(e.pointerId);
e.preventDefault();
onResizeStart == null || onResizeStart("pointer");
}, [onResizeStart]);
const handlePointerMove = useCallback(e => {
const g = gesture.current;
if (!g || g.source !== "pointer") return;
// Measured from the gesture start, so the host resolves every event
// against one snapshot instead of accumulating.
emitDelta(e.clientX - g.x, e.clientY - g.y);
}, [emitDelta]);
/**
* Arrows nudge the running total; Shift makes it coarse. Home and End need
* bounds to resolve, which the bare shell does not have — they fall through
* to `useStackedCardResize`, or to the browser.
*/
const handleKeyDown = useCallback(e => {
const step = DEFAULT_KEYBOARD_STEP * (e.shiftKey ? SHIFT_STEP_MULTIPLIER : 1);
let dx = 0;
let dy = 0;
if (e.key === "ArrowRight") dx = step;else if (e.key === "ArrowLeft") dx = -step;else if (e.key === "ArrowDown") dy = step;else if (e.key === "ArrowUp") dy = -step;else return;
e.preventDefault();
if (!gesture.current || gesture.current.source !== "keyboard") {
gesture.current = {
source: "keyboard",
x: 0,
y: 0,
dx: 0,
dy: 0,
moved: false
};
onResizeStart == null || onResizeStart("keyboard");
}
const g = gesture.current;
g.dx += dx;
g.dy += dy;
emitDelta(g.dx, g.dy);
}, [emitDelta, onResizeStart]);
const handleKeyUp = useCallback(e => {
// One commit per hold, mirroring pointer-up after a stream of moves.
if (isResizeKey(e.key)) endGesture();
}, [endGesture]);
return /*#__PURE__*/React.createElement(StyledStackedCard, _extends({
ref: ref,
$selected: selected,
$disabled: disabled,
$pending: pending,
$isDragSource: isDragSource,
$isDragPreview: isDragPreview,
$entered: entered,
$forceHover: forceHover,
$priorityColor: priorityColor,
className: classnames("c-stacked-card", className)
}, rest), children, resizable ? /*#__PURE__*/React.createElement(StyledStackedCardResizeHandle, _extends({
$axis: resizeAxis,
className: classnames("c-stacked-card__resize-handle", STACKED_CARD_CONTROL_CLASS),
role: "separator",
tabIndex: 0,
"aria-label": resizeLabel,
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: endGesture,
onLostPointerCapture: endGesture,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp
}, resizeHandleProps), /*#__PURE__*/React.createElement(StackedCardResizeHandleGlyph, null)) : null);
});
StackedCard.displayName = "StackedCard";
//# sourceMappingURL=StackedCard.js.map