From 2fca323cffb24883d983778f47e8d8c6db5cb18c Mon Sep 17 00:00:00 2001 From: "Ruanqianqian (Lisa) Huang" Date: Thu, 18 Aug 2022 07:30:49 -0700 Subject: [PATCH] Keyboard navigation (#109) * add tabIndex to allow tabbing on the editor element * tab on editable blocks to start editing * able to enter/escape from editing mode only when not actually making any changes to the code * navigation working in a hacky way * use named variables for focus and blur handlers * remove document-level keydown handler with useEffect * use constant vars for props * use the same closure instance for add/remove onkeydown handler on document * quick fix * dependencies for handleKeyDown --- .../TutorialComponents/CodeBlock.tsx | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/website/src/components/TutorialComponents/CodeBlock.tsx b/website/src/components/TutorialComponents/CodeBlock.tsx index 6cbc2d368..db9f6a8ce 100644 --- a/website/src/components/TutorialComponents/CodeBlock.tsx +++ b/website/src/components/TutorialComponents/CodeBlock.tsx @@ -65,7 +65,7 @@ export function UndoBtn(props: { undoCode: () => void }) { 'clean-btn', codeBlockContentStyles.codeButton, )} - style={{borderColor: "var(--custom-editor-reset-color)"}} + style={{ borderColor: "var(--custom-editor-reset-color)" }} onClick={undoCode}> @@ -93,9 +93,10 @@ function CodeEditor(props: { }) { const editorRef = useRef(null); const [code, setCode] = useState(props.code || ""); - const [disabled, setDisabled] = useState(props.disabled); + // const [disabled, setDisabled] = useState(props.disabled); const [allowUndo, setAllowUndo] = useState(false); const [tmpCode, setTmpCode] = useState(""); + const [hasFocus, setHasFocus] = useState(false); useEffect(() => { setCode(props.code); @@ -105,7 +106,11 @@ function CodeEditor(props: { setCode(_code.slice(0, -1)); }, []); - useEditable(editorRef, onEditableChange, { + + const disabled = props.disabled || !hasFocus; + // we might use `editObj` later for fixing cursor position + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const editObj = useEditable(editorRef, onEditableChange, { disabled: disabled, indentation: 2, }); @@ -119,20 +124,68 @@ function CodeEditor(props: { const onClickReset = () => { setTmpCode(code.slice()); // use copy not reference setCode(props.code); - setDisabled(true); + // setDisabled(true); + setHasFocus(false); setAllowUndo(true); setTimeout(() => { - setDisabled(props.disabled); + // setDisabled(props.disabled); + setHasFocus(true); setAllowUndo(false); }, 3000); } const onClickUndo = () => { - console.log(tmpCode) setCode(tmpCode); } - // prismIncludeLanguages(Prism); + const handleFocus = () => { + const selectObj = window.getSelection(); + if (selectObj.rangeCount === 0) { + // when focusing on the editor without using the mouse, + // merely from the Tab key + const range = new Range(); + range.collapse(true); + selectObj.addRange(range); + } + setHasFocus(true); + } + + const handleBlur = () => { + setHasFocus(false); + } + + const handleKeyDown = (e: KeyboardEvent) => { + const editor = editorRef.current; + if (e.key === 'Escape' && !props.disabled) { + if (e.target === editor) { + e.stopImmediatePropagation(); + e.stopPropagation(); + e.preventDefault(); + editor.focus(); // sometimes blur won't fire without focus first + editor.blur(); + } + } + } + + const handleKeyDownIfEnabled = useCallback((_e: KeyboardEvent) => { + if (!disabled) { + handleKeyDown(_e); + } + }, [disabled]); + + + useEffect(() => { + document.addEventListener('keydown', + handleKeyDownIfEnabled, + { + capture: true, + } + ); + return () => { + document.removeEventListener('keydown', handleKeyDownIfEnabled); + }; + }, [handleKeyDownIfEnabled]); + return (
@@ -164,20 +217,19 @@ function CodeEditor(props: { ))} }