facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
19.09k stars 1.61k forks source link

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

Open asso1985 opened 1 year ago

asso1985 commented 1 year ago

Steps To Reproduce

Link

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

ChristianJacobsen commented 1 year ago

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?

asso1985 commented 1 year ago

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.

abpbackup commented 11 months ago

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}
            />
ljs19923 commented 10 months ago

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

DZiems commented 9 months ago

@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); 
jnous-5 commented 7 months ago

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]);
abdessamadely commented 6 months ago

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}
/>
damikun commented 5 months ago

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]);