diff --git a/web/src/App.tsx b/web/src/App.tsx index 77279b0f..b330d4e6 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,16 +1,7 @@ -import { - type FormEvent, - type KeyboardEvent, - type ReactNode, - useCallback, - useEffect, - useRef, - useState, - useTransition, -} from 'react'; +import { type KeyboardEvent, type ReactNode, useCallback, useEffect, useRef, useState, useTransition } from 'react'; import { ThreeDotsScale } from 'react-svg-spinners'; import { fend } from './lib/fend'; -import { useHistory } from './hooks/useHistory'; +import { useCurrentInput } from './hooks/useCurrentInput'; const examples = ` > 5'10" to cm @@ -51,11 +42,9 @@ function NewTabLink({ children, href }: { children: ReactNode; href: string }) { } export default function App({ widget = false }: { widget?: boolean }) { - const [currentInput, setCurrentInput] = useState(''); const [output, setOutput] = useState(widget ? <> : exampleContent); - const { history, addToHistory } = useHistory(); + const { currentInput, addToHistory, onInput, upArrow, downArrow } = useCurrentInput(); const [variables, setVariables] = useState(''); - const [navigation, setNavigation] = useState(0); const [hint, setHint] = useState(''); useEffect(() => { void (async () => { @@ -77,10 +66,7 @@ export default function App({ widget = false }: { widget?: boolean }) { inputText.current?.focus(); } }, []); - const update = useCallback((e: FormEvent) => { - setCurrentInput(e.currentTarget.value); - setNavigation(0); - }, []); + const [isPending, startTransition] = useTransition(); const onKeyDown = useCallback( (event: KeyboardEvent) => { @@ -92,19 +78,14 @@ export default function App({ widget = false }: { widget?: boolean }) { setOutput(null); return; } - if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { - if (navigation > 0) { - event.preventDefault(); - if (event.key === 'ArrowUp') { - setNavigation(n => Math.min(n + 1, history.length)); - } else { - setNavigation(n => Math.max(n - 1, 0)); - setCurrentInput(''); - } - } else if (currentInput.trim().length === 0 && event.key === 'ArrowUp' && history.length > 0) { - event.preventDefault(); - setNavigation(1); - } + if (event.key === 'ArrowUp') { + event.preventDefault(); + upArrow(); + return; + } + if (event.key === 'ArrowDown') { + event.preventDefault(); + downArrow(); return; } @@ -115,7 +96,7 @@ export default function App({ widget = false }: { widget?: boolean }) { } event.preventDefault(); if (currentInput.trim() === 'clear') { - setCurrentInput(''); + onInput(''); setOutput(null); return; } @@ -123,12 +104,11 @@ export default function App({ widget = false }: { widget?: boolean }) { startTransition(async () => { const request =

{`> ${currentInput}`}

; addToHistory(currentInput); - setNavigation(0); const fendResult = await fend(currentInput, 1000000000, variables); if (!fendResult.ok && fendResult.message === 'cancelled') { return; } - setCurrentInput(''); + onInput(''); console.log(fendResult); const result =

{fendResult.ok ? fendResult.result : fendResult.message}

; if (fendResult.ok && fendResult.variables.length > 0) { @@ -144,7 +124,7 @@ export default function App({ widget = false }: { widget?: boolean }) { inputHint.current?.scrollIntoView(); }); }, - [currentInput, addToHistory, variables, history.length, navigation], + [currentInput, addToHistory, variables, onInput, downArrow, upArrow], ); useEffect(() => { document.addEventListener('click', focus); @@ -152,11 +132,6 @@ export default function App({ widget = false }: { widget?: boolean }) { document.removeEventListener('click', focus); }; }, [focus]); - useEffect(() => { - if (navigation > 0) { - setCurrentInput(history[history.length - navigation]); - } - }, [navigation, history]); return (
{!widget && ( @@ -177,13 +152,13 @@ export default function App({ widget = false }: { widget?: boolean }) { rows={currentInput.split('\n').length} ref={inputText} value={currentInput} - onInput={update} + onInput={onInput} onKeyDown={onKeyDown} autoFocus />

- {hint || (isPending ? : null)} + {hint || (isPending ? : <> )}

diff --git a/web/src/hooks/useCurrentInput.ts b/web/src/hooks/useCurrentInput.ts new file mode 100644 index 00000000..b83f649e --- /dev/null +++ b/web/src/hooks/useCurrentInput.ts @@ -0,0 +1,65 @@ +import { FormEvent, useCallback, useState } from 'react'; +import { useHistory } from './useHistory'; + +export function useCurrentInput() { + const { history, addToHistory: saveToHistory } = useHistory(); + const [currentInput, setCurrentInput] = useState(''); + const [navigation, setNavigation] = useState(0); + + const navigate = useCallback( + (direction: 'up' | 'down' | 'bottom') => { + setNavigation(n => { + let newValue: number; + switch (direction) { + case 'up': + newValue = Math.min(n + 1, history.length); + break; + case 'down': + newValue = Math.max(n - 1, 0); + break; + case 'bottom': + newValue = 0; + break; + } + if (newValue > 0) { + setCurrentInput(history[history.length - newValue]); + } + if (newValue === 0 && (direction === 'up' || direction === 'down')) { + setCurrentInput(''); + } + return newValue; + }); + }, + [history], + ); + + const onInput = useCallback( + (e: string | FormEvent) => { + navigate('bottom'); + setCurrentInput(typeof e === 'string' ? e : e.currentTarget.value); + }, + [navigate], + ); + + const upArrow = useCallback(() => { + if (currentInput.trim().length !== 0 && navigation === 0) { + // todo we should allow navigating history if input has been typed + return; + } + navigate('up'); + }, [currentInput, navigate, navigation]); + + const downArrow = useCallback(() => { + navigate('down'); + }, [navigate]); + + const addToHistory = useCallback( + (newEntry: string) => { + saveToHistory(newEntry); + setNavigation(0); + }, + [saveToHistory], + ); + + return { currentInput, addToHistory, onInput, downArrow, upArrow }; +}