diff --git a/src/elements/toolbar.js b/src/elements/toolbar.js index 366e3c360..215f83aa2 100644 --- a/src/elements/toolbar.js +++ b/src/elements/toolbar.js @@ -134,13 +134,15 @@ export class LexicalToolbarElement extends HTMLElement { } } - #dispatchButtonCommand(event, { dataset: { command, payload } }) { + #dispatchButtonCommand(event, button) { + const { command, payload } = button.dataset const isKeyboard = event instanceof PointerEvent && event.pointerId === -1 this.editor.update(() => { this.editor.dispatchCommand(command, payload) }, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined }) + if (button.closest(".lexxy-editor__toolbar-dropdown")) this.closeDropdowns() if (!isKeyboard) this.editor.focus() } @@ -200,14 +202,23 @@ export class LexicalToolbarElement extends HTMLElement { } #monitorSelectionChanges() { - this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - this.#updateButtonStates() - this.closeDropdowns() - }) + this.#listeners.track(this.editor.registerUpdateListener(({ editorState, prevEditorState }) => { + editorState.read(() => this.#updateButtonStates()) + if (this.#selectionChanged(editorState, prevEditorState)) this.closeDropdowns() })) } + #selectionChanged(editorState, prevEditorState) { + const selection = editorState.read($getSelection) + const previousSelection = prevEditorState.read($getSelection) + + if (selection === null || previousSelection === null) { + return selection !== previousSelection + } else { + return !selection.is(previousSelection) + } + } + #monitorHistoryChanges() { this.#listeners.track( this.editor.registerCommand(CAN_UNDO_COMMAND, (enabled) => { this.#setButtonDisabled("undo", !enabled) }, COMMAND_PRIORITY_LOW), diff --git a/test/browser/tests/formatting/toolbar.test.js b/test/browser/tests/formatting/toolbar.test.js index d0577f44d..ea124db84 100644 --- a/test/browser/tests/formatting/toolbar.test.js +++ b/test/browser/tests/formatting/toolbar.test.js @@ -223,6 +223,45 @@ test.describe("Toolbar", () => { expect(accept).toBeNull() }) + test("dropdown stays open during editor updates that do not change the selection", async ({ page, editor }) => { + await editor.setValue(HELLO_EVERYONE) + + const formatPanel = page.locator("lexxy-toolbar-dropdown.lexxy-editor__toolbar-dropdown [data-dropdown-panel]").first() + + await page.locator("lexxy-toolbar button[name='format']").click() + await expect(formatPanel).toBeVisible() + + // Simulate an upload-driven update: content mutates but the selection is untouched, + // the same way upload progress / placeholder mutations fire while a dropdown is open. + await page.evaluate(async () => { + const element = document.querySelector("lexxy-editor") + await new Promise((resolve) => { + element.editor.update(() => { + const editorState = element.editor._pendingEditorState + const root = editorState._nodeMap.get("root") + const paragraph = editorState._nodeMap.get(root.__first) + const textNode = editorState._nodeMap.get(paragraph.__first) + textNode.setTextContent("Hello everyone uploading…") + }, { onUpdate: resolve }) + }) + }) + + await expect(formatPanel).toBeVisible() + }) + + test("dropdown closes when the selection changes", async ({ page, editor }) => { + await editor.setValue(HELLO_EVERYONE) + + const formatPanel = page.locator("lexxy-toolbar-dropdown.lexxy-editor__toolbar-dropdown [data-dropdown-panel]").first() + + await page.locator("lexxy-toolbar button[name='format']").click() + await expect(formatPanel).toBeVisible() + + await editor.select("everyone") + + await expect(formatPanel).toBeHidden() + }) + test("external toolbar", async ({ page }) => { await page.goto("/toolbar-external.html") await expect(