diff --git a/.changeset/clean-jeans-clean.md b/.changeset/clean-jeans-clean.md new file mode 100644 index 0000000..ff0fb80 --- /dev/null +++ b/.changeset/clean-jeans-clean.md @@ -0,0 +1,5 @@ +--- +"react-live": patch +--- + +Add tab gate to disable focus trap ([#399](https://github.com/FormidableLabs/react-live/pull/399)) diff --git a/packages/react-live/src/components/Editor/index.tsx b/packages/react-live/src/components/Editor/index.tsx index fb028af..24b0e83 100644 --- a/packages/react-live/src/components/Editor/index.tsx +++ b/packages/react-live/src/components/Editor/index.tsx @@ -1,6 +1,7 @@ import { Highlight, Prism, themes } from "prism-react-renderer"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useEditable } from "use-editable"; +import useTabGate from "../../hooks/useTabGate"; export type Props = { className?: string; @@ -16,10 +17,13 @@ export type Props = { const CodeEditor = (props: Props) => { const { tabMode = "indentation" } = props; + const containerRef = useRef(null); const editorRef = useRef(null); const [code, setCode] = useState(props.code || ""); const { theme } = props; + useTabGate(containerRef, editorRef, tabMode === "indentation"); + useEffect(() => { setCode(props.code); }, [props.code]); @@ -36,7 +40,7 @@ const CodeEditor = (props: Props) => { }, [code]); return ( -
+
, + editor: RefObject, + enabled = true +) { + const [tabGate, setTabGate] = useState(false); + + const setTabIndex = (element: RefObject, index: number) => + element.current && (element.current.tabIndex = index); + + const resetTabIndexes = () => { + if (container.current?.hasAttribute("tabIndex")) { + container.current?.removeAttribute("tabIndex"); + } + setTabIndex(editor, 0); + }; + + const containerBlur = (event: FocusEvent) => { + if (event.relatedTarget !== container.current) resetTabIndexes(); + }; + + const catchEscape = (event: KeyboardEvent) => { + if (event.code === "Escape" && enabled) setTabGate(true); + }; + + const containerFocus = () => editor.current?.focus(); + + const editorFocus = () => { + resetTabIndexes(); + setTabGate(false); + }; + + useEffect(() => { + container.current?.addEventListener("blur", containerBlur); + container.current?.addEventListener("focus", containerFocus); + editor.current?.addEventListener("keydown", catchEscape); + editor.current?.addEventListener("focus", editorFocus); + + return () => { + container.current?.removeEventListener("blur", containerBlur); + container.current?.removeEventListener("focus", containerFocus); + editor.current?.removeEventListener("keydown", catchEscape); + editor.current?.removeEventListener("focus", editorFocus); + }; + }, []); + + useEffect(() => { + if (!tabGate) return; + + setTabIndex(container, 0); + editor.current?.blur(); + setTabIndex(editor, -1); + }, [tabGate]); + + return tabGate; +}