lexical icon indicating copy to clipboard operation
lexical copied to clipboard

Bug: editor.focus() won't work if called after setting editor as editable

Open asso1985 opened this issue 2 years ago • 10 comments

Steps To Reproduce

  • Click the focus editor button

  • The editor has focus

  • In App.js change editable prop to editable:

    Before <Editor editable={true} autoFocus={editable} />

    After <Editor editable={editable} autoFocus={editable} />

  • Refresh

  • Click the focus editor button

  • Editor has NO focus

Link

https://codesandbox.io/s/editable-focus-issue-o9jc86?file=/src/Editor.js

asso1985 avatar May 05 '23 11:05 asso1985

We're struggling with the same issue for our whiteboard.

For our app, we first mark the element, then the second click makes the element editable.

What we observe is that the callback given to editor.focus() is never called, which to us sounds like some events are not propagating properly inside Lexical's update machinery. When stepping through the code using the debugger, we can see that the callback - together with the event itself - is added to the internal queue, but it doesn't seem like Lexical attempts to process the queue until we click the element a third time (which makes sense since we then actively click an editable element).

Were you able to find a workaround @asso1985?

ChristianJacobsen avatar Jul 29 '23 12:07 ChristianJacobsen

hey @ChristianJacobsen, still no luck on our side since for now we can skip this but surely this will come back hunting us soon as well.

The only way I managed to solve this (while spiking) was to basically render the component that make use of Lexical twice (one for view one for edit) but surely not an ideal solution.

asso1985 avatar Jul 31 '23 08:07 asso1985

Here is a workaround without using the FOCUS_COMMAND which only works for me the first time.

(document.querySelector('.editor-input') as HTMLInputElement).focus(); ...

<RichTextPlugin
              contentEditable={<ContentEditable className="editor-input" />}
              placeholder={<Placeholder value={placeholder} />}
              ErrorBoundary={LexicalErrorBoundary}
            />

abpbackup avatar Sep 27 '23 17:09 abpbackup

How to do it with multiples instances of the lexical component @abpbackup ? Thanks a lot

ljs19923 avatar Nov 11 '23 08:11 ljs19923

@abpbackup Thank you so much for sharing this. Saved me a nasty headache.

@ljs19923 I was able to handle this by setting a unique id for each editor and running the querySelector on this id instead of a className. I used uuid for this, but there may be better options. With uuid, you have to ensure it doesn't start with a number for it to be a valid selector. I just I added "id_" to the front and it seems to work fine.

import { v4 as uuidv4 } from "uuid";

//the unique id:
  const editorId = "id_" + uuidv4()
  
//on content editable:
            <RichTextPlugin
              contentEditable={
                <ContentEditable
                  id={editorId}
                  //...rest
                />
              }
       
//calling it
  const handleManuallyFocusEditor = () => {
    (document.querySelector(`#${editorId}`) as HTMLInputElement).focus();
  };              

As another tip, you can register this as a custom editor command and then call it from any component within that editor. Mine looks something like this:

//registering
//create the command
export const REFOCUS_COMMAND: LexicalCommand<void> = createCommand();
//then inside a component:
  useEffect(() => {
      return editor.registerCommand(
        REFOCUS_COMMAND,
        () => {
          handleManuallyFocusEditor();
          return true;
        },
        COMMAND_PRIORITY_NORMAL
      )
  }, [editor]);
  
//calling it:
  const [editor] = useLexicalComposerContext();
  editor.dispatchCommand(REFOCUS_COMMAND, undefined); 

DZiems avatar Nov 22 '23 23:11 DZiems

The above workaround didn't work in my case so I had to try a different approach. So basically, I registered an event that is called every time the editable state changes. This handler will then apply focus if the editor is editable.

useEffect(() => {
    const removeEditableListener = editor.registerEditableListener((isEditable) => {
        if (!isEditable) return;
        // This is the key to make focus work.
        setTimeout(() => editor.focus());
    });

    return removeEditableListener;
}, [editor]);

jnous-5 avatar Jan 19 '24 12:01 jnous-5

Here is a workaround without using the FOCUS_COMMAND which only works for me the first time.

(document.querySelector('.editor-input') as HTMLInputElement).focus(); ...

<RichTextPlugin
              contentEditable={<ContentEditable className="editor-input" />}
              placeholder={<Placeholder value={placeholder} />}
              ErrorBoundary={LexicalErrorBoundary}
            />

This one worked for me, here is how I defined the Placeholder:

function Placeholder(isEditable: boolean) {
  const [editor] = useLexicalComposerContext()
  return (
    <div
      className="absolute left-3 top-3"
      onClick={() => {
        if (isEditable) {
          editor.focus()
        }
      }}
    >
      Enter some text...
    </div>
  )
}

Then:

<RichTextPlugin
  contentEditable={
    <ContentEditable className="h-96 w-full bg-white shadow-mercury rounded-md p-3 focus:outline-none" />
  }
  placeholder={Placeholder}
  ErrorBoundary={LexicalErrorBoundary}
/>

abdessamadely avatar Mar 09 '24 10:03 abdessamadely

Hey small trick hope it help others :)

    useEffect(() => {
        return editor.registerCommand(
            LEXICAL_FOCUS_COMMAND,
            () => {
                if (editor.isEditable()) {
                    editor._rootElement?.focus()
                }

                return true;
            },
            COMMAND_PRIORITY_NORMAL
        )
    }, [editor]);

damikun avatar Mar 24 '24 20:03 damikun

riffing off @damikun's command example above, here's a quick plugin that gives the editor the initial focus

import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { FC, useEffect } from "react";

const InitialFocusPlugin: FC = () => {
    const [editor] = useLexicalComposerContext();

    useEffect(() => {
        if (editor.isEditable()) {
            editor._rootElement?.focus();
        }
    }, [editor]);

    return null;
};

export { InitialFocusPlugin };

yoDon avatar Feb 23 '25 17:02 yoDon

I don't think these workarounds should be required in recent versions (possibly as early as v0.22)

etrepum avatar Feb 23 '25 18:02 etrepum