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

3

5

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?

Sugarcoat answered 11/5, 2022 at 18:1 Comment(0)
S
10

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.

Sugarcoat answered 11/5, 2022 at 18:1 Comment(0)
H
8

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.

Homy answered 12/5, 2022 at 8:7 Comment(4)
I'm thankful for your answer, but I'm having a tough time understanding what you are trying to convey. More specifically: 1. I don't really understand your first sentence 2. From your code, it seems like you're detecting focus via editor.getRootElement() === document.activeElement - is this a reliable way? If so, why use anything else? I was hesitant to use such solution, because root element has sub-elements and I'm not sure which one has focus 3. you advise against useState, yet you use it in your example - so would you use it for focus, or not? 4. what does mergeRegister do?Sugarcoat
@MichalKurz - 1. That you can't do document.querySelector('[contenteditable="true"]').addListener('beforeinput') for example, it will never trigger on your end. Commands are subscriptions, if you need to understand when focus changes in real time, you want commands. 2. Won't work for nested editors (but they're different editor instances anyway). 3. I just continued your example, use useState if you need to do something like isFocus && <span>...</span>, otherwise store it somewhere else 4. Does the unmount part of the useEffect like in your example but for many at onceHomy
Thank you very much for your comment, all of your clarifications did the job for me :)Sugarcoat
So, just to reassure: If I don't work with nested editors (which I'm not intending to), I can ditch my approach and just go with hasFocus = (editor) => editor.getRootElement() === document.activeElement, correct?Sugarcoat
F
0

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

Forensic answered 19/12, 2022 at 18:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.