@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.
- package/dist/LICENSE.txt +45 -0
- package/dist/autocomplete.cjs.js +14 -0
- package/dist/autocomplete.cjs.js.map +1 -0
- package/dist/autocomplete.d.ts +25 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.es.js +437 -0
- package/dist/autocomplete.es.js.map +1 -0
- package/dist/listbox.d.ts +22 -0
- package/dist/listbox.d.ts.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/useAutocomplete.d.ts +40 -0
- package/dist/useAutocomplete.d.ts.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +65 -0
- package/readme.mdx +164 -0
- package/src/autocomplete.module.scss +9 -0
- package/src/autocomplete.stories.tsx +202 -0
- package/src/autocomplete.test.tsx +515 -0
- package/src/autocomplete.tsx +81 -0
- package/src/global.d.ts +4 -0
- package/src/listbox.module.scss +60 -0
- package/src/listbox.tsx +96 -0
- package/src/useAutocomplete.ts +302 -0
- package/src/utils.ts +28 -0
|
@@ -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"}
|
package/dist/styles.css
ADDED
|
@@ -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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -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,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
|
+
};
|