diff --git a/packages/lexical-playground/__tests__/unit/FloatingTextFormatToolbar.test.tsx b/packages/lexical-playground/__tests__/unit/FloatingTextFormatToolbar.test.tsx new file mode 100644 index 00000000000..ed61d8e6e9f --- /dev/null +++ b/packages/lexical-playground/__tests__/unit/FloatingTextFormatToolbar.test.tsx @@ -0,0 +1,111 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {RovingTabIndexExtension} from '@lexical/a11y'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer'; +import {RichTextExtension} from '@lexical/rich-text'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $selectAll, + defineExtension, + type LexicalEditor, +} from 'lexical'; +import * as React from 'react'; +import {act} from 'react'; +import {createRoot, type Root} from 'react-dom/client'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; + +import FloatingTextFormatToolbarPlugin from '../../src/plugins/FloatingTextFormatToolbarPlugin'; + +const ToolbarTestExtension = /* @__PURE__ */ defineExtension({ + $initialEditorState: () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('hello'))); + }, + dependencies: [RichTextExtension, RovingTabIndexExtension], + name: '[test-floating-toolbar]', +}); + +describe('FloatingTextFormatToolbarPlugin', () => { + let container: HTMLDivElement; + let anchorElem: HTMLDivElement; + let reactRoot: Root; + let editor: LexicalEditor; + + function Capture() { + const [contextEditor] = useLexicalComposerContext(); + editor = contextEditor; + return null; + } + + beforeEach(async () => { + container = document.createElement('div'); + anchorElem = document.createElement('div'); + document.body.append(container, anchorElem); + reactRoot = createRoot(container); + + await act(async () => { + reactRoot.render( + + + + , + ); + }); + + // The popup only opens for a non-collapsed selection whose DOM anchor is + // inside the root element, so set both. + await act(async () => { + editor.update(() => void $selectAll(), {discrete: true}); + const textDOM = editor.getRootElement()!.querySelector('p')!.firstChild! + .firstChild!; + document + .getSelection()! + .setBaseAndExtent(textDOM, 0, textDOM, 'hello'.length); + document.dispatchEvent(new Event('selectionchange')); + }); + }); + + afterEach(async () => { + await act(async () => { + reactRoot.unmount(); + }); + container.remove(); + anchorElem.remove(); + }); + + function formatButtonCount(): number { + return anchorElem.querySelectorAll( + '.floating-text-format-popup button[aria-label^="Format text"]', + ).length; + } + + it('hides its format buttons when the editor becomes read-only', async () => { + expect(anchorElem.querySelector('.floating-text-format-popup')).not.toBe( + null, + ); + expect(formatButtonCount()).toBeGreaterThan(0); + + await act(async () => { + editor.setEditable(false); + }); + expect(formatButtonCount()).toBe(0); + + await act(async () => { + editor.setEditable(true); + }); + expect(formatButtonCount()).toBeGreaterThan(0); + }); +}); diff --git a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx index 88d56ebc83e..ccd4f0bcba7 100644 --- a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx @@ -12,6 +12,7 @@ import {useMergeRefs} from '@floating-ui/react'; import {$isCodeNode} from '@lexical/code'; import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import {useLexicalRovingTabIndexRef} from '@lexical/react/useLexicalRovingTabIndexRef'; import { $getSelection, @@ -82,6 +83,9 @@ function TextFormatFloatingToolbar({ }): JSX.Element { const popupCharStylesEditorRef = useRef(null); const rovingRef = useLexicalRovingTabIndexRef(); + // Subscribed rather than read off the editor, so that setEditable() actually + // re-renders the toolbar. + const isEditable = useLexicalEditable(); const mergedRef = useMergeRefs([popupCharStylesEditorRef, rovingRef, ref]); const insertLink = useCallback(() => { @@ -224,7 +228,7 @@ function TextFormatFloatingToolbar({ className="floating-text-format-popup" role="toolbar" aria-label="Floating text format toolbar"> - {editor.isEditable() && ( + {isEditable && ( <>