@purpurds/autocomplete 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,515 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { Autocomplete } from "./autocomplete";
7
+
8
+ expect.extend(matchers);
9
+
10
+ describe("B2xAutocomplete", () => {
11
+ afterEach(cleanup);
12
+
13
+ const options = [
14
+ { id: "0", label: "Option 0" },
15
+ { id: "1", label: "Option 1" },
16
+ { id: "2", label: "Option 2" },
17
+ { id: "10", label: "Option 10" },
18
+ { id: "12", label: "Option 12" },
19
+ ];
20
+
21
+ it("should render given input with correct props", () => {
22
+ render(
23
+ <Autocomplete
24
+ id="test-id"
25
+ data-testid="test-id"
26
+ listboxLabel="listbox-label"
27
+ options={options}
28
+ selectedOption={options[0]}
29
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
30
+ />
31
+ );
32
+
33
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
34
+
35
+ expect(input).toBeVisible();
36
+ expect(input).toHaveAttribute("autoComplete", "off");
37
+ expect(input).toHaveAttribute("aria-controls", "test-id-listbox");
38
+ expect(input).toHaveAttribute("aria-autocomplete", "list");
39
+ expect(input).toHaveAttribute("aria-expanded", "false");
40
+ expect(input).toHaveAttribute("id", "test-id-input");
41
+ expect(input).toHaveAttribute("role", "combobox");
42
+ expect(input.value).toEqual(options[0].label);
43
+ });
44
+
45
+ it("should show listbox with items when input is focused if openOnFocus is set", async () => {
46
+ render(
47
+ <Autocomplete
48
+ options={options}
49
+ id="test-id"
50
+ data-testid="test-id"
51
+ selectedOption={options[0]}
52
+ listboxLabel="Options"
53
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
54
+ openOnFocus
55
+ />
56
+ );
57
+
58
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
59
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
60
+ fireEvent.focus(input);
61
+
62
+ const listbox = screen.getByTestId("test-id-listbox");
63
+
64
+ expect(listbox).toBeVisible();
65
+ expect(listbox).toHaveAttribute("aria-label", "Options");
66
+ expect(listbox).toHaveAttribute("aria-expanded", "true");
67
+ expect(listbox).toHaveAttribute("id", "test-id-listbox");
68
+ expect(listbox).toHaveAttribute("role", "listbox");
69
+ expect(listbox.children).toHaveLength(5);
70
+ listbox.childNodes.forEach((listboxItem, index) => {
71
+ expect(listboxItem).toBeVisible();
72
+ expect(listboxItem).toHaveAttribute("aria-disabled", "false");
73
+ expect(listboxItem).toHaveAttribute("aria-selected", `${index === 0}`);
74
+ expect(listboxItem).toHaveAttribute("id", `test-id-listbox-item-${options[index].id}`);
75
+ expect(listboxItem).toHaveAttribute("role", "option");
76
+ expect(listboxItem).toHaveAttribute("tabIndex", "-1");
77
+ });
78
+ });
79
+
80
+ it("should not show listbox when input is focused if openOnFocus isn't set", async () => {
81
+ render(
82
+ <Autocomplete
83
+ options={options}
84
+ id="test-id"
85
+ data-testid="test-id"
86
+ selectedOption={options[0]}
87
+ listboxLabel="Options"
88
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
89
+ />
90
+ );
91
+
92
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
93
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
94
+ fireEvent.focus(input);
95
+
96
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
97
+ });
98
+
99
+ it("should have listbox with items when input is focused", async () => {
100
+ render(
101
+ <Autocomplete
102
+ options={options}
103
+ id="test-id"
104
+ data-testid="test-id"
105
+ selectedOption={options[0]}
106
+ listboxLabel="Options"
107
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
108
+ />
109
+ );
110
+
111
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
112
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
113
+ fireEvent.mouseDown(input);
114
+
115
+ const listbox = screen.getByTestId("test-id-listbox");
116
+
117
+ expect(listbox).toBeVisible();
118
+ expect(listbox).toHaveAttribute("aria-label", "Options");
119
+ expect(listbox).toHaveAttribute("aria-expanded", "true");
120
+ expect(listbox).toHaveAttribute("id", "test-id-listbox");
121
+ expect(listbox).toHaveAttribute("role", "listbox");
122
+ expect(listbox.children).toHaveLength(5);
123
+ listbox.childNodes.forEach((listboxItem, index) => {
124
+ expect(listboxItem).toBeVisible();
125
+ expect(listboxItem).toHaveAttribute("aria-disabled", "false");
126
+ expect(listboxItem).toHaveAttribute("aria-selected", `${index === 0}`);
127
+ expect(listboxItem).toHaveAttribute("id", `test-id-listbox-item-${options[index].id}`);
128
+ expect(listboxItem).toHaveAttribute("role", "option");
129
+ expect(listboxItem).toHaveAttribute("tabIndex", "-1");
130
+ });
131
+ });
132
+
133
+ it("should select clicked listbox item", () => {
134
+ const onSelectMock = vi.fn();
135
+ render(
136
+ <Autocomplete
137
+ options={options}
138
+ id="test-id"
139
+ data-testid="test-id"
140
+ selectedOption={options[0]}
141
+ listboxLabel="Options"
142
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
143
+ onSelect={onSelectMock}
144
+ />
145
+ );
146
+
147
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
148
+ fireEvent.mouseDown(input);
149
+ fireEvent.mouseUp(screen.getByTestId(`test-id-listbox-item-${options[1].id}`));
150
+
151
+ expect(onSelectMock).toHaveBeenCalledWith(options[1]);
152
+ expect(input.value).toEqual(options[1].label);
153
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
154
+ });
155
+
156
+ it("should highlight correct listbox item on arrow keys", () => {
157
+ render(
158
+ <Autocomplete
159
+ options={options}
160
+ id="test-id"
161
+ data-testid="test-id"
162
+ selectedOption={options[0]}
163
+ listboxLabel="Options"
164
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
165
+ />
166
+ );
167
+
168
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
169
+ fireEvent.mouseDown(input);
170
+ expect(screen.getByTestId(`test-id-listbox-item-${options[0].id}`).className).not.toContain(
171
+ "highlighted"
172
+ );
173
+ fireEvent.keyDown(input, { key: "ArrowDown" });
174
+ expect(screen.getByTestId(`test-id-listbox-item-${options[0].id}`).className).toContain(
175
+ "highlighted"
176
+ );
177
+
178
+ fireEvent.keyDown(input, { key: "ArrowDown" });
179
+ expect(screen.getByTestId(`test-id-listbox-item-${options[1].id}`).className).toContain(
180
+ "highlighted"
181
+ );
182
+
183
+ fireEvent.keyDown(input, { key: "ArrowUp" });
184
+ fireEvent.keyDown(input, { key: "ArrowUp" });
185
+ expect(
186
+ screen.getByTestId(`test-id-listbox-item-${options[options.length - 1].id}`).className
187
+ ).toContain("highlighted");
188
+ fireEvent.keyDown(input, { key: "ArrowUp" });
189
+ expect(
190
+ screen.getByTestId(`test-id-listbox-item-${options[options.length - 2].id}`).className
191
+ ).toContain("highlighted");
192
+ });
193
+
194
+ it("should highlight correct item automatically when highlightFirstOption is given", () => {
195
+ render(
196
+ <Autocomplete
197
+ options={options}
198
+ id="test-id"
199
+ data-testid="test-id"
200
+ selectedOption={options[0]}
201
+ listboxLabel="Options"
202
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
203
+ highlightFirstOption
204
+ />
205
+ );
206
+
207
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
208
+ fireEvent.mouseDown(input);
209
+ expect(screen.getByTestId(`test-id-listbox-item-${options[0].id}`).className).toContain(
210
+ "highlighted"
211
+ );
212
+ });
213
+
214
+ it("should show noOptionText item when input is clicked and no options are shown due to filter logic", () => {
215
+ render(
216
+ <Autocomplete
217
+ options={options}
218
+ id="test-id"
219
+ data-testid="test-id"
220
+ selectedOption={undefined}
221
+ listboxLabel="Options"
222
+ filterOption={() => false}
223
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
224
+ noOptionsText="No options"
225
+ />
226
+ );
227
+
228
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
229
+ fireEvent.mouseDown(input);
230
+
231
+ const listbox = screen.getByTestId("test-id-listbox");
232
+ expect(listbox.children).toHaveLength(1);
233
+ expect(listbox.children[0]).toHaveTextContent("No options");
234
+ });
235
+
236
+ it("should not show noOptionText item when input is clicked and no options are shown due to filter logic if noOptionText isn't given", () => {
237
+ render(
238
+ <Autocomplete
239
+ options={options}
240
+ id="test-id"
241
+ data-testid="test-id"
242
+ selectedOption={undefined}
243
+ listboxLabel="Options"
244
+ filterOption={() => false}
245
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
246
+ />
247
+ );
248
+
249
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
250
+ fireEvent.mouseDown(input);
251
+
252
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
253
+ });
254
+
255
+ it("should show noOptionText when input is clicked and no options are given", () => {
256
+ render(
257
+ <Autocomplete
258
+ options={[]}
259
+ id="test-id"
260
+ data-testid="test-id"
261
+ selectedOption={undefined}
262
+ listboxLabel="Options"
263
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
264
+ noOptionsText="No options"
265
+ />
266
+ );
267
+
268
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
269
+ fireEvent.mouseDown(input);
270
+
271
+ const listbox = screen.getByTestId("test-id-listbox");
272
+ expect(listbox.children).toHaveLength(1);
273
+ expect(listbox.children[0]).toHaveTextContent("No options");
274
+ });
275
+
276
+ it("should not show noOptionText when input is clicked and no options are given if noOptionText isn't given", () => {
277
+ render(
278
+ <Autocomplete
279
+ options={[]}
280
+ id="test-id"
281
+ data-testid="test-id"
282
+ selectedOption={undefined}
283
+ listboxLabel="Options"
284
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
285
+ />
286
+ );
287
+
288
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
289
+ fireEvent.mouseDown(input);
290
+
291
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
292
+ });
293
+
294
+ it("should select highlighted listbox item on enter", () => {
295
+ const onSelectMock = vi.fn();
296
+ render(
297
+ <Autocomplete
298
+ options={options}
299
+ id="test-id"
300
+ data-testid="test-id"
301
+ selectedOption={options[0]}
302
+ listboxLabel="Options"
303
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
304
+ onSelect={onSelectMock}
305
+ />
306
+ );
307
+
308
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
309
+ fireEvent.mouseDown(input);
310
+ expect(
311
+ screen.getByTestId(`test-id-listbox-item-${options[options.length - 1].id}`).className
312
+ ).not.toContain("highlighted");
313
+
314
+ fireEvent.keyDown(input, { key: "ArrowUp" });
315
+ expect(
316
+ screen.getByTestId(`test-id-listbox-item-${options[options.length - 1].id}`).className
317
+ ).toContain("highlighted");
318
+
319
+ fireEvent.keyDown(input, { key: "ArrowDown" });
320
+ expect(screen.getByTestId(`test-id-listbox-item-${options[0].id}`).className).toContain(
321
+ "highlighted"
322
+ );
323
+
324
+ fireEvent.keyDown(input, { key: "Enter" });
325
+ expect(onSelectMock).toHaveBeenCalledWith(options[0]);
326
+ });
327
+
328
+ it("should not select highlighted listbox item on enter when listbox is closed", () => {
329
+ const onSelectMock = vi.fn();
330
+ render(
331
+ <Autocomplete
332
+ options={options}
333
+ id="test-id"
334
+ data-testid="test-id"
335
+ selectedOption={options[0]}
336
+ listboxLabel="Options"
337
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
338
+ onSelect={onSelectMock}
339
+ />
340
+ );
341
+
342
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
343
+ fireEvent.click(input);
344
+ fireEvent.keyDown(input, { key: "ArrowDown" });
345
+ expect(screen.getByTestId(`test-id-listbox-item-${options[0].id}`).className).toContain(
346
+ "highlighted"
347
+ );
348
+
349
+ fireEvent.keyDown(input, { key: "ArrowDown" });
350
+ expect(screen.getByTestId(`test-id-listbox-item-${options[1].id}`).className).toContain(
351
+ "highlighted"
352
+ );
353
+
354
+ fireEvent.keyDown(input, { key: "Escape" });
355
+ fireEvent.keyDown(input, { key: "Enter" });
356
+ expect(onSelectMock).not.toHaveBeenCalled();
357
+ });
358
+
359
+ it("should add hover styling on correct listbox item on mouse move", () => {
360
+ const onSelectMock = vi.fn();
361
+ render(
362
+ <Autocomplete
363
+ options={options}
364
+ id="test-id"
365
+ data-testid="test-id"
366
+ selectedOption={options[0]}
367
+ listboxLabel="Options"
368
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
369
+ onSelect={onSelectMock}
370
+ />
371
+ );
372
+
373
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
374
+ fireEvent.mouseDown(input);
375
+ fireEvent.mouseMove(screen.getByTestId(`test-id-listbox-item-${options[0].id}`));
376
+ expect(screen.getByTestId(`test-id-listbox-item-${options[0].id}`).className).toContain(
377
+ "hovered"
378
+ );
379
+
380
+ fireEvent.mouseMove(screen.getByTestId(`test-id-listbox-item-${options[3].id}`));
381
+ expect(screen.getByTestId(`test-id-listbox-item-${options[3].id}`).className).toContain(
382
+ "hovered"
383
+ );
384
+ });
385
+
386
+ it("should open listbox if closed on keydown enter", () => {
387
+ const onSelectMock = vi.fn();
388
+ render(
389
+ <Autocomplete
390
+ options={options}
391
+ id="test-id"
392
+ data-testid="test-id"
393
+ selectedOption={options[0]}
394
+ listboxLabel="Options"
395
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
396
+ onSelect={onSelectMock}
397
+ openOnFocus
398
+ />
399
+ );
400
+
401
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
402
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
403
+ fireEvent.mouseDown(input);
404
+ expect(screen.getByTestId("test-id-listbox")).toBeVisible();
405
+ fireEvent.keyDown(input, { key: "Escape" });
406
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
407
+ fireEvent.keyDown(input, { key: "Tab" });
408
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
409
+ fireEvent.focus(input);
410
+ expect(screen.getByTestId("test-id-listbox")).toBeVisible();
411
+ fireEvent.keyDown(input, { key: "Escape" });
412
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
413
+ fireEvent.keyDown(input, { key: "Enter" });
414
+ expect(screen.getByTestId("test-id-listbox")).toBeVisible();
415
+ });
416
+
417
+ it("should close listbox on keydown escape", () => {
418
+ const onSelectMock = vi.fn();
419
+ render(
420
+ <Autocomplete
421
+ options={options}
422
+ id="test-id"
423
+ data-testid="test-id"
424
+ selectedOption={options[0]}
425
+ listboxLabel="Options"
426
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
427
+ onSelect={onSelectMock}
428
+ />
429
+ );
430
+
431
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
432
+ fireEvent.mouseDown(input);
433
+ expect(screen.getByTestId("test-id-listbox")).toBeVisible();
434
+
435
+ fireEvent.keyDown(input, { key: "Escape" });
436
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
437
+ });
438
+
439
+ it("should close listbox on keydown tab", () => {
440
+ const onSelectMock = vi.fn();
441
+ render(
442
+ <Autocomplete
443
+ options={options}
444
+ id="test-id"
445
+ data-testid="test-id"
446
+ selectedOption={options[0]}
447
+ listboxLabel="Options"
448
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
449
+ onSelect={onSelectMock}
450
+ />
451
+ );
452
+
453
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
454
+ fireEvent.mouseDown(input);
455
+ expect(screen.getByTestId("test-id-listbox")).toBeVisible();
456
+
457
+ fireEvent.keyDown(input, { key: "Tab" });
458
+ expect(screen.queryByTestId("test-id-listbox")).not.toBeInTheDocument();
459
+ });
460
+
461
+ it("should only display options with labels containing input value", () => {
462
+ const onSelectMock = vi.fn();
463
+ render(
464
+ <Autocomplete
465
+ options={options}
466
+ id="test-id"
467
+ data-testid="test-id"
468
+ selectedOption={options[0]}
469
+ listboxLabel="Options"
470
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
471
+ onSelect={onSelectMock}
472
+ />
473
+ );
474
+
475
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
476
+ fireEvent.focus(input);
477
+ fireEvent.change(input, { target: { value: "0" } });
478
+ options
479
+ .filter(({ label }) => label.includes("0"))
480
+ .forEach(({ id }) => {
481
+ expect(screen.getByTestId(`test-id-listbox-item-${id}`)).toBeVisible();
482
+ });
483
+ fireEvent.change(input, { target: { value: "1" } });
484
+ options
485
+ .filter(({ label }) => label.includes("1"))
486
+ .forEach(({ id }) => {
487
+ expect(screen.getByTestId(`test-id-listbox-item-${id}`)).toBeVisible();
488
+ });
489
+ });
490
+
491
+ it("should use custom filtering when given filterOption", () => {
492
+ const onSelectMock = vi.fn();
493
+ render(
494
+ <Autocomplete
495
+ options={options}
496
+ id="test-id"
497
+ data-testid="test-id"
498
+ selectedOption={options[0]}
499
+ listboxLabel="Options"
500
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
501
+ onSelect={onSelectMock}
502
+ filterOption={(_, option) => option.label.includes("2")}
503
+ />
504
+ );
505
+
506
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
507
+ fireEvent.focus(input);
508
+ fireEvent.change(input, { target: { value: "0" } });
509
+ options
510
+ .filter(({ label }) => label.includes("2"))
511
+ .forEach(({ id }) => {
512
+ expect(screen.getByTestId(`test-id-listbox-item-${id}`)).toBeVisible();
513
+ });
514
+ });
515
+ });
@@ -0,0 +1,81 @@
1
+ import React, { ComponentPropsWithRef, ForwardedRef, forwardRef, ReactNode } from "react";
2
+ import c from "classnames/bind";
3
+
4
+ import styles from "./autocomplete.module.scss";
5
+ import * as ListBox from "./listbox";
6
+ import { Option, useAutocomplete, UseAutocompleteOptions } from "./useAutocomplete";
7
+ import { Prettify } from "./utils";
8
+
9
+ const cx = c.bind(styles);
10
+
11
+ type AutocompleteProps<T extends Option> = Prettify<
12
+ UseAutocompleteOptions<T> & {
13
+ className?: string;
14
+ /**
15
+ * Render the input. `props` are native input props
16
+ */
17
+ renderInput: (props: ComponentPropsWithRef<"input">) => ReactNode;
18
+ /**
19
+ * Invoked for each given option. Use to customize the rendering of the options
20
+ */
21
+ renderOption?: (option: T) => ReactNode;
22
+ }
23
+ >;
24
+
25
+ const rootClassName = "purpur-autocomplete";
26
+
27
+ const AutocompleteComponent = <T extends Option>(
28
+ { className, renderInput, renderOption, ...useAutocompleteProps }: AutocompleteProps<T>,
29
+ ref: ForwardedRef<HTMLDivElement>
30
+ ) => {
31
+ const {
32
+ id,
33
+ inputProps,
34
+ internalRef,
35
+ optionsToShow,
36
+ showListbox,
37
+ noOptionsText,
38
+ getListBoxItemProps,
39
+ listboxProps,
40
+ } = useAutocomplete(useAutocompleteProps);
41
+
42
+ const renderListboxItem = (option: T, index: number) => {
43
+ const { key, ...listboxItemProps } = getListBoxItemProps(option, index);
44
+
45
+ return (
46
+ <ListBox.Item key={key} {...listboxItemProps}>
47
+ {renderOption ? renderOption?.(option) : option.label}
48
+ </ListBox.Item>
49
+ );
50
+ };
51
+
52
+ const setRootRef = (node: HTMLDivElement | null) => {
53
+ internalRef.current = node;
54
+ if (typeof ref === "function") {
55
+ ref(node);
56
+ } else if (ref) {
57
+ ref.current = node;
58
+ }
59
+ };
60
+
61
+ return (
62
+ <div id={id} ref={setRootRef} className={cx([className, rootClassName])}>
63
+ {renderInput(inputProps)}
64
+ {showListbox && (
65
+ <ListBox.Root {...listboxProps} className={cx(`${rootClassName}__listbox`)}>
66
+ {!optionsToShow.length ? (
67
+ <ListBox.Item noninteractive>{noOptionsText}</ListBox.Item>
68
+ ) : (
69
+ optionsToShow.filter((option): option is T => !!option).map(renderListboxItem)
70
+ )}
71
+ </ListBox.Root>
72
+ )}
73
+ </div>
74
+ );
75
+ };
76
+
77
+ export const Autocomplete = forwardRef(AutocompleteComponent);
78
+ Autocomplete.displayName = "Autocomplete";
79
+
80
+ export type { Option, UseAutocompleteOptions } from "./useAutocomplete";
81
+ export { useAutocomplete } from "./useAutocomplete";
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,60 @@
1
+ .purpur-listbox {
2
+ padding: 0;
3
+ margin: 0;
4
+ width: 100%;
5
+ list-style-type: none;
6
+ border: var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);
7
+ border-radius: var(--purpur-border-radius-sm);
8
+ color: var(--purpur-color-brand-off-black);
9
+ width: 100%;
10
+ background-color: var(--purpur-color-brand-white);
11
+ max-height: calc(2 * var(--purpur-spacing-1200));
12
+ overflow-y: scroll;
13
+ box-sizing: border-box;
14
+
15
+ &-item {
16
+ $listboxItemRoot: &;
17
+
18
+ cursor: pointer;
19
+ list-style: none;
20
+ padding: var(--purpur-spacing-150);
21
+ border: var(--purpur-border-width-xs) solid transparent;
22
+ cursor: pointer;
23
+ max-width: 100%;
24
+ word-break: break-word;
25
+ transition: background var(--purpur-motion-duration-150) ease;
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ gap: var(--purpur-spacing-100);
30
+
31
+ &--hovered {
32
+ /* Enable only on non-touch devices */
33
+ @media (hover: hover) and (pointer: fine) {
34
+ background: var(--purpur-color-background-interactive-transparent-hover);
35
+ }
36
+ }
37
+
38
+ &--highlighted {
39
+ outline: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
40
+ outline-offset: calc(-1 * var(--purpur-border-width-sm));
41
+ }
42
+
43
+ &:active:not(#{$listboxItemRoot}--noninteractive) {
44
+ background: var(--purpur-color-background-interactive-transparent-active);
45
+ }
46
+
47
+ &--disabled {
48
+ color: var(--purpur-color-text-weak);
49
+ cursor: default;
50
+ }
51
+
52
+ &--noninteractive {
53
+ cursor: default;
54
+ }
55
+
56
+ &__icon {
57
+ color: var(--purpur-color-text-interactive-selected);
58
+ }
59
+ }
60
+ }