mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 01:19:42 +02:00
* fix(search): stop Home/End propagation in command palette inputs cmdk's root CommandPrimitive captures Home/End at the container level for list navigation (jumping to first/last item). When the search input is focused those keys never reach the native text-input handler, so the text caret does not move to the beginning/end of the query. Fix: add an onKeyDown handler on CommandPrimitive.Input that calls stopPropagation() for Home and End before the event reaches cmdk's root listener. The existing caller-supplied onKeyDown is still called so callers can layer their own handlers. Applied in two places: - packages/ui/components/ui/command.tsx (CommandInput wrapper — generic command palette used across the app) - packages/views/search/search-command.tsx (SearchCommand — Ctrl+K global search palette; uses CommandPrimitive.Input directly) Fixes #5655 * test(search): cover Home/End caret handling in command palette inputs Add regression coverage for the Home/End fix (MUL-5260, PR #5870): - search-command.test.tsx: Home/End move the query caret and do NOT hijack cmdk result selection, while ArrowUp/ArrowDown still navigate. - command-input.test.tsx: the shared CommandInput stops Home/End from bubbling to cmdk's root handler while still forwarding every key to a caller-provided onKeyDown; other keys keep bubbling. Lives in @multica/views because @multica/ui has no test runner. Verified as a genuine guard: reverting the fix hunks fails exactly these new assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: NevilleQingNY <nevilleqing@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { Command, CommandInput } from "@multica/ui/components/ui/command";
|
|
|
|
// Contract for the shared CommandInput: Home/End must stop propagating to
|
|
// cmdk's root keydown handler (which otherwise hijacks them to jump the result
|
|
// list to first/last), while every key — including Home/End — is still handed
|
|
// to a caller-provided onKeyDown. cmdk lives in @multica/ui, which has no test
|
|
// runner, so the contract is pinned here alongside the SearchCommand
|
|
// regression that depends on it.
|
|
describe("CommandInput", () => {
|
|
const renderInput = () => {
|
|
// An ancestor listener outside <Command> is the propagation probe: it only
|
|
// fires if the keydown bubbled past the input (i.e. past cmdk's root).
|
|
const ancestorKeyDown = vi.fn();
|
|
const callerKeyDown = vi.fn();
|
|
render(
|
|
<div onKeyDown={ancestorKeyDown}>
|
|
<Command>
|
|
<CommandInput placeholder="search" onKeyDown={callerKeyDown} />
|
|
</Command>
|
|
</div>,
|
|
);
|
|
return { ancestorKeyDown, callerKeyDown };
|
|
};
|
|
|
|
it.each(["{Home}", "{End}"])(
|
|
"stops %s from bubbling past the input while still calling the caller onKeyDown",
|
|
async (key) => {
|
|
const user = userEvent.setup();
|
|
const { ancestorKeyDown, callerKeyDown } = renderInput();
|
|
|
|
await user.click(screen.getByPlaceholderText("search"));
|
|
await user.keyboard(key);
|
|
|
|
expect(callerKeyDown).toHaveBeenCalledTimes(1);
|
|
expect(ancestorKeyDown).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("lets other keys bubble so cmdk list navigation keeps working", async () => {
|
|
const user = userEvent.setup();
|
|
const { ancestorKeyDown, callerKeyDown } = renderInput();
|
|
|
|
await user.click(screen.getByPlaceholderText("search"));
|
|
await user.keyboard("{ArrowDown}");
|
|
|
|
// Not Home/End, so the interception must not apply: the event reaches the
|
|
// ancestor (and thus cmdk's root), and the caller callback still runs.
|
|
expect(callerKeyDown).toHaveBeenCalledTimes(1);
|
|
expect(ancestorKeyDown).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|