@availity/mui-autocomplete 0.3.1 → 0.4.0
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/CHANGELOG.md +14 -0
- package/README.md +81 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +71 -4
- package/dist/index.mjs +63 -4
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/lib/AsyncAutocomplete.test.tsx +86 -0
- package/src/lib/AsyncAutocomplete.tsx +74 -0
- package/src/lib/Autocomplete.stories.tsx +109 -1
- package/src/lib/Autocomplete.tsx +17 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [0.4.0](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.3.2...@availity/mui-autocomplete@0.4.0) (2024-04-11)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **mui-autocomplete:** add AsyncAutocomplete component ([2318b3f](https://github.com/Availity/element/commit/2318b3fd70055322c5e0ea2f28514a6886c29a98))
|
|
11
|
+
|
|
12
|
+
## [0.3.2](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.3.1...@availity/mui-autocomplete@0.3.2) (2024-04-01)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Performance Improvements
|
|
16
|
+
|
|
17
|
+
* **mui-autocomplete:** use path imports for mui deps and move element deps to peerDeps ([6379ec5](https://github.com/Availity/element/commit/6379ec5489a7e6926f99dc07cf085f90ba735d03))
|
|
18
|
+
|
|
5
19
|
## [0.3.1](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.3.0...@availity/mui-autocomplete@0.3.1) (2024-02-20)
|
|
6
20
|
|
|
7
21
|
## [0.3.0](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.2.1...@availity/mui-autocomplete@0.3.0) (2023-12-14)
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ yarn add @availity/element
|
|
|
34
34
|
|
|
35
35
|
#### NPM
|
|
36
36
|
|
|
37
|
-
_This package has a few peer dependencies. Add `@mui/material
|
|
37
|
+
_This package has a few peer dependencies. Add `@mui/material`, `@emotion/react`, `@availity/mui-form-utils`, & `@availity/mui-textfield` to your project if not already installed._
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npm install @availity/mui-autocomplete
|
|
@@ -50,8 +50,26 @@ yarn add @availity/mui-autocomplete
|
|
|
50
50
|
|
|
51
51
|
#### Import through @availity/element
|
|
52
52
|
|
|
53
|
+
The `Autcomplete` component can be used standalone or with a form state library like [react-hook-form](https://react-hook-form.com/).
|
|
54
|
+
|
|
55
|
+
`Autocomplete` uses the `TextField` component to render the input. You must pass your field related props: `label`, `helperText`, `error`, etc. to the the `FieldProps` prop.
|
|
56
|
+
|
|
53
57
|
```tsx
|
|
54
58
|
import { Autocomplete } from '@availity/element';
|
|
59
|
+
|
|
60
|
+
const MyAutocomplete = () => {
|
|
61
|
+
return (
|
|
62
|
+
<Autocomplete
|
|
63
|
+
options={[
|
|
64
|
+
{ label: 'Option 1', value: 1 },
|
|
65
|
+
{ label: 'Option 2', value: 2 },
|
|
66
|
+
{ label: 'Option 3', value: 3 },
|
|
67
|
+
]}
|
|
68
|
+
getOptionLabel={(value) => value.label}
|
|
69
|
+
FieldProps={{ label: 'My Autocomplete Field', helperText: 'Text that helps the user' }}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
55
73
|
```
|
|
56
74
|
|
|
57
75
|
#### Direct import
|
|
@@ -59,3 +77,65 @@ import { Autocomplete } from '@availity/element';
|
|
|
59
77
|
```tsx
|
|
60
78
|
import { Autocomplete } from '@availity/mui-autocomplete';
|
|
61
79
|
```
|
|
80
|
+
|
|
81
|
+
#### Usage with `react-hook-form`
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
85
|
+
import { Autocomplete, Button } from '@availity/element';
|
|
86
|
+
|
|
87
|
+
const Form = () => {
|
|
88
|
+
const { handleSubmit } = useForm();
|
|
89
|
+
|
|
90
|
+
const onSubmit = (values) => {
|
|
91
|
+
console.log(values);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
96
|
+
<Controller
|
|
97
|
+
control={control}
|
|
98
|
+
name="dropdown"
|
|
99
|
+
render={({ field: { onChange, value, onBlur } }) => {
|
|
100
|
+
return (
|
|
101
|
+
<Autocomplete
|
|
102
|
+
onChange={(event, value, reason) => {
|
|
103
|
+
if (reason === 'clear') {
|
|
104
|
+
onChange(null);
|
|
105
|
+
}
|
|
106
|
+
onChange(value);
|
|
107
|
+
}}
|
|
108
|
+
onBlur={onBlur}
|
|
109
|
+
FieldProps={{ label: 'Dropdown', helperText: 'This is helper text', placeholder: 'Value' }}
|
|
110
|
+
options={['Bulbasaur', 'Squirtle', 'Charmander']}
|
|
111
|
+
value={value || null}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
<Button type="submit">Submit</Button>
|
|
117
|
+
</form>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `AsyncAutocomplete` Usage
|
|
123
|
+
|
|
124
|
+
An `AsyncAutocomplete` component is exported for use cases that require fetching paginated results from an api. You will need to use the `loadOptions` prop. The `loadOptions` function will be called when the user scrolls to the bottom of the dropdown. It will be passed the current page and limit. The `limit` prop controls what is passed to `loadOptions` and is defaulted to `50`. The `loadOptions` function must return an object that has an array of `options` and a `hasMore` property. `hasMore` tells the `AsyncAutocomplete` component whether or not it should call `loadOptions` again. The returned `options` will be concatenated to the existing options array.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { Autocomplete } from '@availity/element';
|
|
128
|
+
|
|
129
|
+
const Example = () => {
|
|
130
|
+
const loadOptions = async (page: number) => {
|
|
131
|
+
const response = await callApi(page);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
options: repsonse.data,
|
|
135
|
+
hasMore: response.totalCount > response.count,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return <Autocomplete FieldProps={{ label: 'Async Dropdown' }} loadOptions={loadOptions} />;
|
|
140
|
+
};
|
|
141
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { AutocompleteProps as AutocompleteProps$1 } from '@mui/material/Autocomplete';
|
|
3
|
+
import { ChipTypeMap } from '@mui/material/Chip';
|
|
3
4
|
import { TextFieldProps } from '@availity/mui-textfield';
|
|
4
5
|
|
|
5
6
|
interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AutocompleteProps$1<T, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'clearIcon' | 'clearText' | 'closeText' | 'componentsProps' | 'disabledItemsFocusable' | 'forcePopupIcon' | 'fullWidth' | 'handleHomeEndKeys' | 'includeInputInList' | 'openOnFocus' | 'openText' | 'PaperComponent' | 'PopperComponent' | 'popupIcon' | 'selectOnFocus' | 'size' | 'renderInput' | 'slotProps'> {
|
|
@@ -9,4 +10,16 @@ interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClea
|
|
|
9
10
|
}
|
|
10
11
|
declare const Autocomplete: <T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any> = "div">({ FieldProps, ...props }: AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) => JSX.Element;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
interface AsyncAutocompleteProps<Option, Multiple extends boolean | undefined, DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'options'> {
|
|
14
|
+
/** Function that returns a promise with options and hasMore */
|
|
15
|
+
loadOptions: (page: number, limit: number) => Promise<{
|
|
16
|
+
options: Option[];
|
|
17
|
+
hasMore: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
/** The number of options to request from the api
|
|
20
|
+
* @default 50 */
|
|
21
|
+
limit?: number;
|
|
22
|
+
}
|
|
23
|
+
declare const AsyncAutocomplete: <Option, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any> = "div">({ loadOptions, limit, ...rest }: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => JSX.Element;
|
|
24
|
+
|
|
25
|
+
export { AsyncAutocomplete, AsyncAutocompleteProps, Autocomplete, AutocompleteProps };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,18 +17,25 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
18
24
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
25
|
|
|
20
26
|
// src/index.ts
|
|
21
27
|
var src_exports = {};
|
|
22
28
|
__export(src_exports, {
|
|
29
|
+
AsyncAutocomplete: () => AsyncAutocomplete,
|
|
23
30
|
Autocomplete: () => Autocomplete
|
|
24
31
|
});
|
|
25
32
|
module.exports = __toCommonJS(src_exports);
|
|
26
33
|
|
|
27
34
|
// src/lib/Autocomplete.tsx
|
|
28
35
|
var import_react = require("react");
|
|
29
|
-
var
|
|
36
|
+
var import_Autocomplete = __toESM(require("@mui/material/Autocomplete"));
|
|
37
|
+
var import_CircularProgress = __toESM(require("@mui/material/CircularProgress"));
|
|
38
|
+
var import_IconButton = __toESM(require("@mui/material/IconButton"));
|
|
30
39
|
var import_mui_textfield = require("@availity/mui-textfield");
|
|
31
40
|
var import_mui_form_utils = require("@availity/mui-form-utils");
|
|
32
41
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -36,12 +45,17 @@ var PopupIndicatorWrapper = (0, import_react.forwardRef)((props, ref) => /* @__P
|
|
|
36
45
|
orientation: "vertical",
|
|
37
46
|
className: "MuiSelect-avEndAdornmentDivider"
|
|
38
47
|
}),
|
|
39
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
48
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_IconButton.default, {
|
|
40
49
|
...props,
|
|
41
50
|
ref
|
|
42
51
|
})
|
|
43
52
|
]
|
|
44
53
|
}));
|
|
54
|
+
var progressSx = { marginRight: ".5rem" };
|
|
55
|
+
var LoadingIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_CircularProgress.default, {
|
|
56
|
+
size: 20,
|
|
57
|
+
sx: progressSx
|
|
58
|
+
});
|
|
45
59
|
var Autocomplete = ({
|
|
46
60
|
FieldProps,
|
|
47
61
|
...props
|
|
@@ -53,14 +67,20 @@ var Autocomplete = ({
|
|
|
53
67
|
const resolvedProps = (params) => ({
|
|
54
68
|
InputProps: {
|
|
55
69
|
...FieldProps == null ? void 0 : FieldProps.InputProps,
|
|
56
|
-
...params == null ? void 0 : params.InputProps
|
|
70
|
+
...params == null ? void 0 : params.InputProps,
|
|
71
|
+
endAdornment: props.loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, {
|
|
72
|
+
children: [
|
|
73
|
+
(params == null ? void 0 : params.InputProps.endAdornment) || null,
|
|
74
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingIndicator, {})
|
|
75
|
+
]
|
|
76
|
+
}) : (params == null ? void 0 : params.InputProps.endAdornment) || null
|
|
57
77
|
},
|
|
58
78
|
inputProps: {
|
|
59
79
|
...FieldProps == null ? void 0 : FieldProps.inputProps,
|
|
60
80
|
...params == null ? void 0 : params.inputProps
|
|
61
81
|
}
|
|
62
82
|
});
|
|
63
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
83
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Autocomplete.default, {
|
|
64
84
|
renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_mui_textfield.TextField, {
|
|
65
85
|
...params,
|
|
66
86
|
...resolvedProps(params),
|
|
@@ -74,7 +94,54 @@ var Autocomplete = ({
|
|
|
74
94
|
...defaultProps
|
|
75
95
|
});
|
|
76
96
|
};
|
|
97
|
+
|
|
98
|
+
// src/lib/AsyncAutocomplete.tsx
|
|
99
|
+
var import_react2 = require("react");
|
|
100
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
101
|
+
var AsyncAutocomplete = ({
|
|
102
|
+
loadOptions,
|
|
103
|
+
limit = 50,
|
|
104
|
+
...rest
|
|
105
|
+
}) => {
|
|
106
|
+
const [page, setPage] = (0, import_react2.useState)(0);
|
|
107
|
+
const [options, setOptions] = (0, import_react2.useState)([]);
|
|
108
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
109
|
+
const [hasMore, setHasMore] = (0, import_react2.useState)(true);
|
|
110
|
+
(0, import_react2.useEffect)(() => {
|
|
111
|
+
const getInitialOptions = async () => {
|
|
112
|
+
setLoading(true);
|
|
113
|
+
const result = await loadOptions(page, limit);
|
|
114
|
+
setOptions(result.options);
|
|
115
|
+
setHasMore(result.hasMore);
|
|
116
|
+
setPage((prev) => prev + 1);
|
|
117
|
+
setLoading(false);
|
|
118
|
+
};
|
|
119
|
+
if (!loading && hasMore && page === 0) {
|
|
120
|
+
getInitialOptions();
|
|
121
|
+
}
|
|
122
|
+
}, [page, loading, loadOptions]);
|
|
123
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Autocomplete, {
|
|
124
|
+
...rest,
|
|
125
|
+
loading,
|
|
126
|
+
options,
|
|
127
|
+
ListboxProps: {
|
|
128
|
+
onScroll: async (event) => {
|
|
129
|
+
const listboxNode = event.currentTarget;
|
|
130
|
+
const difference = listboxNode.scrollHeight - (listboxNode.scrollTop + listboxNode.clientHeight);
|
|
131
|
+
if (difference <= 5 && !loading && hasMore) {
|
|
132
|
+
setLoading(true);
|
|
133
|
+
const result = await loadOptions(page, limit);
|
|
134
|
+
setOptions([...options, ...result.options]);
|
|
135
|
+
setHasMore(result.hasMore);
|
|
136
|
+
setPage((prev) => prev + 1);
|
|
137
|
+
setLoading(false);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
77
143
|
// Annotate the CommonJS export names for ESM import in node:
|
|
78
144
|
0 && (module.exports = {
|
|
145
|
+
AsyncAutocomplete,
|
|
79
146
|
Autocomplete
|
|
80
147
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/lib/Autocomplete.tsx
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
default as MuiAutocomplete
|
|
5
|
+
} from "@mui/material/Autocomplete";
|
|
6
|
+
import CircularProgress from "@mui/material/CircularProgress";
|
|
7
|
+
import { default as MuiIconButton } from "@mui/material/IconButton";
|
|
7
8
|
import { TextField } from "@availity/mui-textfield";
|
|
8
9
|
import { SelectDivider, SelectExpandIcon } from "@availity/mui-form-utils";
|
|
9
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -19,6 +20,11 @@ var PopupIndicatorWrapper = forwardRef((props, ref) => /* @__PURE__ */ jsxs(Frag
|
|
|
19
20
|
})
|
|
20
21
|
]
|
|
21
22
|
}));
|
|
23
|
+
var progressSx = { marginRight: ".5rem" };
|
|
24
|
+
var LoadingIndicator = () => /* @__PURE__ */ jsx(CircularProgress, {
|
|
25
|
+
size: 20,
|
|
26
|
+
sx: progressSx
|
|
27
|
+
});
|
|
22
28
|
var Autocomplete = ({
|
|
23
29
|
FieldProps,
|
|
24
30
|
...props
|
|
@@ -30,7 +36,13 @@ var Autocomplete = ({
|
|
|
30
36
|
const resolvedProps = (params) => ({
|
|
31
37
|
InputProps: {
|
|
32
38
|
...FieldProps == null ? void 0 : FieldProps.InputProps,
|
|
33
|
-
...params == null ? void 0 : params.InputProps
|
|
39
|
+
...params == null ? void 0 : params.InputProps,
|
|
40
|
+
endAdornment: props.loading ? /* @__PURE__ */ jsxs(Fragment, {
|
|
41
|
+
children: [
|
|
42
|
+
(params == null ? void 0 : params.InputProps.endAdornment) || null,
|
|
43
|
+
/* @__PURE__ */ jsx(LoadingIndicator, {})
|
|
44
|
+
]
|
|
45
|
+
}) : (params == null ? void 0 : params.InputProps.endAdornment) || null
|
|
34
46
|
},
|
|
35
47
|
inputProps: {
|
|
36
48
|
...FieldProps == null ? void 0 : FieldProps.inputProps,
|
|
@@ -51,6 +63,53 @@ var Autocomplete = ({
|
|
|
51
63
|
...defaultProps
|
|
52
64
|
});
|
|
53
65
|
};
|
|
66
|
+
|
|
67
|
+
// src/lib/AsyncAutocomplete.tsx
|
|
68
|
+
import { useState, useEffect } from "react";
|
|
69
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
70
|
+
var AsyncAutocomplete = ({
|
|
71
|
+
loadOptions,
|
|
72
|
+
limit = 50,
|
|
73
|
+
...rest
|
|
74
|
+
}) => {
|
|
75
|
+
const [page, setPage] = useState(0);
|
|
76
|
+
const [options, setOptions] = useState([]);
|
|
77
|
+
const [loading, setLoading] = useState(false);
|
|
78
|
+
const [hasMore, setHasMore] = useState(true);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const getInitialOptions = async () => {
|
|
81
|
+
setLoading(true);
|
|
82
|
+
const result = await loadOptions(page, limit);
|
|
83
|
+
setOptions(result.options);
|
|
84
|
+
setHasMore(result.hasMore);
|
|
85
|
+
setPage((prev) => prev + 1);
|
|
86
|
+
setLoading(false);
|
|
87
|
+
};
|
|
88
|
+
if (!loading && hasMore && page === 0) {
|
|
89
|
+
getInitialOptions();
|
|
90
|
+
}
|
|
91
|
+
}, [page, loading, loadOptions]);
|
|
92
|
+
return /* @__PURE__ */ jsx2(Autocomplete, {
|
|
93
|
+
...rest,
|
|
94
|
+
loading,
|
|
95
|
+
options,
|
|
96
|
+
ListboxProps: {
|
|
97
|
+
onScroll: async (event) => {
|
|
98
|
+
const listboxNode = event.currentTarget;
|
|
99
|
+
const difference = listboxNode.scrollHeight - (listboxNode.scrollTop + listboxNode.clientHeight);
|
|
100
|
+
if (difference <= 5 && !loading && hasMore) {
|
|
101
|
+
setLoading(true);
|
|
102
|
+
const result = await loadOptions(page, limit);
|
|
103
|
+
setOptions([...options, ...result.options]);
|
|
104
|
+
setHasMore(result.hasMore);
|
|
105
|
+
setPage((prev) => prev + 1);
|
|
106
|
+
setLoading(false);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
};
|
|
54
112
|
export {
|
|
113
|
+
AsyncAutocomplete,
|
|
55
114
|
Autocomplete
|
|
56
115
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@availity/mui-autocomplete",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Availity MUI Autocomplete Component - part of the @availity/element design system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -33,8 +33,6 @@
|
|
|
33
33
|
"publish:canary": "yarn npm publish --access public --tag canary"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@availity/mui-form-utils": "0.9.6",
|
|
37
|
-
"@availity/mui-textfield": "0.5.11",
|
|
38
36
|
"@mui/types": "^7.2.5"
|
|
39
37
|
},
|
|
40
38
|
"devDependencies": {
|
|
@@ -45,6 +43,8 @@
|
|
|
45
43
|
"typescript": "^4.6.4"
|
|
46
44
|
},
|
|
47
45
|
"peerDependencies": {
|
|
46
|
+
"@availity/mui-form-utils": "^0.10.1",
|
|
47
|
+
"@availity/mui-textfield": "^0.5.15",
|
|
48
48
|
"@mui/material": "^5.11.9",
|
|
49
49
|
"react": ">=16.3.0"
|
|
50
50
|
},
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { AsyncAutocomplete } from './AsyncAutocomplete';
|
|
3
|
+
|
|
4
|
+
describe('AsyncAutocomplete', () => {
|
|
5
|
+
test('should render successfully', () => {
|
|
6
|
+
const { getByLabelText } = render(
|
|
7
|
+
<AsyncAutocomplete
|
|
8
|
+
FieldProps={{ label: 'Test' }}
|
|
9
|
+
loadOptions={async () => ({
|
|
10
|
+
options: ['1', '2', '3'],
|
|
11
|
+
hasMore: false,
|
|
12
|
+
})}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
expect(getByLabelText('Test')).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('options should be available', async () => {
|
|
19
|
+
const loadOptions = () =>
|
|
20
|
+
Promise.resolve({
|
|
21
|
+
options: [{ label: 'Option 1' }],
|
|
22
|
+
getOptionLabel: (option: { label: string }) => option.label,
|
|
23
|
+
hasMore: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
render(<AsyncAutocomplete loadOptions={loadOptions} FieldProps={{ label: 'Test' }} />);
|
|
27
|
+
|
|
28
|
+
const input = screen.getByRole('combobox');
|
|
29
|
+
fireEvent.click(input);
|
|
30
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
31
|
+
|
|
32
|
+
waitFor(() => {
|
|
33
|
+
expect(screen.getByText('Option 1')).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
fireEvent.click(await screen.findByText('Option 1'));
|
|
37
|
+
|
|
38
|
+
waitFor(() => {
|
|
39
|
+
expect(screen.getByText('Option 1')).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should call loadOptions when scroll to the bottom', async () => {
|
|
44
|
+
const loadOptions = jest.fn();
|
|
45
|
+
loadOptions.mockResolvedValueOnce({
|
|
46
|
+
options: [
|
|
47
|
+
{ label: 'Option 1' },
|
|
48
|
+
{ label: 'Option 2' },
|
|
49
|
+
{ label: 'Option 3' },
|
|
50
|
+
{ label: 'Option 4' },
|
|
51
|
+
{ label: 'Option 5' },
|
|
52
|
+
{ label: 'Option 6' },
|
|
53
|
+
],
|
|
54
|
+
hasMore: true,
|
|
55
|
+
});
|
|
56
|
+
render(<AsyncAutocomplete loadOptions={loadOptions} FieldProps={{ label: 'Test' }} />);
|
|
57
|
+
|
|
58
|
+
const input = screen.getByRole('combobox');
|
|
59
|
+
fireEvent.click(input);
|
|
60
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
61
|
+
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(screen.getByText('Option 1')).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(loadOptions).toHaveBeenCalled();
|
|
67
|
+
expect(loadOptions).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(loadOptions).toHaveBeenCalledWith(0, 50);
|
|
69
|
+
|
|
70
|
+
loadOptions.mockResolvedValueOnce({
|
|
71
|
+
options: [{ label: 'Option 7' }],
|
|
72
|
+
hasMore: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await act(async () => {
|
|
76
|
+
const options = await screen.findByRole('listbox');
|
|
77
|
+
fireEvent.scroll(options, { target: { scrollTop: options.scrollHeight } });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(loadOptions).toHaveBeenCalled();
|
|
82
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
83
|
+
expect(loadOptions).toHaveBeenLastCalledWith(1, 50);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import type { ChipTypeMap } from '@mui/material/Chip';
|
|
3
|
+
|
|
4
|
+
import { Autocomplete, AutocompleteProps } from './Autocomplete';
|
|
5
|
+
|
|
6
|
+
export interface AsyncAutocompleteProps<
|
|
7
|
+
Option,
|
|
8
|
+
Multiple extends boolean | undefined,
|
|
9
|
+
DisableClearable extends boolean | undefined,
|
|
10
|
+
FreeSolo extends boolean | undefined,
|
|
11
|
+
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']
|
|
12
|
+
> extends Omit<AutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'options'> {
|
|
13
|
+
/** Function that returns a promise with options and hasMore */
|
|
14
|
+
loadOptions: (page: number, limit: number) => Promise<{ options: Option[]; hasMore: boolean }>;
|
|
15
|
+
/** The number of options to request from the api
|
|
16
|
+
* @default 50 */
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const AsyncAutocomplete = <
|
|
21
|
+
Option,
|
|
22
|
+
Multiple extends boolean | undefined = false,
|
|
23
|
+
DisableClearable extends boolean | undefined = false,
|
|
24
|
+
FreeSolo extends boolean | undefined = false,
|
|
25
|
+
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']
|
|
26
|
+
>({
|
|
27
|
+
loadOptions,
|
|
28
|
+
limit = 50,
|
|
29
|
+
...rest
|
|
30
|
+
}: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => {
|
|
31
|
+
const [page, setPage] = useState(0);
|
|
32
|
+
const [options, setOptions] = useState<Option[]>([]);
|
|
33
|
+
const [loading, setLoading] = useState(false);
|
|
34
|
+
const [hasMore, setHasMore] = useState(true);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const getInitialOptions = async () => {
|
|
38
|
+
setLoading(true);
|
|
39
|
+
const result = await loadOptions(page, limit);
|
|
40
|
+
setOptions(result.options);
|
|
41
|
+
setHasMore(result.hasMore);
|
|
42
|
+
setPage((prev) => prev + 1);
|
|
43
|
+
setLoading(false);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!loading && hasMore && page === 0) {
|
|
47
|
+
getInitialOptions();
|
|
48
|
+
}
|
|
49
|
+
}, [page, loading, loadOptions]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Autocomplete
|
|
53
|
+
{...rest}
|
|
54
|
+
loading={loading}
|
|
55
|
+
options={options}
|
|
56
|
+
ListboxProps={{
|
|
57
|
+
onScroll: async (event: React.SyntheticEvent) => {
|
|
58
|
+
const listboxNode = event.currentTarget;
|
|
59
|
+
const difference = listboxNode.scrollHeight - (listboxNode.scrollTop + listboxNode.clientHeight);
|
|
60
|
+
|
|
61
|
+
// Only fetch if we are near the bottom, not already fetching, and there are more results
|
|
62
|
+
if (difference <= 5 && !loading && hasMore) {
|
|
63
|
+
setLoading(true);
|
|
64
|
+
const result = await loadOptions(page, limit);
|
|
65
|
+
setOptions([...options, ...result.options]);
|
|
66
|
+
setHasMore(result.hasMore);
|
|
67
|
+
setPage((prev) => prev + 1);
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Each exported component in the package should have its own stories file
|
|
2
|
-
|
|
3
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
3
|
import { Autocomplete } from './Autocomplete';
|
|
4
|
+
import { AsyncAutocomplete } from './AsyncAutocomplete';
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof Autocomplete> = {
|
|
7
7
|
title: 'Components/Autocomplete/Autocomplete',
|
|
@@ -42,3 +42,111 @@ export const _Multi: StoryObj<typeof Autocomplete> = {
|
|
|
42
42
|
multiple: true,
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
|
+
|
|
46
|
+
type Org = {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const organizations: Org[] = [
|
|
52
|
+
{
|
|
53
|
+
id: '1',
|
|
54
|
+
name: 'Org 1',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: '2',
|
|
58
|
+
name: 'Org 2',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: '3',
|
|
62
|
+
name: 'Org 3',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: '4',
|
|
66
|
+
name: 'Org 4',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: '5',
|
|
70
|
+
name: 'Org 5',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: '6',
|
|
74
|
+
name: 'Org 6',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: '7',
|
|
78
|
+
name: 'Org 7',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: '8',
|
|
82
|
+
name: 'Org 8',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: '9',
|
|
86
|
+
name: 'Org 9',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: '10',
|
|
90
|
+
name: 'Org 10',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: '11',
|
|
94
|
+
name: 'Org 11',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: '12',
|
|
98
|
+
name: 'Org 12',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: '13',
|
|
102
|
+
name: 'Org 13',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: '14',
|
|
106
|
+
name: 'Org 14',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: '15',
|
|
110
|
+
name: 'Org 15',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
async function sleep(duration = 2500) {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, duration));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const getResults = (page: number, limit: number) => {
|
|
119
|
+
const offset = page * limit;
|
|
120
|
+
const orgs = organizations.slice(page * offset, page * offset + limit);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
totalCount: organizations.length,
|
|
124
|
+
offset,
|
|
125
|
+
limit,
|
|
126
|
+
orgs,
|
|
127
|
+
count: orgs.length,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const loadOptions = async (page: number, limit: number) => {
|
|
132
|
+
await sleep(1000);
|
|
133
|
+
|
|
134
|
+
const { orgs, totalCount, offset } = getResults(page, limit);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
options: orgs,
|
|
138
|
+
hasMore: offset + limit < totalCount,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const _Async: StoryObj<typeof AsyncAutocomplete> = {
|
|
143
|
+
render: (args) => {
|
|
144
|
+
return <AsyncAutocomplete {...args} />;
|
|
145
|
+
},
|
|
146
|
+
args: {
|
|
147
|
+
FieldProps: { label: 'Async Select', helperText: 'Helper Text', fullWidth: false },
|
|
148
|
+
getOptionLabel: (val: Org) => val.name,
|
|
149
|
+
loadOptions,
|
|
150
|
+
limit: 10,
|
|
151
|
+
},
|
|
152
|
+
};
|
package/src/lib/Autocomplete.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { forwardRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
default as MuiAutocomplete,
|
|
4
4
|
AutocompleteProps as MuiAutocompleteProps,
|
|
5
5
|
AutocompleteRenderInputParams,
|
|
6
6
|
AutocompletePropsSizeOverrides,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from '@mui/material';
|
|
7
|
+
} from '@mui/material/Autocomplete';
|
|
8
|
+
import CircularProgress from '@mui/material/CircularProgress';
|
|
9
|
+
import { default as MuiIconButton, IconButtonProps as MuiIconButtonProps } from '@mui/material/IconButton';
|
|
10
|
+
import { ChipTypeMap } from '@mui/material/Chip';
|
|
11
11
|
import { OverridableStringUnion } from '@mui/types';
|
|
12
12
|
import { TextField, TextFieldProps } from '@availity/mui-textfield';
|
|
13
13
|
import { SelectDivider, SelectExpandIcon } from '@availity/mui-form-utils';
|
|
@@ -51,6 +51,10 @@ const PopupIndicatorWrapper = forwardRef<HTMLButtonElement, MuiIconButtonProps>(
|
|
|
51
51
|
</>
|
|
52
52
|
));
|
|
53
53
|
|
|
54
|
+
const progressSx = { marginRight: '.5rem' };
|
|
55
|
+
|
|
56
|
+
const LoadingIndicator = () => <CircularProgress size={20} sx={progressSx} />;
|
|
57
|
+
|
|
54
58
|
export const Autocomplete = <
|
|
55
59
|
T,
|
|
56
60
|
Multiple extends boolean | undefined = false,
|
|
@@ -72,6 +76,14 @@ export const Autocomplete = <
|
|
|
72
76
|
InputProps: {
|
|
73
77
|
...FieldProps?.InputProps,
|
|
74
78
|
...params?.InputProps,
|
|
79
|
+
endAdornment: props.loading ? (
|
|
80
|
+
<>
|
|
81
|
+
{params?.InputProps.endAdornment || null}
|
|
82
|
+
<LoadingIndicator />
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
params?.InputProps.endAdornment || null
|
|
86
|
+
),
|
|
75
87
|
},
|
|
76
88
|
inputProps: {
|
|
77
89
|
...FieldProps?.inputProps,
|