@activecollab/components
Version:
ActiveCollab Components
208 lines (202 loc) • 8.81 kB
JavaScript
;
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