@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 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` & `@emotion/react` to your project if not already installed._
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 { ChipTypeMap, AutocompleteProps as AutocompleteProps$1 } from '@mui/material';
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
- export { Autocomplete, AutocompleteProps };
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 import_material = require("@mui/material");
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)(import_material.IconButton, {
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)(import_material.Autocomplete, {
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
- Autocomplete as MuiAutocomplete,
5
- IconButton as MuiIconButton
6
- } from "@mui/material";
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.1",
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
@@ -1 +1,2 @@
1
1
  export * from './lib/Autocomplete';
2
+ export * from './lib/AsyncAutocomplete';
@@ -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
+ };
@@ -1,13 +1,13 @@
1
1
  import { forwardRef } from 'react';
2
2
  import {
3
- Autocomplete as MuiAutocomplete,
3
+ default as MuiAutocomplete,
4
4
  AutocompleteProps as MuiAutocompleteProps,
5
5
  AutocompleteRenderInputParams,
6
6
  AutocompletePropsSizeOverrides,
7
- IconButton as MuiIconButton,
8
- IconButtonProps as MuiIconButtonProps,
9
- ChipTypeMap,
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,