Skip to content

Commit e7808bc

Browse files
committed
fix(multi-select): restore complete keyboard navigation for filterable variant (#2317)
Fixes #2313
1 parent e4b5c43 commit e7808bc

File tree

4 files changed

+44
-28
lines changed

4 files changed

+44
-28
lines changed

src/ListBox/ListBoxSelection.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
<div
6868
bind:this={ref}
6969
role="button"
70-
tabindex={disabled ? -1 : 0}
70+
tabindex="-1"
7171
class:bx--tag__close-icon={true}
7272
on:click|preventDefault|stopPropagation={(e) => {
7373
if (!disabled) {
@@ -92,7 +92,7 @@
9292
role="button"
9393
aria-label={description}
9494
title={description}
95-
tabindex={disabled ? "-1" : "0"}
95+
tabindex="-1"
9696
class:bx--list-box__selection={true}
9797
class:bx--tag--filter={selectionCount}
9898
class:bx--list-box__selection--multi={selectionCount}

src/MultiSelect/MultiSelect.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@
420420
}}
421421
on:keyup
422422
on:focus
423+
on:focus={() => {
424+
if (disabled) return;
425+
open = true;
426+
}}
423427
on:blur
424428
on:paste
425429
{disabled}

tests/ListBox/ListBoxSelection.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ describe("ListBoxSelection", () => {
2727
expect(button).toHaveAttribute("tabindex", "-1");
2828
});
2929

30-
it("should handle enabled state for single selection", () => {
30+
it("should not be in tab order (accessibility fix)", () => {
3131
render(ListBoxSelection, { props: { disabled: false } });
3232

3333
const button = screen.getByRole("button");
34-
expect(button).toHaveAttribute("tabindex", "0");
34+
expect(button).toHaveAttribute("tabindex", "-1");
3535
});
3636

3737
it("should handle disabled state for multi selection", () => {

tests/MultiSelect/MultiSelect.test.ts

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ describe("MultiSelect", () => {
654654

655655
// Regression test for https://github.com/carbon-design-system/carbon-components-svelte/issues/2313
656656
describe("keyboard navigation (issue #2313)", () => {
657-
it("filterable: menu opens when starting to type after Tab focus", async () => {
657+
it("filterable: menu opens when tabbing into field", async () => {
658658
render(MultiSelect, {
659659
props: {
660660
items,
@@ -665,19 +665,14 @@ describe("MultiSelect", () => {
665665

666666
const input = screen.getByPlaceholderText("Filter...");
667667

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");
668+
await user.tab();
669+
expect(input).toHaveFocus();
674670

675-
// Menu should now be open
676671
expect(input).toHaveAttribute("aria-expanded", "true");
677672
expect(screen.getByRole("listbox")).toBeInTheDocument();
678673
});
679674

680-
it("filterable: accepts keyboard input after tabbing into field", async () => {
675+
it("filterable: first character shows correctly after tabbing", async () => {
681676
render(MultiSelect, {
682677
props: {
683678
items,
@@ -688,14 +683,15 @@ describe("MultiSelect", () => {
688683

689684
const input = screen.getByPlaceholderText("Filter...");
690685

691-
// Simulate tabbing into the field
692-
input.focus();
686+
await user.tab();
687+
expect(input).toHaveFocus();
693688

694-
// Should be able to type immediately
695-
await user.type(input, "slack");
689+
await user.type(input, "s");
690+
expect(input).toHaveValue("s");
691+
692+
await user.type(input, "lack");
696693
expect(input).toHaveValue("slack");
697694

698-
// Filtered results should be shown
699695
expect(screen.getByText("Slack")).toBeInTheDocument();
700696
expect(screen.queryByText("Email")).not.toBeInTheDocument();
701697
});
@@ -711,18 +707,13 @@ describe("MultiSelect", () => {
711707

712708
const input = screen.getByPlaceholderText("Filter...");
713709
await user.click(input);
714-
715-
// Menu should be open
716710
expect(input).toHaveAttribute("aria-expanded", "true");
717711

718-
// Press Tab - menu should close to allow natural tab navigation
719712
await user.keyboard("{Tab}");
720-
721-
// Menu should close when Tab is pressed to move focus away
722713
expect(input).toHaveAttribute("aria-expanded", "false");
723714
});
724715

725-
it("filterable: focus should go to input, not clear button when items selected", async () => {
716+
it("filterable: focus goes to input, not clear button", async () => {
726717
render(MultiSelect, {
727718
props: {
728719
items,
@@ -733,15 +724,36 @@ describe("MultiSelect", () => {
733724
});
734725

735726
const input = screen.getByPlaceholderText("Filter...");
727+
const clearButton = screen.getAllByRole("button", { name: /clear/i })[0];
736728

737-
// Simulate tabbing into the field
738-
input.focus();
729+
expect(clearButton).toHaveAttribute("tabindex", "-1");
730+
await user.tab();
739731

740-
// Input should have focus, not the clear button
741732
expect(input).toHaveFocus();
733+
expect(clearButton).not.toHaveFocus();
734+
expect(input).toHaveAttribute("aria-expanded", "true");
735+
});
742736

737+
it("filterable: clear button is not keyboard accessible but works with mouse", async () => {
738+
render(MultiSelect, {
739+
props: {
740+
items,
741+
filterable: true,
742+
placeholder: "Filter...",
743+
selectedIds: ["0", "1"],
744+
},
745+
});
746+
747+
const input = screen.getByPlaceholderText("Filter...");
743748
const clearButton = screen.getAllByRole("button", { name: /clear/i })[0];
744-
expect(clearButton).not.toHaveFocus();
749+
750+
expect(clearButton).toHaveAttribute("tabindex", "-1");
751+
await user.click(clearButton);
752+
753+
await user.click(input);
754+
const options = screen.getAllByRole("option");
755+
expect(options[0]).toHaveAttribute("aria-selected", "false");
756+
expect(options[1]).toHaveAttribute("aria-selected", "false");
745757
});
746758
});
747759
});

0 commit comments

Comments
 (0)