Files
multica/packages/views/search/command-input.test.tsx
Rusty Raven 485f0f3124 fix(search): stop Home/End propagation in command palette inputs (#5870)
* 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>
2026-07-24 17:03:36 +08:00

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);
});
});