How can I detect if a @lexical/react editor is focused?

5k Views Asked by At

I want to create a function that can determine if my editor has focus:

function hasFocus(editor: LexicalEditor) {
  const hasFocus = editor.getEditorState().read(() => {
      // return $...
  })
  
  return hasFocus
}

I dag through source code and docs, but found no method that could detect this directly. In my testing, Selection object doesn't seem to reliably determine whether the Editor is focused in DOM or not.

So, how can I detect editor focus?

3

There are 3 best solutions below

0
On

For some reason, the focus command didn't work fine for me, so I decided to use the global editor listener instead for reading keyboard and mouse updates

const [hasFocus, setFocus] = React.useState(false)
...
  useEffect(() => {
    const update = (): void => {
      editor.getEditorState().read(() => {
        setFocus(true)
      })
    }
    update()
  }, [])
  useEffect(() => {
    return mergeRegister(
      editor.registerUpdateListener(({ editorState }) => {
        editorState.read(() => {
          setFocus(true)
        })
      }),
      editor.registerCommand(
        BLUR_COMMAND,
        () => {
          setFocus(false)
          return false
        },
        COMMAND_PRIORITY_LOW
      )
    )
  }, [editor, hasFocus])

It could have some improvements (useRef, ...) but worked for me

0
On

I found out I can subscribe to FOCUS_COMMAND and BLUR_COMMAND and update a local state when they change:

const useEditorFocus = () => {
  const [editor] = useLexicalComposerContext()
  // Possibly use useRef for synchronous updates but no re-rendering effect
  const [hasFocus, setFocus] = useState(false)
  

  useEffect(
    () =>
      editor.registerCommand(
        BLUR_COMMAND,
        () => {
          setFocus(false)
          return false
        },
        COMMAND_PRIORITY_LOW
      ),
    []
  )

  useEffect(
    () =>
      editor.registerCommand(
        FOCUS_COMMAND,
        () => {
          setFocus(true)
          return false
        },
        COMMAND_PRIORITY_LOW
      ),
    []
  )

  return hasFocus
}

This seems sufficient, but I'm still wondering if it is possible to get the information directly from the source of truth (EditorState), instead of tracking it via a side-effect.

4
On

To expand on your answer

We introduced commands as the primary mechanism to handle input events because we will prevent them by default via event.preventDefault (and users may still want to listen to them or override them).

Focus was not strictly necessary but it felt natural to follow the same command pattern.

// Commands are subscriptions so the default state is important!
const [hasFocus, setHasFocus] = useState(() => {
  return editor.getRootElement() === document.activeElement);
});

useLayoutEffect(() => {
  setHasFocus(editor.getRootElement() === document.activeElement);
  return mergeRegister(
    editor.registerCommand(FOCUS_COMMAND, () => { ... }),
    editor.registerCommand(BLUR_COMMAND, () => { ... }),
  );
}, [editor]);

When possible you should avoid useState altogether since React re-renders are expensive (if you don't need to display something different when this value changes).

Note that the EditorState does not have information on whether the input is focused.