Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions packages/react/src/components/Popovers/GenericPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,18 @@ export const GenericPopover = (
const element =
"element" in props.reference ? props.reference.element : undefined;

if (element !== undefined) {
if (
element !== undefined &&
(props.focusManagerProps?.disabled ||
!editor.isWithinEditor(element))
) {
// Only set domReference when FloatingFocusManager is disabled.
// When FloatingFocusManager is active (disabled !== false) and the
// reference is inside the ProseMirror editor, setting domReference
// causes floating-ui to call insertAdjacentElement on the reference,
// inserting a focus-return <span> into the PM contenteditable. This
// triggers PM's MutationObserver and resets the editor selection.
// (issue #2525)
refs.setReference(element);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand All @@ -166,7 +177,7 @@ export const GenericPopover = (
contextElement: element,
});
}
}, [props.reference, refs]);
}, [props.reference, refs, props.focusManagerProps?.disabled, editor]);

// Stores the last rendered `innerHTML` of the popover while it was open. The
// `innerHTML` is used while the popover is closing, as the React children
Expand Down
54 changes: 54 additions & 0 deletions tests/src/end-to-end/ai/ai-selection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from "@playwright/test";
import { test } from "../../setup/setupScript.js";
import { AI_URL } from "../../utils/const.js";
import { focusOnEditor } from "../../utils/editor.js";

const AI_BUTTON_SELECTOR = `[data-test="editwithAI"]`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the AI button selector match the rendered component.

AIToolbarButton does not expose data-test="editwithAI" in the provided component snippet, so this test can fail before reaching the regression assertion. Add the attribute to the button component or switch the test to a selector that exists.

Also applies to: 32-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/end-to-end/ai/ai-selection.test.ts` at line 6, The test's
AI_BUTTON_SELECTOR (`AI_BUTTON_SELECTOR = [data-test="editwithAI"]`) doesn't
match the actual rendered AIToolbarButton, causing failures; either add the
missing data-test="editwithAI" attribute to the AIToolbarButton component (where
it's defined) or update `AI_BUTTON_SELECTOR` to use the selector that the
component actually renders (e.g., the component's real data-test value, role,
aria-label, or text content); make the same change for the other occurrences
referenced around lines 32-35 so all selectors in this file target the rendered
element.


test.beforeEach(async ({ page }) => {
await page.goto(AI_URL);
});

test.describe("AI toolbar button should preserve selection (issue #2525)", () => {
test("Editor selection must be preserved after clicking the AI toolbar button", async ({
page,
}) => {
await focusOnEditor(page);

// Select text in the first paragraph
await page.keyboard.press("Home");
await page.keyboard.press("Shift+End");
await page.waitForTimeout(500);

// Record the PM selection before clicking
const selBefore = await page.evaluate(() => {
const pm = (window as any).ProseMirror;
return { from: pm.state.selection.from, to: pm.state.selection.to };
});
expect(selBefore.to - selBefore.from).toBeGreaterThan(0);

Check failure on line 28 in tests/src/end-to-end/ai/ai-selection.test.ts

View workflow job for this annotation

GitHub Actions / Playwright Tests - chromium (1/2)

[chromium] › src/end-to-end/ai/ai-selection.test.ts:13:7 › AI toolbar button should preserve selection (issue #2525) › Editor selection must be preserved after clicking the AI toolbar button

1) [chromium] › src/end-to-end/ai/ai-selection.test.ts:13:7 › AI toolbar button should preserve selection (issue #2525) › Editor selection must be preserved after clicking the AI toolbar button Error: expect(received).toBeGreaterThan(expected) Expected: > 0 Received: 0 26 | return { from: pm.state.selection.from, to: pm.state.selection.to }; 27 | }); > 28 | expect(selBefore.to - selBefore.from).toBeGreaterThan(0); | ^ 29 | 30 | // Click the AI button using page.mouse to trigger real browser 31 | // focus-shift behavior (Playwright's locator.click() bypasses it) at /__w/BlockNote/BlockNote/tests/src/end-to-end/ai/ai-selection.test.ts:28:43

// Click the AI button using page.mouse to trigger real browser
// focus-shift behavior (Playwright's locator.click() bypasses it)
const aiButton = page.locator(AI_BUTTON_SELECTOR);
await expect(aiButton).toBeVisible();
const box = (await aiButton.boundingBox())!;
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);

// Wait for AI menu to appear
await page
.locator(".bn-combobox-input input, .bn-combobox input")
.waitFor({ state: "visible", timeout: 3000 });

// The PM selection must match what we had before clicking.
// Without skipping refs.setReference for in-editor references while
// FloatingFocusManager is active, floating-ui inserts a focus-return
// element into the PM contenteditable, triggering its MutationObserver
// and resetting the selection.
const selAfter = await page.evaluate(() => {
const pm = (window as any).ProseMirror;
return { from: pm.state.selection.from, to: pm.state.selection.to };
});
expect(selAfter.from).toBe(selBefore.from);
expect(selAfter.to).toBe(selBefore.to);
});
});
Loading