Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FC, useCallback, useEffect } from "react";

import { useBlockNoteContext } from "../../../editor/BlockNoteContext.js";
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
import { getSuggestionMenuItemId } from "../getSuggestionMenuItemId.js";
import { useCloseSuggestionMenuNoItems } from "../hooks/useCloseSuggestionMenuNoItems.js";
import { useLoadSuggestionMenuItems } from "../hooks/useLoadSuggestionMenuItems.js";
import { useGridSuggestionMenuKeyboardNavigation } from "./hooks/useGridSuggestionMenuKeyboardNavigation.js";
Expand Down Expand Up @@ -79,9 +80,7 @@ export function GridSuggestionMenuWrapper<Item>(props: {
useEffect(() => {
setContentEditableProps((p) => ({
...p,
"aria-activedescendant": selectedIndex
? "bn-suggestion-menu-item-" + selectedIndex
: undefined,
"aria-activedescendant": getSuggestionMenuItemId(selectedIndex),
}));
return () => {
setContentEditableProps((p) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, it } from "vite-plus/test";
import { getSuggestionMenuItemId } from "./getSuggestionMenuItemId.js";
import { SuggestionMenuController } from "./SuggestionMenuController.js";

it("has good typing", () => {
Expand Down Expand Up @@ -40,3 +41,9 @@ it("has good typing", () => {

expect(menu).toBeDefined();
});

it("returns an active descendant id for the first suggestion", () => {
expect(getSuggestionMenuItemId(0)).toBe("bn-suggestion-menu-item-0");
expect(getSuggestionMenuItemId(2)).toBe("bn-suggestion-menu-item-2");
expect(getSuggestionMenuItemId(undefined)).toBeUndefined();
});
Comment on lines +44 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

target="packages/react/src/components/SuggestionMenu/SuggestionMenu.test.tsx"

printf '%s\n' '--- target file map ---'
ast-grep outline "$target" --view expanded

printf '%s\n' '--- target test file ---'
cat -n "$target"

printf '%s\n' '--- related SuggestionMenu files ---'
fd -i 'SuggestionMenu' packages/react/src/components | sort

printf '%s\n' '--- wrapper definitions and usages ---'
rg -n -C 4 'SuggestionMenuWrapper|GridSuggestionMenuWrapper|aria-activedescendant|getSuggestionMenuItemId' packages/react/src

Repository: TypeCellOS/BlockNote

Length of output: 20042


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- all active-descendant tests and wrapper tests ---'
rg -n -C 5 'aria-activedescendant|SuggestionMenuWrapper|GridSuggestionMenuWrapper' \
  --glob '*.{test,spec}.{ts,tsx,js,jsx}' \
  --glob '!node_modules' .

printf '%s\n' '--- SuggestionMenuWrapper implementation ---'
cat -n packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx

printf '%s\n' '--- GridSuggestionMenuWrapper implementation ---'
cat -n packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuWrapper.tsx

printf '%s\n' '--- test configuration and React test utilities ---'
rg -n -C 3 'vite-plus/test|vitest|testing-library|render\(' \
  --glob '*.{json,js,jsx,ts,tsx,mjs,cjs}' \
  packages/react package.json pnpm-workspace.yaml

Repository: TypeCellOS/BlockNote

Length of output: 212


🏁 Script executed:

#!/bin/bash

printf '%s\n' '--- all active-descendant tests and wrapper tests ---'
rg -n -C 5 'aria-activedescendant|SuggestionMenuWrapper|GridSuggestionMenuWrapper' \
  --glob '*.{test,spec}.{ts,tsx,js,jsx}' \
  --glob '!node_modules' . || true

printf '%s\n' '--- SuggestionMenuWrapper implementation ---'
cat -n packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx

printf '%s\n' '--- GridSuggestionMenuWrapper implementation ---'
cat -n packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuWrapper.tsx

printf '%s\n' '--- test configuration and React test utilities ---'
rg -n -C 3 'vite-plus/test|vitest|testing-library|render\(' \
  --glob '*.{json,js,jsx,ts,tsx,mjs,cjs}' \
  packages/react package.json pnpm-workspace.yaml || true

Repository: TypeCellOS/BlockNote

Length of output: 11649


🏁 Script executed:

#!/bin/bash

printf '%s\n' '--- keyboard navigation hooks ---'
cat -n packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts
cat -n packages/react/src/components/SuggestionMenu/GridSuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts

printf '%s\n' '--- package test dependencies and scripts ---'
rg -n -C 3 '"(test|react-testing-library|testing-library|jsdom|vite-plus)"|scripts:' \
  packages/react/package.json package.json pnpm-workspace.yaml

printf '%s\n' '--- read-only coverage verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

root = Path(".")
test_files = [
    p for p in root.rglob("*")
    if p.is_file()
    and p.suffix in {".ts", ".tsx", ".js", ".jsx"}
    and (".test." in p.name or ".spec." in p.name)
    and "node_modules" not in p.parts
]

terms = {
    "SuggestionMenuWrapper": re.compile(r"\bSuggestionMenuWrapper\b"),
    "GridSuggestionMenuWrapper": re.compile(r"\bGridSuggestionMenuWrapper\b"),
    "aria-activedescendant": re.compile(r"aria-activedescendant"),
}

for name, pattern in terms.items():
    matches = []
    for path in test_files:
        text = path.read_text(errors="replace")
        if pattern.search(text):
            matches.append(str(path))
    print(f"{name}: {len(matches)} matching test files")
    for path in matches:
        print(f"  {path}")
PY

Repository: TypeCellOS/BlockNote

Length of output: 6723


Add wrapper-level aria-activedescendant coverage

No test file covers either wrapper or this attribute. Add tests for index 0, a later index, undefined, and cleanup through both wrappers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/components/SuggestionMenu/SuggestionMenu.test.tsx` around
lines 44 - 49, Extend the SuggestionMenu tests around getSuggestionMenuItemId to
cover wrapper-level aria-activedescendant behavior for both wrapper components.
Verify index 0, a later index, and undefined, and confirm the attribute is
removed during cleanup through each wrapper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added direct wrapper coverage in 00624ef. The tests render both the list and grid wrappers, exercise index 0, a later index, and undefined, and verify unmount cleanup clears aria-activedescendant. The full @blocknote/react test and lint commands pass locally.

Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from "vite-plus/test";

import { GridSuggestionMenuWrapper } from "./GridSuggestionMenu/GridSuggestionMenuWrapper.js";
import { SuggestionMenuWrapper } from "./SuggestionMenuWrapper.js";

const mocks = vi.hoisted(() => ({
selectedIndex: undefined as number | undefined,
setContentEditableProps: vi.fn(),
}));

vi.mock("../../editor/BlockNoteContext.js", () => ({
useBlockNoteContext: () => ({
setContentEditableProps: mocks.setContentEditableProps,
}),
}));

vi.mock("../../hooks/useBlockNoteEditor.js", () => ({
useBlockNoteEditor: () => ({}),
}));

vi.mock("./hooks/useLoadSuggestionMenuItems.js", () => ({
useLoadSuggestionMenuItems: () => ({
items: ["first", "second", "third"],
usedQuery: "",
loadingState: "loaded",
}),
}));

vi.mock("./hooks/useCloseSuggestionMenuNoItems.js", () => ({
useCloseSuggestionMenuNoItems: () => undefined,
}));

vi.mock("./hooks/useSuggestionMenuKeyboardNavigation.js", () => ({
useSuggestionMenuKeyboardNavigation: () => ({
selectedIndex: mocks.selectedIndex,
}),
}));

vi.mock(
"./GridSuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.js",
() => ({
useGridSuggestionMenuKeyboardNavigation: () => ({
selectedIndex: mocks.selectedIndex,
}),
}),
);

type ContentEditableProps = {
"aria-activedescendant"?: string;
"aria-controls"?: string;
"aria-expanded"?: boolean;
};

let container: HTMLDivElement;
let root: Root;
let contentEditableProps: ContentEditableProps;

beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
contentEditableProps = {};
mocks.selectedIndex = undefined;
mocks.setContentEditableProps.mockImplementation(
(update: (props: ContentEditableProps) => ContentEditableProps) => {
contentEditableProps = update(contentEditableProps);
},
);
});

afterEach(async () => {
await act(async () => root.unmount());
document.body.removeChild(container);
vi.clearAllMocks();
});

function Menu() {
return null;
}

function renderWrapper(type: "list" | "grid") {
const commonProps = {
query: "",
closeMenu: vi.fn(),
clearQuery: vi.fn(),
getItems: async () => ["first", "second", "third"],
};

return act(async () => {
root.render(
type === "list" ? (
<SuggestionMenuWrapper
{...commonProps}
suggestionMenuComponent={Menu}
/>
) : (
<GridSuggestionMenuWrapper
{...commonProps}
columns={2}
gridSuggestionMenuComponent={Menu}
/>
),
);
});
}

describe.each(["list", "grid"] as const)(
"%s suggestion menu wrapper",
(type) => {
it("updates and clears aria-activedescendant", async () => {
mocks.selectedIndex = 0;
await renderWrapper(type);
expect(contentEditableProps["aria-activedescendant"]).toBe(
"bn-suggestion-menu-item-0",
);

mocks.selectedIndex = 2;
await renderWrapper(type);
expect(contentEditableProps["aria-activedescendant"]).toBe(
"bn-suggestion-menu-item-2",
);

mocks.selectedIndex = undefined;
await renderWrapper(type);
expect(contentEditableProps["aria-activedescendant"]).toBeUndefined();

mocks.selectedIndex = 0;
await renderWrapper(type);
await act(async () => root.unmount());
expect(contentEditableProps["aria-activedescendant"]).toBeUndefined();
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FC, useCallback, useEffect } from "react";

import { useBlockNoteContext } from "../../editor/BlockNoteContext.js";
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { getSuggestionMenuItemId } from "./getSuggestionMenuItemId.js";
import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems.js";
import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems.js";
import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKeyboardNavigation.js";
Expand Down Expand Up @@ -76,9 +77,7 @@ export function SuggestionMenuWrapper<Item>(props: {
useEffect(() => {
setContentEditableProps((p) => ({
...p,
"aria-activedescendant": selectedIndex
? "bn-suggestion-menu-item-" + selectedIndex
: undefined,
"aria-activedescendant": getSuggestionMenuItemId(selectedIndex),
}));
return () => {
setContentEditableProps((p) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function getSuggestionMenuItemId(selectedIndex: number | undefined) {
return selectedIndex !== undefined
? "bn-suggestion-menu-item-" + selectedIndex
: undefined;
}
Loading