@vectara/vectara-ui
Version:
Vectara's design system, codified as a React and Sass component library
128 lines (127 loc) • 4.78 kB
JavaScript
import { useRef, useState } from "react";
function loadHistory(storageKey) {
try {
const stored = sessionStorage.getItem(storageKey);
return stored ? JSON.parse(stored) : [];
}
catch (_a) {
return [];
}
}
function saveHistory(storageKey, history) {
try {
sessionStorage.setItem(storageKey, JSON.stringify(history));
}
catch (_a) {
// Quota exceeded or unavailable — silently drop.
}
}
/**
* Cycles through previous submissions with UP/DOWN arrow keys.
*
* State machine for navigation (3 phases):
*
* idle — no navigation active.
* primed — first key press (UP/DOWN) consumed by native cursor movement.
* cycling — UP/DOWN keys walk through the history stack.
*
* Transitions on arrow key press:
*
* idle + empty input + UP → cycling (skip primed, start navigating immediately)
* idle + empty input + DOWN → idle (nothing to cycle to)
* idle + has text + UP/DN → primed (let native cursor move first)
* primed + same direction → cycling (second press enters history)
* primed + diff direction → primed (re-prime for the new direction)
* cycling + same direction → cycling (keep walking history)
* cycling + diff direction → primed (let native cursor adjust, re-prime)
*
* The composer calls reset() on any user edit and record() on submit, both of
* which return the machine to idle.
*/
export function useComposerHistory({ storageKey, value, setValue }) {
const historyRef = useRef(storageKey ? loadHistory(storageKey) : []);
const [historyIndex, setHistoryIndex] = useState(null);
const draftRef = useRef("");
const phaseRef = useRef("idle");
const directionRef = useRef(null);
const reset = () => {
setHistoryIndex(null);
phaseRef.current = "idle";
directionRef.current = null;
};
const record = (input) => {
if (input.trim()) {
historyRef.current = [...historyRef.current, input];
if (storageKey)
saveHistory(storageKey, historyRef.current);
}
reset();
};
const handleKeyDown = (e) => {
if (e.key !== "ArrowUp" && e.key !== "ArrowDown")
return;
const history = historyRef.current;
if (history.length === 0)
return;
const direction = e.key === "ArrowUp" ? "up" : "down";
const phase = phaseRef.current;
const lastDirection = directionRef.current;
// Idle with text in input — let native cursor handle the first press.
if (phase === "idle" && value.length > 0) {
phaseRef.current = "primed";
directionRef.current = direction;
return;
}
// Idle with empty input — skip primed and start cycling immediately on UP; ignore DOWN.
if (phase === "idle") {
if (direction === "down")
return;
e.preventDefault();
draftRef.current = value;
const newIndex = history.length - 1;
setHistoryIndex(newIndex);
setValue(history[newIndex]);
phaseRef.current = "cycling";
directionRef.current = direction;
return;
}
// Direction changed — let native cursor handle it, re-prime.
if (direction !== lastDirection) {
phaseRef.current = "primed";
directionRef.current = direction;
return;
}
// Same direction: primed → cycling, or cycling → cycling.
// Primed DOWN with no active cycling — nothing to cycle to.
if (direction === "down" && historyIndex === null)
return;
e.preventDefault();
if (direction === "up") {
if (historyIndex === null) {
draftRef.current = value;
const newIndex = history.length - 1;
setHistoryIndex(newIndex);
setValue(history[newIndex]);
}
else if (historyIndex > 0) {
const newIndex = historyIndex - 1;
setHistoryIndex(newIndex);
setValue(history[newIndex]);
}
}
else {
if (historyIndex !== null && historyIndex < history.length - 1) {
const newIndex = historyIndex + 1;
setHistoryIndex(newIndex);
setValue(history[newIndex]);
}
else if (historyIndex !== null) {
setHistoryIndex(null);
setValue(draftRef.current);
}
}
phaseRef.current = "cycling";
directionRef.current = direction;
};
return { handleKeyDown, record, reset };
}