facebook / facebook/lexical

Bug: selection.hasFormat(FORMAT) always true, even after removing all editor content

Open
#5,518 5 comments 0 reactions 0 assignees View on GitHub
formatting
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.