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
104 changes: 104 additions & 0 deletions apps/app/src/components/settings/KeyboardSettingsSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,110 @@ afterEach(() => {
});

describe("KeyboardSettingsSection", () => {
it("ranks visible label matches ahead of description matches across groups", () => {
render(<KeyboardSettingsSection />);

fireEvent.change(
screen.getByRole("textbox", { name: "Search keyboard shortcuts" }),
{
target: { value: " SiDeBaR " },
},
);

expect(
screen
.getAllByRole("button", { name: /^Record shortcut for / })[0]
?.getAttribute("aria-label"),
).toMatch(/^Record shortcut for Toggle sidebar,/);
});

it("ranks exact labels, label prefixes, label substrings, IDs, and descriptions in order", () => {
setPluginSlotRegistrations(
"test-shortcuts",
collectPluginAppRegistrations({
__bbPluginApp: true,
setup(app) {
for (const [id, title] of [
["sidebar-open", "Open navigation"],
["tools", "Sidebar tools"],
["exact", "Sidebar"],
["settings", "Sidebar settings"],
]) {
app.commands.register({ id, title, run() {} });
}
},
}),
);
render(<KeyboardSettingsSection />);

fireEvent.change(
screen.getByRole("textbox", { name: "Search keyboard shortcuts" }),
{
target: { value: "sidebar" },
},
);

const labels = screen
.getAllByRole("button", { name: /^Record shortcut for / })
.map(
(recorder) =>
recorder.getAttribute("aria-label")?.split(", current shortcut")[0],
);
expect(labels.slice(0, 6)).toEqual([
"Record shortcut for Sidebar",
"Record shortcut for Sidebar tools",
"Record shortcut for Sidebar settings",
"Record shortcut for Toggle sidebar",
"Record shortcut for Open navigation",
"Record shortcut for Previous thread",
]);
});

it("restores category and command order when the search is cleared", () => {
render(<KeyboardSettingsSection />);
const originalLabels = screen
.getAllByRole("button", { name: /^Record shortcut for / })
.map((recorder) => recorder.getAttribute("aria-label"));
const originalHeadings = screen
.getAllByRole("heading", { level: 3 })
.map((heading) => heading.textContent);
const search = screen.getByRole("textbox", {
name: "Search keyboard shortcuts",
});

fireEvent.change(search, { target: { value: "sidebar" } });
fireEvent.change(search, { target: { value: " " } });

expect(
screen
.getAllByRole("button", { name: /^Record shortcut for / })
.map((recorder) => recorder.getAttribute("aria-label")),
).toEqual(originalLabels);
expect(
screen
.getAllByRole("heading", { level: 3 })
.map((heading) => heading.textContent),
).toEqual(originalHeadings);
});

it("shows the empty state when no shortcut matches", () => {
render(<KeyboardSettingsSection />);

fireEvent.change(
screen.getByRole("textbox", { name: "Search keyboard shortcuts" }),
{
target: { value: "nonexistent-shortcut" },
},
);

expect(
screen.getByText("No shortcuts match “nonexistent-shortcut”."),
).toBeDefined();
expect(
screen.queryAllByRole("button", { name: /^Record shortcut for / }),
).toHaveLength(0);
});

it("lists commands without defaults and persists bindings under stable plugin IDs", () => {
setPluginSlotRegistrations(
"test-shortcuts",
Expand Down
27 changes: 18 additions & 9 deletions apps/app/src/components/settings/KeyboardSettingsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -552,15 +552,24 @@ export function KeyboardSettingsSection() {
const visibleGroups = useMemo(() => {
const query = search.trim().toLowerCase();
if (query.length === 0) return commandGroups;
return commandGroups.flatMap((group) => {
const commands = group.commands.filter(
(metadata) =>
metadata.label.toLowerCase().includes(query) ||
metadata.description.toLowerCase().includes(query) ||
metadata.command.toLowerCase().includes(query),
);
return commands.length === 0 ? [] : [{ ...group, commands }];
});
const commands = commandGroups
.flatMap((group) =>
group.commands.map((metadata) => {
const label = metadata.label.toLowerCase();
const rank = [
label === query,
label.startsWith(query),
label.includes(query),
metadata.command.toLowerCase().includes(query),
metadata.description.toLowerCase().includes(query),
].findIndex(Boolean);
return { metadata, rank };
}),
)
.filter(({ rank }) => rank !== -1)
.sort((left, right) => left.rank - right.rank)
.map(({ metadata }) => metadata);
return commands.length === 0 ? [] : [{ label: "Search results", commands }];
}, [commandGroups, search]);

const latestSettingsRef = useRef({
Expand Down
Loading