Skip to content

Commit ce0b9ca

Browse files
committed
fix(multi-select): restore keyboard navigation for filterable variant (#2314)
Fixes #2313
1 parent 148deff commit ce0b9ca

File tree

2 files changed

+101
-5
lines changed

2 files changed

+101
-5
lines changed

src/MultiSelect/MultiSelect.svelte

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,10 @@
403403
} else if (key === "Tab") {
404404
open = false;
405405
} else if (key === "ArrowDown") {
406+
if (!open) open = true;
406407
change(1);
407408
} else if (key === "ArrowUp") {
409+
if (!open) open = true;
408410
change(-1);
409411
} else if (key === "Escape") {
410412
open = false;
@@ -413,6 +415,9 @@
413415
}
414416
}}
415417
on:input
418+
on:input={() => {
419+
if (!open) open = true;
420+
}}
416421
on:keyup
417422
on:focus
418423
on:blur
@@ -471,14 +476,12 @@
471476
if (key === " ") {
472477
open = !open;
473478
} else if (key === "Tab") {
474-
if (selectionRef && checked.length > 0) {
475-
selectionRef.focus();
476-
} else {
477-
open = false;
478-
}
479+
open = false;
479480
} else if (key === "ArrowDown") {
481+
if (!open) open = true;
480482
change(1);
481483
} else if (key === "ArrowUp") {
484+
if (!open) open = true;
482485
change(-1);
483486
} else if (key === "Enter") {
484487
if (highlightedIndex > -1) {

tests/MultiSelect/MultiSelect.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,4 +651,97 @@ describe("MultiSelect", () => {
651651
expect(options[2]).toHaveAttribute("aria-selected", "true");
652652
expect(input).toHaveFocus();
653653
});
654+
655+
// Regression test for https://github.com/carbon-design-system/carbon-components-svelte/issues/2313
656+
describe("keyboard navigation (issue #2313)", () => {
657+
it("filterable: menu opens when starting to type after Tab focus", async () => {
658+
render(MultiSelect, {
659+
props: {
660+
items,
661+
filterable: true,
662+
placeholder: "Filter...",
663+
},
664+
});
665+
666+
const input = screen.getByPlaceholderText("Filter...");
667+
668+
// Simulate tabbing into the field
669+
input.focus();
670+
671+
// Menu doesn't need to open immediately on focus for filterable variant
672+
// but should open when user starts typing
673+
await user.type(input, "s");
674+
675+
// Menu should now be open
676+
expect(input).toHaveAttribute("aria-expanded", "true");
677+
expect(screen.getByRole("listbox")).toBeInTheDocument();
678+
});
679+
680+
it("filterable: accepts keyboard input after tabbing into field", async () => {
681+
render(MultiSelect, {
682+
props: {
683+
items,
684+
filterable: true,
685+
placeholder: "Filter...",
686+
},
687+
});
688+
689+
const input = screen.getByPlaceholderText("Filter...");
690+
691+
// Simulate tabbing into the field
692+
input.focus();
693+
694+
// Should be able to type immediately
695+
await user.type(input, "slack");
696+
expect(input).toHaveValue("slack");
697+
698+
// Filtered results should be shown
699+
expect(screen.getByText("Slack")).toBeInTheDocument();
700+
expect(screen.queryByText("Email")).not.toBeInTheDocument();
701+
});
702+
703+
it("filterable: Tab key does not close menu when navigating", async () => {
704+
render(MultiSelect, {
705+
props: {
706+
items,
707+
filterable: true,
708+
placeholder: "Filter...",
709+
},
710+
});
711+
712+
const input = screen.getByPlaceholderText("Filter...");
713+
await user.click(input);
714+
715+
// Menu should be open
716+
expect(input).toHaveAttribute("aria-expanded", "true");
717+
718+
// Press Tab - menu should close to allow natural tab navigation
719+
await user.keyboard("{Tab}");
720+
721+
// Menu should close when Tab is pressed to move focus away
722+
expect(input).toHaveAttribute("aria-expanded", "false");
723+
});
724+
725+
it("filterable: focus should go to input, not clear button when items selected", async () => {
726+
render(MultiSelect, {
727+
props: {
728+
items,
729+
filterable: true,
730+
placeholder: "Filter...",
731+
selectedIds: ["0"],
732+
},
733+
});
734+
735+
const input = screen.getByPlaceholderText("Filter...");
736+
737+
// Simulate tabbing into the field
738+
input.focus();
739+
740+
// Input should have focus, not the clear button
741+
expect(input).toHaveFocus();
742+
743+
const clearButton = screen.getAllByRole("button", { name: /clear/i })[0];
744+
expect(clearButton).not.toHaveFocus();
745+
});
746+
});
654747
});

0 commit comments

Comments
 (0)