lexical
lexical copied to clipboard
Bug: editor.focus() won't work if called after setting editor as editable
Steps To Reproduce
-
Click the
focus editorbutton -
The editor has focus
-
In
App.jschangeeditableprop toeditable:Before
<Editor editable={true} autoFocus={editable} />After
<Editor editable={editable} autoFocus={editable} /> -
Refresh
-
Click the
focus editorbutton -
Editor has NO focus
Link
https://codesandbox.io/s/editable-focus-issue-o9jc86?file=/src/Editor.js
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?
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.
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}
/>
How to do it with multiples instances of the lexical component @abpbackup ? Thanks a lot
@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);
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]);
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}
/>
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]);
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 };
I don't think these workarounds should be required in recent versions (possibly as early as v0.22)