Bug: selection.hasFormat(FORMAT) always true, even after removing all editor content
- Dominant language
- TypeScript
- Stars
- 23.9k
- Forks
- 2.2k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 61
Description
Lexical version: 0.12.6
## Steps To Reproduce
1. Press `ctrl+b` enter some text
2. Press `ctrl+a`
3. Press backspace, `selection.hasFormat(FORMAT)` stays true
4. Start typing again, bold styles are automatically applied
In this example [https://lexical-rich-text-react-demo.vercel.app/](https://lexical-rich-text-react-demo.vercel.app/) using v0.6.4 it works, upgrading to v0.12.6 breaks [https://playground.lexical.dev/](https://playground.lexical.dev/)
```Editor.tsx
import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin';
import { InitialConfigType, LexicalComposer } from '@lexical/react/LexicalComposer';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { EditorState } from 'lexical';
import { forwardRef, memo, useState } from 'react';
import { EditorWrapper, Placeholder } from './Editor.styled';
import { EditorProps } from './Editor.types';
import { AutoFocusPlugin } from './plugins/AutoFocusPlugin';
import { ToolbarPlugin } from './plugins/ToolbarPlugin';
function onError(error: any) {
console.error(error);
}
const initialConfig: InitialConfigType = {
namespace: 'MyEditor',
theme: {
root: 'editor-root',
paragraph: 'editor-paragraph',
text: {
bold: 'editor-text-bold',
italic: 'editor-text-italic',
underline: 'editor-text-underline',
strikethrough: 'editor-text-strikethrough',
underlineStrikethrough: 'editor-text-underlineStrikethrough',
},
},
onError,
};
const EditorComponent = forwardRef(() => {
const [editorState, setEditorState] = useState();
return (
}
placeholder={}
ErrorBoundary={LexicalErrorBoundary}
/>
);
});
export const Editor = memo(EditorComponent);
```
```ToolbarPlugin.tsx
import { $isLinkNode } from '@lexical/link';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $findMatchingParent, mergeRegister } from '@lexical/utils';
import {
$getRoot,
$getSelection,
$isElementNode,
$isParagraphNode,
$isRangeSelection,
CLEAR_EDITOR_COMMAND,
FORMAT_ELEMENT_COMMAND,
} from 'lexical';
import { memo, useCallback, useEffect, useState } from 'react';
import { alignmentControlsConfig, fomattingControlsConfig } from './ToolbarPlugin.constants';
import { Separator, StyledToolbar, ToolbarControl } from './ToolbarPlugin.styled';
import { getSelectedNode } from './ToolbarPlugin.utils';
const ToolbarPluginComponent = () => {
const [editor] = useLexicalComposerContext();
const [blockType, setBlockType] = useState('paragraph');
const [isEditorEmpty, setIsEditorEmpty] = useState(true);
const [formatStyles, setFormatStyles] = useState({
bold: false,
italic: false,
underline: false,
strikethrough: false,
});
const [alignmentStyles, setAlignmentStyles] = useState({
left: false,
center: false,
right: false,
});
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const node = getSelectedNode(selection);
const parent = node.getParent();
const element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM !== null) {
const type = element.getType();
setBlockType(() => type);
}
let matchingParent;
if ($isLinkNode(parent)) {
// If node is a link, we need to fetch the parent paragraph node to set format
matchingParent = $findMatchingParent(node, parentNode => $isElementNode(parentNode) && !parentNode.isInline());
}
const alignmentStyle = $isElementNode(matchingParent)
? matchingParent?.getFormatType?.()
: $isElementNode(node)
? node.getFormatType()
: parent?.getFormatType() || 'left';
setFormatStyles({
bold: selection.hasFormat('bold'),
italic: selection.hasFormat('italic'),
underline: selection.hasFormat('underline'),
strikethrough: selection.hasFormat('strikethrough'),
});
setAlignmentStyles({
center: false,
left: false,
right: false,
[alignmentStyle]: true,
});
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
const root = $getRoot();
const children = root.getChildren();
if (children.length > 1) {
setIsEditorEmpty(false);
return;
}
if ($isParagraphNode(children[0])) {
setIsEditorEmpty(children[0].getChildren().length === 0);
} else {
setIsEditorEmpty(false);
}
});
}),
);
}, [editor, updateToolbar]);
useEffect(() => {
if (isEditorEmpty) {
editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
}
}, [editor, isEditorEmpty]);
return (
{fomattingControlsConfig.map(config => {
return (
}
square
variant="ghost"
isActive={formatStyles[config.control]}
onClick={() => {}}
/>
);
})}
{alignmentControlsConfig.map(config => {
return (
}
square
variant="ghost"
isActive={alignmentStyles[config.control]}
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, config.control);
}}
/>
);
})}
);
};
export const ToolbarPlugin = memo(ToolbarPluginComponent);
```
## The current behavior
Does not clear text formatting
## The expected behavior
Should clear text formatting
Contributor guide
Research direction
Reproduce the formatting state issue using the steps in Editor.tsx and ToolbarPlugin.tsx, then inspect the updateToolbar listener and the ClearEditorPlugin interaction. Compare behavior in the linked v0.6.4 demo and the v0.12.6 playground; done means deleting all content clears selection formatting and newly typed text is no longer bold.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100