UNPKG

@activecollab/components

Version:

ActiveCollab Components

208 lines (202 loc) 8.81 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useInlineEdit = void 0; var _react = require("react"); function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /** * useInlineEdit — story-local click-to-edit state machine for StackedCard * (task 4/8). Presentation-only; not exported from the design-system barrel. * * A card in preview/placeholder mode enters an EDITING state on click: a * card-centered text field with confirm/cancel keyboard rules, an optional * validator, and a single-line ↔ multi-line mode switch. The hook owns only the * editing lifecycle (mode, working value, error, keyboard + outside-click); the * card chrome and the async handoff after commit (e.g. the link unfurl ladder * from task 3/8) live in the story that consumes it. * * ── The decided rules (AC task #454744) ── * Single-line Enter confirms · click-outside confirms · Esc cancels + reverts * Multi-line Enter = newline · Cmd/Ctrl+Enter saves · click-outside saves · * Esc cancels + reverts * Validator validate(value) => true | errorMessage, run before every accept * pass → commit + exit editing * fail via a key → STAY editing, surface the message (retry) * fail via outside → REVERT (acts as cancel) — user never trapped * * Keyboard is handled on the field's own keydown (never a global listener) and * every keydown stops propagation, so the card focus model (task 2/8) can't * swallow the Enter/Esc that belong to the editor. */ /** Props to spread onto the `<input>` or `<textarea>` while editing. */ var useInlineEdit = exports.useInlineEdit = function useInlineEdit() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$initialValue = _ref.initialValue, initialValue = _ref$initialValue === void 0 ? "" : _ref$initialValue, _ref$multiline = _ref.multiline, multiline = _ref$multiline === void 0 ? false : _ref$multiline, validate = _ref.validate, onCommit = _ref.onCommit; var _useState = (0, _react.useState)(false), _useState2 = _slicedToArray(_useState, 2), editing = _useState2[0], setEditingState = _useState2[1]; var _useState3 = (0, _react.useState)(initialValue), _useState4 = _slicedToArray(_useState3, 2), value = _useState4[0], setValueState = _useState4[1]; var _useState5 = (0, _react.useState)(initialValue), _useState6 = _slicedToArray(_useState5, 2), committed = _useState6[0], setCommittedState = _useState6[1]; var _useState7 = (0, _react.useState)(null), _useState8 = _slicedToArray(_useState7, 2), error = _useState8[0], setError = _useState8[1]; // Refs mirror the state that the (stable) event handlers read, so the handlers // never go stale and never need to be re-created as the value changes. var elRef = (0, _react.useRef)(null); var valueRef = (0, _react.useRef)(value); var committedRef = (0, _react.useRef)(committed); var editingRef = (0, _react.useRef)(editing); // Set right before a programmatic exit so the blur that follows the field // unmounting is ignored (it isn't a real click-outside). var skipBlurRef = (0, _react.useRef)(false); var validateRef = (0, _react.useRef)(validate); var onCommitRef = (0, _react.useRef)(onCommit); (0, _react.useEffect)(function () { validateRef.current = validate; onCommitRef.current = onCommit; }); var setValue = (0, _react.useCallback)(function (v) { valueRef.current = v; setValueState(v); }, []); var setCommitted = (0, _react.useCallback)(function (v) { committedRef.current = v; setCommittedState(v); }, []); var setEditing = (0, _react.useCallback)(function (v) { editingRef.current = v; setEditingState(v); }, []); var begin = (0, _react.useCallback)(function () { skipBlurRef.current = false; setValue(committedRef.current); setError(null); setEditing(true); }, [setValue, setEditing]); var cancel = (0, _react.useCallback)(function () { skipBlurRef.current = true; setValue(committedRef.current); setError(null); setEditing(false); }, [setValue, setEditing]); var reset = (0, _react.useCallback)(function () { var next = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialValue; skipBlurRef.current = true; setCommitted(next); setValue(next); setError(null); setEditing(false); }, [initialValue, setCommitted, setValue, setEditing]); var commit = (0, _react.useCallback)(function (v) { var _onCommitRef$current; skipBlurRef.current = true; setCommitted(v); setValue(v); setError(null); setEditing(false); (_onCommitRef$current = onCommitRef.current) === null || _onCommitRef$current === void 0 || _onCommitRef$current.call(onCommitRef, v); }, [setCommitted, setValue, setEditing]); // Run the validator (if any) and accept, or handle the failure per source: // a key retry keeps the user in the field with a message; an outside click // reverts so the user is never trapped in an invalid editor. var attempt = (0, _react.useCallback)(function (source) { var v = valueRef.current; var result = validateRef.current ? validateRef.current(v) : true; if (result === true) { commit(v); return true; } if (source === "outside") { cancel(); return false; } setError(result); return false; }, [commit, cancel]); var onChange = (0, _react.useCallback)(function (e) { setValue(e.target.value); // Clear a stale error as the user edits; the next accept re-validates. setError(function (prev) { return prev == null ? prev : null; }); }, [setValue]); var onKeyDown = (0, _react.useCallback)(function (e) { // Keep Enter/Esc from reaching the card's own keyboard model (task 2/8). e.stopPropagation(); if (e.key === "Escape") { e.preventDefault(); cancel(); return; } if (e.key === "Enter") { var withMeta = e.metaKey || e.ctrlKey; // Multi-line: a bare Enter is a newline; only Cmd/Ctrl+Enter saves. if (multiline && !withMeta) return; e.preventDefault(); attempt("key"); } }, [attempt, cancel, multiline]); var onBlur = (0, _react.useCallback)(function () { if (skipBlurRef.current) { skipBlurRef.current = false; return; } if (!editingRef.current) return; attempt("outside"); }, [attempt]); var setRef = (0, _react.useCallback)(function (el) { elRef.current = el; }, []); // On entering editing, focus the field and drop the caret at the end so the // cursor is visible and existing text is ready to extend. (0, _react.useEffect)(function () { if (!editing) return; var el = elRef.current; if (!el) return; el.focus(); var len = el.value.length; try { el.setSelectionRange(len, len); } catch (_unused) { /* some input types disallow setSelectionRange — harmless here. */ } }, [editing]); return { editing, value, committed, error, multiline, begin, cancel, reset, bind: { ref: setRef, value, onChange, onKeyDown, onBlur, "aria-invalid": error != null } }; }; //# sourceMappingURL=useInlineEdit.js.map