@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,22 @@
1
+ import { default as React, ComponentPropsWithRef } from 'react';
2
+
3
+ export * from './useAutocomplete';
4
+ type ListboxProps = Omit<ComponentPropsWithRef<"ul">, "role"> & {
5
+ "data-testid"?: string;
6
+ "aria-label": NonNullable<ComponentPropsWithRef<"ul">["aria-label"]>;
7
+ "aria-expanded": NonNullable<ComponentPropsWithRef<"ul">["aria-expanded"]>;
8
+ };
9
+ type ListboxItemProps = Omit<ComponentPropsWithRef<"li">, "role"> & {
10
+ "data-testid"?: string;
11
+ highlighted?: boolean;
12
+ hovered?: boolean;
13
+ key?: string;
14
+ selected?: boolean;
15
+ disabled?: boolean;
16
+ noninteractive?: boolean;
17
+ };
18
+ declare const Root: React.ForwardRefExoticComponent<Omit<ListboxProps, "ref"> & React.RefAttributes<HTMLUListElement>>;
19
+ declare const Item: React.ForwardRefExoticComponent<Omit<ListboxItemProps, "ref"> & React.RefAttributes<HTMLLIElement>>;
20
+ export { Item, Root };
21
+ export type { ListboxItemProps, ListboxProps };
22
+ //# sourceMappingURL=listbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listbox.d.ts","sourceRoot":"","sources":["../src/listbox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAEZ,qBAAqB,EAOtB,MAAM,OAAO,CAAC;AAOf,cAAc,mBAAmB,CAAC;AAKlC,KAAK,YAAY,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,GAAG;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,WAAW,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IACrE,eAAe,EAAE,WAAW,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;CAC5E,CAAC;AAgBF,KAAK,gBAAgB,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,GAAG;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AA0CF,QAAA,MAAM,IAAI,oGAAU,CAAC;AACrB,QAAA,MAAM,IAAI,qGAAc,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACtB,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ ._purpur-autocomplete_19u9x_1{position:relative}._purpur-autocomplete__listbox_19u9x_4{position:absolute;z-index:1;top:calc(100% + var(--purpur-spacing-100))}._purpur-listbox_qhoi7_1{padding:0;margin:0;list-style-type:none;border:var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);border-radius:var(--purpur-border-radius-sm);color:var(--purpur-color-brand-off-black);width:100%;background-color:var(--purpur-color-brand-white);max-height:calc(2 * var(--purpur-spacing-1200));overflow-y:scroll;box-sizing:border-box}._purpur-listbox-item_qhoi7_15{list-style:none;padding:var(--purpur-spacing-150);border:var(--purpur-border-width-xs) solid transparent;cursor:pointer;max-width:100%;word-break:break-word;transition:background var(--purpur-motion-duration-150) ease;display:flex;justify-content:space-between;align-items:center;gap:var(--purpur-spacing-100)}@media (hover: hover) and (pointer: fine){._purpur-listbox-item--hovered_qhoi7_29{background:var(--purpur-color-background-interactive-transparent-hover)}}._purpur-listbox-item--highlighted_qhoi7_37{outline:var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);outline-offset:calc(-1 * var(--purpur-border-width-sm))}._purpur-listbox-item_qhoi7_15:active:not(._purpur-listbox-item--noninteractive_qhoi7_41){background:var(--purpur-color-background-interactive-transparent-active)}._purpur-listbox-item--disabled_qhoi7_44{color:var(--purpur-color-text-weak);cursor:default}._purpur-listbox-item--noninteractive_qhoi7_41{cursor:default}._purpur-listbox-item__icon_qhoi7_51{color:var(--purpur-color-text-interactive-selected)}
@@ -0,0 +1,40 @@
1
+ import { ListboxItemProps, ListboxProps } from './listbox';
2
+ import { ReactNode } from 'react';
3
+
4
+ export type Option = {
5
+ label: string;
6
+ id: string;
7
+ value?: string;
8
+ disabled?: boolean;
9
+ };
10
+ export type UseAutocompleteOptions<T extends Option> = {
11
+ highlightFirstOption?: boolean;
12
+ defaultInputValue?: string;
13
+ inputValue?: string;
14
+ filterOption?: (inputValue: string | undefined, option: T) => boolean;
15
+ id: string;
16
+ listboxLabel: string;
17
+ listboxMaxHeight?: string | number;
18
+ noOptionsText?: ReactNode;
19
+ onInputChange?: (value: string) => void;
20
+ openOnFocus?: boolean;
21
+ onSelect?: (option: T | undefined) => void;
22
+ options: T[];
23
+ selectedOption?: T;
24
+ ["data-testid"]?: string;
25
+ };
26
+ export declare const useAutocomplete: <T extends Option>({ highlightFirstOption, defaultInputValue, inputValue, filterOption, id, listboxLabel, listboxMaxHeight, onInputChange, openOnFocus, noOptionsText, onSelect, options, selectedOption, ["data-testid"]: dataTestid, }: UseAutocompleteOptions<T>) => {
27
+ id: string;
28
+ inputProps: Omit<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref"> & {
29
+ ref?: ((instance: HTMLInputElement | null) => void) | import('react').RefObject<HTMLInputElement> | null | undefined;
30
+ } & {
31
+ "data-testid"?: string | undefined;
32
+ };
33
+ internalRef: import('react').MutableRefObject<HTMLDivElement | null>;
34
+ optionsToShow: (T | undefined)[];
35
+ showListbox: boolean;
36
+ noOptionsText: ReactNode;
37
+ getListBoxItemProps: (option: T, index: number) => ListboxItemProps;
38
+ listboxProps: ListboxProps;
39
+ };
40
+ //# sourceMappingURL=useAutocomplete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAutocomplete.d.ts","sourceRoot":"","sources":["../src/useAutocomplete.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,SAAS,EAAoB,MAAM,OAAO,CAAC;AAE1F,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG3D,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,MAAM,IAAI;IAIrD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAI/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAI3B,UAAU,CAAC,EAAE,MAAM,CAAC;IAIpB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;IAItE,EAAE,EAAE,MAAM,CAAC;IAIX,YAAY,EAAE,MAAM,CAAC;IAIrB,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAInC,aAAa,CAAC,EAAE,SAAS,CAAC;IAI1B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAIxC,WAAW,CAAC,EAAE,OAAO,CAAC;IAItB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IAI3C,OAAO,EAAE,CAAC,EAAE,CAAC;IAIb,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,eAAe,4OAezB,uBAAuB,CAAC,CAAC;;;;;;;;;;;kCAsKW,CAAC,SAAS,MAAM,KAAG,gBAAgB;;CAoDzE,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { MutableRefObject } from 'react';
2
+
3
+ export type Prettify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
6
+ export declare const useMutableRefObject: <T>(value: T) => MutableRefObject<T>;
7
+ export declare const useOnClickOutside: (element: HTMLElement | null, callback: () => void) => void;
8
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAkC,MAAM,OAAO,CAAC;AAGzE,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KACvB,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACrB,GAAG,EAAE,CAAC;AAEP,eAAO,MAAM,mBAAmB,aAAc,CAAC,KAAG,iBAAiB,CAAC,CAEnE,CAAC;AAEF,eAAO,MAAM,iBAAiB,YAAa,WAAW,GAAG,IAAI,YAAY,MAAM,IAAI,SAgBlF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@purpurds/autocomplete",
3
+ "version": "0.0.1",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/autocomplete.cjs.js",
6
+ "types": "./dist/autocomplete.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/autocomplete.cjs.js",
10
+ "types": "./dist/autocomplete.d.ts",
11
+ "default": "./dist/autocomplete.es.js"
12
+ },
13
+ "./styles": "./dist/styles.css"
14
+ },
15
+ "source": "src/autocomplete.tsx",
16
+ "dependencies": {
17
+ "classnames": "~2.5.0",
18
+ "@purpurds/icon": "4.5.1",
19
+ "@purpurds/tokens": "4.5.1",
20
+ "@purpurds/paragraph": "4.5.1"
21
+ },
22
+ "devDependencies": {
23
+ "@rushstack/eslint-patch": "~1.10.0",
24
+ "@storybook/blocks": "~7.6.0",
25
+ "@storybook/client-api": "~7.6.0",
26
+ "@storybook/react": "~7.6.0",
27
+ "@telia/base-rig": "~8.2.0",
28
+ "@telia/react-rig": "~3.2.0",
29
+ "@testing-library/dom": "~9.3.3",
30
+ "@testing-library/jest-dom": "~6.4.0",
31
+ "@testing-library/react": "~14.3.0",
32
+ "@types/node": "18",
33
+ "@types/react-dom": "~18.3.0",
34
+ "@types/react": "~18.3.0",
35
+ "eslint-plugin-testing-library": "~6.2.0",
36
+ "eslint": "~8.57.0",
37
+ "jsdom": "~22.1.0",
38
+ "lint-staged": "~10.5.3",
39
+ "prettier": "~2.8.8",
40
+ "react-dom": "~18.3.0",
41
+ "react": "~18.3.0",
42
+ "typescript": "~5.4.2",
43
+ "vite": "~5.2.2",
44
+ "vitest": "~1.5.0",
45
+ "@purpurds/icon": "4.5.1",
46
+ "@purpurds/button": "4.5.1",
47
+ "@purpurds/search-field": "4.5.1",
48
+ "@purpurds/text-field": "4.5.1",
49
+ "@purpurds/component-rig": "1.0.0"
50
+ },
51
+ "scripts": {
52
+ "build:dev": "vite",
53
+ "build:watch": "vite build --watch",
54
+ "build": "vite build",
55
+ "ci:build": "rushx build",
56
+ "coverage": "vitest run --coverage",
57
+ "lint:fix": "eslint . --fix",
58
+ "lint": "lint-staged --no-stash 2>&1",
59
+ "sbdev": "rush sbdev",
60
+ "test:unit": "vitest run --passWithNoTests",
61
+ "test:watch": "vitest --watch",
62
+ "test": "rushx test:unit",
63
+ "typecheck": "tsc -p ./tsconfig.json"
64
+ }
65
+ }
package/readme.mdx ADDED
@@ -0,0 +1,164 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as AutocompleteStories from "./src/autocomplete.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/Autocomplete" of={AutocompleteStories} />
7
+
8
+ # Autocomplete
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ ### Showcase
13
+
14
+ <Primary />
15
+
16
+ ### Properties
17
+
18
+ <ArgTypes />
19
+
20
+ ### Installation
21
+
22
+ #### Via NPM
23
+
24
+ Add the dependency to your consumer app like `"@purpurds/purpur": "^x.y.z"`
25
+
26
+ In MyApp.tsx
27
+
28
+ ```tsx
29
+ import "@purpurds/purpur/styles";
30
+ ```
31
+
32
+ ### Examples
33
+
34
+ In MyComponent.tsx
35
+
36
+ #### With uncontrolled input (using Purpur TextField)
37
+
38
+ ```tsx
39
+ import { Autocomplete, Option, TextField } from "@purpurds/purpur";
40
+ import { useState } from "React";
41
+
42
+ export const MyComponent = () => {
43
+ const options: Options[] = [
44
+ /* Your options */
45
+ ];
46
+ const [selectedOption, setSelectedOption] = useState<Option>();
47
+
48
+ return (
49
+ <Autocomplete
50
+ id="autocomplete"
51
+ listboxLabel="Label for listbox, for a11y (describe the options)"
52
+ selectedOption={selectedOption}
53
+ onSelect={selectedOption}
54
+ options={options}
55
+ selectedOption
56
+ renderInput={(inputProps) => (
57
+ <TextField
58
+ {...inputProps}
59
+ label="Text Field"
60
+ id="autocomplete-input"
61
+ type="text"
62
+ placeholder="Write something"
63
+ />
64
+ )}
65
+ />
66
+ );
67
+ };
68
+ ```
69
+
70
+ #### With controlled input (using Purpur SearchField)
71
+
72
+ ```tsx
73
+ import { Autocomplete, Option, SearchField } from "@purpurds/purpur";
74
+ import { useState } from "React";
75
+
76
+ export const MyComponent = () => {
77
+ const options: Options[] = [
78
+ /* Your options */
79
+ ];
80
+ const [selectedOption, setSelectedOption] = useState<Option>();
81
+ const [inputValue, setInputValue] = useState("");
82
+
83
+ return (
84
+ <Autocomplete
85
+ id="autocomplete"
86
+ listboxLabel="Label for listbox, for a11y (describe the options)"
87
+ selectedOption={selectedOption}
88
+ onSelect={selectedOption}
89
+ options={options}
90
+ selectedOption
91
+ onInputChange={setInputValue}
92
+ inputValue={inputValue}
93
+ renderInput={(inputProps) => (
94
+ <SearchField
95
+ {...inputProps}
96
+ label="Text Field"
97
+ id="autocomplete-input"
98
+ type="text"
99
+ placeholder="Write something"
100
+ onClear={() => setInputValue("")}
101
+ clearButtonAllyLabel="Rensa sökfältet"
102
+ variant="button-attached"
103
+ iconOnlySearchButton
104
+ searchButtonLabel="Search"
105
+ />
106
+ )}
107
+ />
108
+ );
109
+ };
110
+ ```
111
+
112
+ #### With controlled input & with `renderOption` function (using Purpur TextField)
113
+
114
+ Highlight parts of the option labels that are matching the input value
115
+
116
+ ```tsx
117
+ import { Autocomplete, Option, Paragraph, TextField } from "@purpurds/purpur";
118
+ import { useState } from "React";
119
+
120
+ export const MyComponent = () => {
121
+ const options: Options[] = [
122
+ /* Your options */
123
+ ];
124
+ const [inputValue, setInputValue] = useState("");
125
+ const [selectedOption, setSelectedOption] = useState<Option>();
126
+
127
+ function renderOption(option: Option) {
128
+ const optionText = option.label;
129
+ if (!inputValue.trim()) {
130
+ return optionText;
131
+ }
132
+
133
+ const escapeRegExp = (str = "") => str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
134
+ const regex = new RegExp(`(${escapeRegExp(inputValue)})`, "gi");
135
+ const parts = optionText.split(regex);
136
+
137
+ return (
138
+ <Paragraph>
139
+ {parts
140
+ .filter((part) => part)
141
+ .map((part, i) =>
142
+ regex.test(part) ? <strong key={i}>{part}</strong> : <span key={i}>{part}</span>
143
+ )}
144
+ </Paragraph>
145
+ );
146
+ }
147
+
148
+ return (
149
+ <Autocomplete
150
+ id="autocomplete"
151
+ listboxLabel="Label for listbox, for a11y (describe the options)"
152
+ selectedOption={selectedOption}
153
+ onSelect={setSelectedOption}
154
+ onInputChange={setInputValue}
155
+ inputValue={inputValue}
156
+ renderOption={renderOption}
157
+ options={options}
158
+ renderInput={(inputProps) => (
159
+ <TextField {...inputProps} label="With renderOption" id="autocomplete-input" type="text" />
160
+ )}
161
+ />
162
+ );
163
+ };
164
+ ```
@@ -0,0 +1,9 @@
1
+ .purpur-autocomplete {
2
+ position: relative;
3
+
4
+ &__listbox {
5
+ position: absolute;
6
+ z-index: 1;
7
+ top: calc(100% + var(--purpur-spacing-100));
8
+ }
9
+ }
@@ -0,0 +1,202 @@
1
+ import React from "react";
2
+ import { Paragraph } from "@purpurds/paragraph";
3
+ import { SearchField } from "@purpurds/search-field";
4
+ import { TextField } from "@purpurds/text-field";
5
+ import { useArgs, useState } from "@storybook/client-api";
6
+ import type { Meta, StoryObj } from "@storybook/react";
7
+
8
+ import "@purpurds/button/styles";
9
+ import "@purpurds/icon/styles";
10
+ import "@purpurds/search-field/styles";
11
+ import "@purpurds/text-field/styles";
12
+ import { Autocomplete } from "./autocomplete";
13
+
14
+ const options = [
15
+ { id: "0", label: "Apple" },
16
+ { id: "1", label: "Apricot" },
17
+ { id: "2", label: "Acai" },
18
+ { id: "4", label: "Almond" },
19
+ { id: "6", label: "Avocado" },
20
+ { id: "7", label: "Banana" },
21
+ { id: "8", label: "Blueberry" },
22
+ { id: "5", label: "Blackberry" },
23
+ { id: "9", label: "Cherry" },
24
+ { id: "3", label: "Date" },
25
+ { id: "10", label: "Grape" },
26
+ { id: "11", label: "Kiwi" },
27
+ { id: "12", label: "Lemon" },
28
+ { id: "13", label: "Mango" },
29
+ { id: "14", label: "Orange" },
30
+ { id: "15", label: "Papaya" },
31
+ { id: "16", label: "Pear" },
32
+ { id: "17", label: "Pineapple" },
33
+ { id: "18", label: "Raspberry" },
34
+ { id: "19", label: "Strawberry" },
35
+ ];
36
+
37
+ const meta: Meta<typeof Autocomplete> = {
38
+ title: "Components/Autocomplete",
39
+ component: Autocomplete,
40
+ args: {
41
+ options,
42
+ listboxLabel: "Fruits",
43
+ noOptionsText: "That is not a fruit... Try again!",
44
+ },
45
+ parameters: {
46
+ design: [
47
+ {
48
+ name: "Autocomplete",
49
+ type: "figma",
50
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=47667%3A53816&mode=design",
51
+ },
52
+ {
53
+ name: "Dropdown",
54
+ type: "figma",
55
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=47667%3A13626&mode=dev",
56
+ },
57
+ ],
58
+ },
59
+ decorators: [
60
+ (Story) => (
61
+ <div style={{ maxWidth: "18.5rem", minHeight: "20rem" }}>
62
+ <Story />
63
+ </div>
64
+ ),
65
+ ],
66
+ };
67
+
68
+ export default meta;
69
+ type Story = StoryObj<typeof Autocomplete>;
70
+
71
+ export const WithTextField: Story = {
72
+ args: {
73
+ id: "with-text-field",
74
+ },
75
+ argTypes: {
76
+ listboxMaxHeight: { type: "string" },
77
+ },
78
+ render: ({ ...args }) => {
79
+ const [{ selectedOption }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
80
+
81
+ return (
82
+ <Autocomplete
83
+ {...args}
84
+ selectedOption={selectedOption}
85
+ onSelect={(selectedOption) => updateArgs({ selectedOption })}
86
+ renderInput={(inputProps) => (
87
+ <TextField
88
+ {...inputProps}
89
+ label="With Text Field"
90
+ id="autocomplete-input"
91
+ type="text"
92
+ placeholder="Enter a fruit"
93
+ />
94
+ )}
95
+ />
96
+ );
97
+ },
98
+ };
99
+
100
+ export const WithSearchField: Story = {
101
+ args: {
102
+ id: "with-search-field",
103
+ },
104
+ argTypes: {
105
+ listboxMaxHeight: { type: "string" },
106
+ inputValue: { table: { disabled: true } },
107
+ onInputChange: { table: { disabled: true } },
108
+ },
109
+ render: ({ ...args }) => {
110
+ const [inputValue, setInputValue] = useState(""); // eslint-disable-line react-hooks/rules-of-hooks
111
+ const [{ selectedOption }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
112
+
113
+ return (
114
+ <Autocomplete
115
+ {...args}
116
+ selectedOption={selectedOption}
117
+ onSelect={(selectedOption) => updateArgs({ selectedOption })}
118
+ onInputChange={(value) => {
119
+ setInputValue(value);
120
+ updateArgs({ selectedOption: undefined });
121
+ }}
122
+ inputValue={inputValue}
123
+ renderInput={(inputProps) => (
124
+ <SearchField
125
+ {...inputProps}
126
+ label="With Search Field"
127
+ id="autocomplete-input"
128
+ type="text"
129
+ onClear={() => {
130
+ setInputValue("");
131
+ updateArgs({ selectedOption: undefined });
132
+ }}
133
+ placeholder="Find your fruit"
134
+ clearButtonAllyLabel="Rensa sökfältet"
135
+ variant="button-attached"
136
+ iconOnlySearchButton
137
+ searchButtonLabel="Search"
138
+ />
139
+ )}
140
+ />
141
+ );
142
+ },
143
+ };
144
+
145
+ export const WithRenderOption: Story = {
146
+ args: {
147
+ id: "with-render-option",
148
+ },
149
+ argTypes: {
150
+ listboxMaxHeight: { type: "string" },
151
+ inputValue: { table: { disabled: true } },
152
+ onInputChange: { table: { disabled: true } },
153
+ },
154
+ render: ({ ...args }) => {
155
+ const [inputValue, setInputValue] = useState(""); // eslint-disable-line react-hooks/rules-of-hooks
156
+ const [{ selectedOption }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
157
+
158
+ return (
159
+ <>
160
+ <Paragraph>The matching parts are bolded</Paragraph>
161
+ <hr />
162
+ <Autocomplete
163
+ {...args}
164
+ selectedOption={selectedOption}
165
+ onSelect={(selectedOption) => updateArgs({ selectedOption })}
166
+ onInputChange={setInputValue}
167
+ inputValue={inputValue}
168
+ renderOption={(option) => {
169
+ const optionText = option.label;
170
+ if (!inputValue.trim()) {
171
+ return optionText;
172
+ }
173
+
174
+ const escapeRegExp = (str = "") => str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
175
+ const regex = new RegExp(`(${escapeRegExp(inputValue)})`, "gi");
176
+ const parts = optionText.split(regex);
177
+
178
+ return (
179
+ <Paragraph>
180
+ {parts
181
+ .filter((part) => part)
182
+ .map((part, i) =>
183
+ // eslint-disable-next-line react/no-array-index-key
184
+ regex.test(part) ? <strong key={i}>{part}</strong> : <span key={i}>{part}</span>
185
+ )}
186
+ </Paragraph>
187
+ );
188
+ }}
189
+ renderInput={(inputProps) => (
190
+ <TextField
191
+ {...inputProps}
192
+ label="With renderOption"
193
+ id="autocomplete-input"
194
+ type="text"
195
+ placeholder="Enter a fruit"
196
+ />
197
+ )}
198
+ />
199
+ </>
200
+ );
201
+ },
202
+ };