Skip to content

Commit 2fc884c

Browse files
authored
fix(combo-box): address accessibility issues (#2186)
Supports #2172
1 parent 211885b commit 2fc884c

File tree

2 files changed

+49
-57
lines changed

2 files changed

+49
-57
lines changed

src/ComboBox/ComboBox.svelte

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@
116116
import WarningFilled from "../icons/WarningFilled.svelte";
117117
import WarningAltFilled from "../icons/WarningAltFilled.svelte";
118118
import ListBox from "../ListBox/ListBox.svelte";
119-
import ListBoxField from "../ListBox/ListBoxField.svelte";
120119
import ListBoxMenu from "../ListBox/ListBoxMenu.svelte";
121120
import ListBoxMenuIcon from "../ListBox/ListBoxMenuIcon.svelte";
122121
import ListBoxMenuItem from "../ListBox/ListBoxMenuItem.svelte";
@@ -254,22 +253,12 @@
254253
{warn}
255254
{warnText}
256255
>
257-
<ListBoxField
258-
role="button"
259-
aria-expanded={open}
260-
on:click={async () => {
261-
if (disabled) return;
262-
open = true;
263-
await tick();
264-
ref.focus();
265-
}}
266-
{id}
267-
{disabled}
268-
{translateWithId}
269-
>
256+
<div class:bx--list-box__field={true}>
270257
<input
271258
bind:this={ref}
272259
bind:value
260+
type="text"
261+
role="combobox"
273262
tabindex="0"
274263
autocomplete="off"
275264
aria-autocomplete="list"
@@ -287,6 +276,10 @@
287276
class:bx--text-input={true}
288277
class:bx--text-input--light={light}
289278
class:bx--text-input--empty={value === ""}
279+
on:click={() => {
280+
if (disabled) return;
281+
open = true;
282+
}}
290283
on:input={({ target }) => {
291284
if (!open && target.value.length > 0) {
292285
open = true;
@@ -384,7 +377,7 @@
384377
{translateWithId}
385378
{open}
386379
/>
387-
</ListBoxField>
380+
</div>
388381
{#if open}
389382
<ListBoxMenu aria-label={ariaLabel} {id} on:scroll bind:ref={listRef}>
390383
{#each filteredItems as item, i (item.id)}

tests/ComboBox/ComboBox.test.ts

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ComboBox from "./ComboBox.test.svelte";
44
import ComboBoxCustom from "./ComboBoxCustom.test.svelte";
55

66
describe("ComboBox", () => {
7+
const getInput = () => screen.getByRole("combobox");
78
const getClearButton = () =>
89
screen.getByRole("button", { name: "Clear selected item" });
910

@@ -15,15 +16,13 @@ describe("ComboBox", () => {
1516
render(ComboBox);
1617

1718
expect(screen.getByText("Contact")).toBeInTheDocument();
18-
const input = screen.getByRole("textbox");
19-
expect(input).toHaveAttribute("placeholder", "Select contact method");
19+
expect(getInput()).toHaveAttribute("placeholder", "Select contact method");
2020
});
2121

2222
it("should open menu on click", async () => {
2323
render(ComboBox);
2424

25-
const input = screen.getByRole("textbox");
26-
await user.click(input);
25+
await user.click(getInput());
2726

2827
const dropdown = screen.getAllByRole("listbox")[1];
2928
expect(dropdown).toBeVisible();
@@ -33,20 +32,20 @@ describe("ComboBox", () => {
3332
const consoleLog = vi.spyOn(console, "log");
3433
render(ComboBox);
3534

36-
await user.click(screen.getByRole("textbox"));
35+
await user.click(getInput());
3736
await user.click(screen.getByText("Email"));
3837

3938
expect(consoleLog).toHaveBeenCalledWith("select", {
4039
selectedId: "1",
4140
selectedItem: { id: "1", text: "Email" },
4241
});
43-
expect(screen.getByRole("textbox")).toHaveValue("Email");
42+
expect(getInput()).toHaveValue("Email");
4443
});
4544

4645
it("should handle keyboard navigation", async () => {
4746
render(ComboBox);
4847

49-
const input = screen.getByRole("textbox");
48+
const input = getInput();
5049
await user.click(input);
5150
await user.keyboard("{ArrowDown}");
5251
await user.keyboard("{Enter}");
@@ -66,7 +65,7 @@ describe("ComboBox", () => {
6665
await user.click(getClearButton());
6766

6867
expect(consoleLog).toHaveBeenCalledWith("clear", expect.any(String));
69-
expect(screen.getByRole("textbox")).toHaveValue("");
68+
expect(getInput()).toHaveValue("");
7069
});
7170

7271
it("should handle clear selection via keyboard navigation (Enter)", async () => {
@@ -79,15 +78,15 @@ describe("ComboBox", () => {
7978
});
8079

8180
expect(consoleLog).not.toHaveBeenCalled();
82-
expect(screen.getByRole("textbox")).toHaveValue("Email");
81+
expect(getInput()).toHaveValue("Email");
8382

8483
const clearButton = getClearButton();
8584
clearButton.focus();
8685
expect(clearButton).toHaveFocus();
8786
await user.keyboard("{Enter}");
8887

8988
expect(consoleLog).toHaveBeenCalledWith("clear", expect.any(String));
90-
expect(screen.getByRole("textbox")).toHaveValue("");
89+
expect(getInput()).toHaveValue("");
9190
});
9291

9392
it("should handle clear selection via keyboard navigation (Space)", async () => {
@@ -100,15 +99,15 @@ describe("ComboBox", () => {
10099
});
101100

102101
expect(consoleLog).not.toHaveBeenCalled();
103-
expect(screen.getByRole("textbox")).toHaveValue("Email");
102+
expect(getInput()).toHaveValue("Email");
104103

105104
const clearButton = getClearButton();
106105
clearButton.focus();
107106
expect(clearButton).toHaveFocus();
108107
await user.keyboard(" ");
109108

110109
expect(consoleLog).toHaveBeenCalledWith("clear", expect.any(String));
111-
expect(screen.getByRole("textbox")).toHaveValue("");
110+
expect(getInput()).toHaveValue("");
112111
});
113112

114113
it("should use custom translations when translateWithId is provided", () => {
@@ -134,7 +133,7 @@ describe("ComboBox", () => {
134133
it("should handle disabled state", () => {
135134
render(ComboBox, { props: { disabled: true } });
136135

137-
expect(screen.getByRole("textbox")).toBeDisabled();
136+
expect(getInput()).toBeDisabled();
138137
expect(screen.getByText("Contact")).toHaveClass("bx--label--disabled");
139138
});
140139

@@ -181,7 +180,7 @@ describe("ComboBox", () => {
181180
it("should handle light variant", () => {
182181
render(ComboBox, { props: { light: true } });
183182

184-
expect(screen.getByRole("textbox")).toHaveClass("bx--text-input--light");
183+
expect(getInput()).toHaveClass("bx--text-input--light");
185184
});
186185

187186
test.each([
@@ -195,7 +194,7 @@ describe("ComboBox", () => {
195194
it("should handle filtering items", async () => {
196195
render(ComboBox);
197196

198-
const input = screen.getByRole("textbox");
197+
const input = getInput();
199198
await user.click(input);
200199
await user.type(input, "em");
201200

@@ -229,21 +228,21 @@ describe("ComboBox", () => {
229228
expect(consoleLog).not.toBeCalled();
230229
await user.click(getClearButton());
231230

232-
expect(screen.getByRole("textbox")).toHaveValue("");
231+
expect(getInput()).toHaveValue("");
233232
expect(consoleLog).toHaveBeenCalledWith("clear", "clear");
234233
});
235234

236235
it("should handle disabled items", async () => {
237236
render(ComboBoxCustom);
238237

239-
await user.click(screen.getByRole("textbox"));
238+
await user.click(getInput());
240239
const disabledOption = screen.getByText(/Fax/).closest('[role="option"]');
241240
assert(disabledOption);
242241
expect(disabledOption).toHaveAttribute("disabled", "true");
243242
expect(disabledOption).toHaveAttribute("aria-disabled", "true");
244243

245244
await user.click(disabledOption);
246-
expect(screen.getByRole("textbox")).toHaveValue("");
245+
expect(getInput()).toHaveValue("");
247246

248247
// Dropdown remains open
249248
const dropdown = screen.getAllByRole("listbox")[1];
@@ -253,7 +252,7 @@ describe("ComboBox", () => {
253252
it("should handle custom item display", async () => {
254253
render(ComboBoxCustom);
255254

256-
await user.click(screen.getByRole("textbox"));
255+
await user.click(getInput());
257256
const options = screen.getAllByRole("option");
258257

259258
expect(options[0]).toHaveTextContent("Item Slack");
@@ -272,19 +271,18 @@ describe("ComboBox", () => {
272271
render(ComboBoxCustom, { props: { selectedId: "1" } });
273272

274273
await user.click(getClearButton());
275-
276-
const input = screen.getByRole("textbox");
277-
expect(input).toHaveValue("");
274+
expect(getInput()).toHaveValue("");
278275
});
279276

280277
it("should programmatically clear selection", async () => {
281278
render(ComboBoxCustom, { props: { selectedId: "1" } });
282279

283-
const textbox = screen.getByRole("textbox");
284-
expect(textbox).toHaveValue("Email");
280+
const input = getInput();
281+
expect(input).toHaveValue("Email");
282+
285283
await user.click(screen.getByText("Clear"));
286-
expect(textbox).toHaveValue("");
287-
expect(textbox).toHaveFocus();
284+
expect(input).toHaveValue("");
285+
expect(input).toHaveFocus();
288286
});
289287

290288
it("should not re-focus textbox if clearOptions.focus is false", async () => {
@@ -295,28 +293,29 @@ describe("ComboBox", () => {
295293
},
296294
});
297295

298-
const textbox = screen.getByRole("textbox");
299-
expect(textbox).toHaveValue("Email");
296+
const input = getInput();
297+
expect(input).toHaveValue("Email");
298+
300299
await user.click(screen.getByText("Clear"));
301-
expect(textbox).toHaveValue("");
302-
expect(textbox).not.toHaveFocus();
300+
expect(input).toHaveValue("");
301+
expect(input).not.toHaveFocus();
303302
});
304303

305304
it("should close menu on Escape key", async () => {
306305
render(ComboBox);
307306

308-
expect(screen.getByRole("textbox")).toHaveValue("");
307+
expect(getInput()).toHaveValue("");
309308

310-
const input = screen.getByRole("textbox");
309+
const input = getInput();
311310
await user.click(input);
312311

313312
const dropdown = screen.getAllByRole("listbox")[1];
314313
expect(dropdown).toBeVisible();
315314

316315
await user.keyboard("{Escape}");
317316
expect(dropdown).not.toBeVisible();
318-
expect(screen.getByRole("textbox")).toHaveValue("");
319-
expect(screen.getByRole("textbox")).toHaveFocus();
317+
expect(getInput()).toHaveValue("");
318+
expect(getInput()).toHaveFocus();
320319
});
321320

322321
it("should close menu and clear selection on Escape key", async () => {
@@ -327,18 +326,18 @@ describe("ComboBox", () => {
327326
},
328327
});
329328

330-
expect(screen.getByRole("textbox")).toHaveValue("Email");
329+
expect(getInput()).toHaveValue("Email");
331330

332-
const input = screen.getByRole("textbox");
331+
const input = getInput();
333332
await user.click(input);
334333

335334
const dropdown = screen.getAllByRole("listbox")[1];
336335
expect(dropdown).toBeVisible();
337336

338337
await user.keyboard("{Escape}");
339338
expect(dropdown).not.toBeVisible();
340-
expect(screen.getByRole("textbox")).toHaveValue("");
341-
expect(screen.getByRole("textbox")).toHaveFocus();
339+
expect(getInput()).toHaveValue("");
340+
expect(getInput()).toHaveFocus();
342341
});
343342

344343
it("should use custom shouldFilterItem function", async () => {
@@ -353,7 +352,7 @@ describe("ComboBox", () => {
353352
item.text.startsWith(value),
354353
},
355354
});
356-
const input = screen.getByRole("textbox");
355+
const input = getInput();
357356
await user.click(input);
358357
await user.type(input, "B");
359358
const options = screen.getAllByRole("option");
@@ -371,7 +370,7 @@ describe("ComboBox", () => {
371370
itemToString: (item: { text: string }) => `Item ${item.text}`,
372371
},
373372
});
374-
const input = screen.getByRole("textbox");
373+
const input = getInput();
375374
await user.click(input);
376375
const options = screen.getAllByRole("option");
377376
expect(options[0]).toHaveTextContent("Item Apple");
@@ -395,7 +394,7 @@ describe("ComboBox", () => {
395394
],
396395
},
397396
});
398-
const input = screen.getByRole("textbox");
397+
const input = getInput();
399398
await user.click(input);
400399
await user.keyboard("{ArrowDown}"); // should highlight A
401400
await user.keyboard("{ArrowDown}"); // should skip B and highlight C
@@ -417,7 +416,7 @@ describe("ComboBox", () => {
417416
render(ComboBox);
418417

419418
await user.keyboard("{Tab}");
420-
expect(screen.getByRole("textbox")).toHaveFocus();
419+
expect(getInput()).toHaveFocus();
421420

422421
const dropdown = screen.queryAllByRole("listbox")[1];
423422
expect(dropdown).toBeUndefined();

0 commit comments

Comments
 (0)