UNPKG

@activecollab/components

Version:

ActiveCollab Components

178 lines (175 loc) 8.68 kB
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