diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index 59974a6d60..24566eff0e 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -38,7 +38,19 @@ inset 0 1px 1px 1px var(--shadow); } -.bn-toolbar .bn-ak-popover { +/* Menus and popovers are portalled into the editor container, so they are + siblings of the positioned UI (side menu, toolbars, …) that opened them + instead of descendants, and must stack above it. The UI elements sit at + `--bn-ui-base-z-index` + 10…90 (see the React controllers); Ariakit's own + stylesheet gives these a fixed 50. */ +.bn-ariakit .bn-ak-menu, +.bn-ariakit .bn-ak-popover { + z-index: calc(var(--bn-ui-base-z-index, 0) + 100); +} + +/* Form popovers (link, caption, rename) stack their fields; lists keep the + tighter spacing. */ +.bn-ariakit .bn-form-popover { gap: 0.5rem; } diff --git a/packages/mantine/src/BlockNoteView.browser.test.tsx b/packages/mantine/src/BlockNoteView.browser.test.tsx index 17773950cd..605b971190 100644 --- a/packages/mantine/src/BlockNoteView.browser.test.tsx +++ b/packages/mantine/src/BlockNoteView.browser.test.tsx @@ -94,6 +94,26 @@ function Harness(props: { setup: Setup }) { ); } +/** + * Waits until no commit has landed for a while. Mounting schedules an update + * from an effect that can commit after `act` has returned, and on Linux WebKit + * whether it does varies from mount to mount; measuring only once things have + * settled makes every mount count the same set of commits. + */ +async function settle() { + let last = commits; + for (let i = 0; i < 20; i++) { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + }); + if (commits === last) { + return; + } + last = commits; + } + throw new Error("commits did not settle"); +} + async function mount(setup: Setup, strict: boolean) { editor = BlockNoteEditor.create(); commits = 0; @@ -103,6 +123,7 @@ async function mount(setup: Setup, strict: boolean) { await act(async () => { root!.render(strict ? {tree} : tree); }); + await settle(); if (!container.querySelector(".bn-editor")) { throw new Error("editor did not mount"); } @@ -152,6 +173,7 @@ describe.each([{ strict: false }, { strict: true }])( await act(async () => { rerenderParent(); }); + await settle(); const cost = { commits: commits - before.commits, childRenders: childRenders - before.childRenders, diff --git a/packages/mantine/src/blocknoteStyles.css b/packages/mantine/src/blocknoteStyles.css index de73c5a66b..11408d5a90 100644 --- a/packages/mantine/src/blocknoteStyles.css +++ b/packages/mantine/src/blocknoteStyles.css @@ -216,15 +216,16 @@ on touch devices (e.g. the mobile formatting toolbar). */ height: 40px; } -.bn-toolbar .mantine-Menu-item { +/* Menus are portalled into the editor container, so they can't be styled + through the toolbar, side menu or table handle that opened them; scope on + the dropdowns themselves instead. */ +.bn-mantine .bn-select .mantine-Menu-item, +.bn-mantine .bn-menu-dropdown .mantine-Menu-item, +.bn-mantine .bn-table-handle-menu .mantine-Menu-item { font-size: 12px; height: 30px; } -.bn-toolbar .mantine-Menu-item:hover { - background-color: var(--bn-colors-hovered-background); -} - .bn-mantine .bn-form-popover { background-color: var(--bn-colors-menu-background); border: var(--bn-border); @@ -494,12 +495,6 @@ on touch devices (e.g. the mobile formatting toolbar). */ overflow: visible; } -.bn-side-menu .mantine-Menu-item, -.bn-table-handle-menu .mantine-Menu-item { - font-size: 12px; - height: 30px; -} - .bn-side-menu .mantine-UnstyledButton-root:not(.mantine-Menu-item) { background-color: transparent; } @@ -519,10 +514,8 @@ on touch devices (e.g. the mobile formatting toolbar). */ display: flex; } -.bn-side-menu .mantine-Menu-dropdown { +.bn-mantine .bn-drag-handle-menu { min-width: 100px; - padding: 2px; - position: absolute; } /* Image Panel styling*/ @@ -736,10 +729,6 @@ on touch devices (e.g. the mobile formatting toolbar). */ color: var(--bn-colors-disabled-text); } -.bn-mantine .bn-action-toolbar .mantine-Menu-itemLabel { - font-size: 12px; -} - /* Badge styling */ .bn-mantine .bn-badge-group { display: flex; diff --git a/tests/src/end-to-end/comments/comments.test.tsx b/tests/src/end-to-end/comments/comments.test.tsx index 261040c093..7c82b131ec 100644 --- a/tests/src/end-to-end/comments/comments.test.tsx +++ b/tests/src/end-to-end/comments/comments.test.tsx @@ -9,7 +9,12 @@ import { sleep, waitForSelector, } from "../../utils/editor.js"; -import { clickAt, getRect, moveMouseOverElement } from "../../utils/mouse.js"; +import { + clickAt, + clickElement, + getRect, + moveMouseOverElement, +} from "../../utils/mouse.js"; /** Double-clicks the centre of an element via the real Playwright mouse. */ async function doubleClickElement(element: Element) { @@ -123,7 +128,9 @@ describe("Check Comments functionality", () => { await expectElement( await waitForSelector(LINK_BUTTON_SELECTOR), ).toBeVisible(); - await userEvent.click(await waitForSelector(LINK_BUTTON_SELECTOR)); + // A real press, not `userEvent.click`: the popover's input must win the + // focus race against the toolbar's focus trap (see `clickElement`). + await clickElement(await waitForSelector(LINK_BUTTON_SELECTOR)); await userEvent.keyboard("https://example.com"); await userEvent.keyboard("{Enter}"); @@ -154,7 +161,8 @@ describe("Check Comments functionality", () => { await expectElement( await waitForSelector(LINK_BUTTON_SELECTOR), ).toBeVisible(); - await userEvent.click(await waitForSelector(LINK_BUTTON_SELECTOR)); + // A real press, not `userEvent.click` (see `clickElement`). + await clickElement(await waitForSelector(LINK_BUTTON_SELECTOR)); await userEvent.keyboard("https://example.com"); await userEvent.keyboard("{Enter}"); diff --git a/tests/src/end-to-end/theming/theming.test.tsx b/tests/src/end-to-end/theming/theming.test.tsx index 3c5d2dfebb..24108cfadf 100644 --- a/tests/src/end-to-end/theming/theming.test.tsx +++ b/tests/src/end-to-end/theming/theming.test.tsx @@ -14,7 +14,11 @@ import { sleep, waitForSelector, } from "../../utils/editor.js"; -import { moveMouseOverElement, mouseSequence } from "../../utils/mouse.js"; +import { + clickElement, + moveMouseOverElement, + mouseSequence, +} from "../../utils/mouse.js"; import { executeSlashCommand } from "../../utils/slashmenu.js"; // Vitest browser mode has no per-test `colorScheme` knob (the playwright @@ -67,7 +71,9 @@ describe("Check Dark Theme is Automatically Applied", () => { await userEvent.keyboard("Paragraph"); await userEvent.keyboard("{Shift>}{Home}{/Shift}"); - await userEvent.click(await waitForSelector(LINK_BUTTON_SELECTOR)); + // A real press, not `userEvent.click`: the popover's input must win the + // focus race against the toolbar's focus trap (see `clickElement`). + await clickElement(await waitForSelector(LINK_BUTTON_SELECTOR)); await sleep(500); await userEvent.keyboard("link"); diff --git a/tests/src/utils/const.ts b/tests/src/utils/const.ts index 93d7f13b73..54d8165968 100644 --- a/tests/src/utils/const.ts +++ b/tests/src/utils/const.ts @@ -18,7 +18,9 @@ export const TABLE_SELECTOR = `[data-content-type="table"]`; export const DRAG_HANDLE_SELECTOR = `[data-test="dragHandle"]`; export const DRAG_HANDLE_ADD_SELECTOR = `[data-test="dragHandleAdd"]`; -export const DRAG_HANDLE_MENU_SELECTOR = `.bn-side-menu > .bn-menu-dropdown`; +// The menu is portalled into the editor container, so it is not a descendant +// of the side menu that opens it; match it by its own class. +export const DRAG_HANDLE_MENU_SELECTOR = `.bn-drag-handle-menu`; export const SLASH_MENU_SELECTOR = `.bn-suggestion-menu`; export const EMOJI_PICKER_SELECTOR = `.bn-grid-suggestion-menu`; diff --git a/tests/src/utils/mouse.ts b/tests/src/utils/mouse.ts index 792d15fe0a..9ccb32055a 100644 --- a/tests/src/utils/mouse.ts +++ b/tests/src/utils/mouse.ts @@ -45,9 +45,33 @@ export function mouseSequence(actions: MouseAction[]): Promise { return runMouse(actions); } -/** Single (or multi-) click at iframe-relative coordinates. */ -export function clickAt(x: number, y: number, clickCount = 1): Promise { - return runMouse([{ type: "click", x, y, clickCount }]); +/** + * Single (or multi-) click at iframe-relative coordinates. `delay` is the time + * the button is held between mousedown and mouseup (Playwright's default is 0). + */ +export function clickAt( + x: number, + y: number, + clickCount = 1, + delay?: number, +): Promise { + return runMouse([{ type: "click", x, y, clickCount, delay }]); +} + +/** + * Clicks the centre of an element with the real mouse, holding the button for + * `delay` ms. Use this instead of `userEvent.click` where an instantaneous + * press behaves differently from a human one: a toolbar button that opens a + * popover arms the toolbar's focus trap on mousedown, and with a 0 ms press + * the trap's deferred focus move lands after the popover's input has taken + * focus, stealing it back. Any real press (≥ ~40 ms) lets the input win. + */ +export function clickElement( + selectorOrElement: string | Element, + { delay = 50 }: { delay?: number } = {}, +): Promise { + const { x, y } = center(getRect(selectorOrElement)); + return clickAt(x, y, 1, delay); } /** Moves the mouse to the centre of an element (e.g. to reveal hover UI). */ diff --git a/tests/src/utils/positionalMouse.ts b/tests/src/utils/positionalMouse.ts index adc3e63ce4..1b52093361 100644 --- a/tests/src/utils/positionalMouse.ts +++ b/tests/src/utils/positionalMouse.ts @@ -23,7 +23,14 @@ export type MouseAction = | { type: "move"; x: number; y: number; steps?: number } | { type: "down" } | { type: "up" } - | { type: "click"; x: number; y: number; clickCount?: number }; + | { + type: "click"; + x: number; + y: number; + clickCount?: number; + /** Time between mousedown and mouseup, like a real press (default 0). */ + delay?: number; + }; /** * Browser-side signature of the {@link positionalMouse} command below, i.e. what @@ -74,6 +81,7 @@ export const positionalMouse: BrowserCommand = async ( case "click": await page.mouse.click(offsetX + action.x, offsetY + action.y, { clickCount: action.clickCount ?? 1, + delay: action.delay, }); break; }