@availity/mui-autocomplete 0.3.2 → 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,13 @@
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
+
5
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)
6
13
 
7
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`, @availity/mui-form-utils, & @availity/mui-textfield 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
@@ -10,4 +10,16 @@ interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClea
10
10
  }
11
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;
12
12
 
13
- 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
@@ -26,6 +26,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
26
26
  // src/index.ts
27
27
  var src_exports = {};
28
28
  __export(src_exports, {
29
+ AsyncAutocomplete: () => AsyncAutocomplete,
29
30
  Autocomplete: () => Autocomplete
30
31
  });
31
32
  module.exports = __toCommonJS(src_exports);
@@ -33,6 +34,7 @@ module.exports = __toCommonJS(src_exports);
33
34
  // src/lib/Autocomplete.tsx
34
35
  var import_react = require("react");
35
36
  var import_Autocomplete = __toESM(require("@mui/material/Autocomplete"));
37
+ var import_CircularProgress = __toESM(require("@mui/material/CircularProgress"));
36
38
  var import_IconButton = __toESM(require("@mui/material/IconButton"));
37
39
  var import_mui_textfield = require("@availity/mui-textfield");
38
40
  var import_mui_form_utils = require("@availity/mui-form-utils");
@@ -49,6 +51,11 @@ var PopupIndicatorWrapper = (0, import_react.forwardRef)((props, ref) => /* @__P
49
51
  })
50
52
  ]
51
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
+ });
52
59
  var Autocomplete = ({
53
60
  FieldProps,
54
61
  ...props
@@ -60,7 +67,13 @@ var Autocomplete = ({
60
67
  const resolvedProps = (params) => ({
61
68
  InputProps: {
62
69
  ...FieldProps == null ? void 0 : FieldProps.InputProps,
63
- ...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
64
77
  },
65
78
  inputProps: {
66
79
  ...FieldProps == null ? void 0 : FieldProps.inputProps,
@@ -81,7 +94,54 @@ var Autocomplete = ({
81
94
  ...defaultProps
82
95
  });
83
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
+ };
84
143
  // Annotate the CommonJS export names for ESM import in node:
85
144
  0 && (module.exports = {
145
+ AsyncAutocomplete,
86
146
  Autocomplete
87
147
  });
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { forwardRef } from "react";
3
3
  import {
4
4
  default as MuiAutocomplete
5
5
  } from "@mui/material/Autocomplete";
6
+ import CircularProgress from "@mui/material/CircularProgress";
6
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";
@@ -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.2",
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",
@@ -43,8 +43,8 @@
43
43
  "typescript": "^4.6.4"
44
44
  },
45
45
  "peerDependencies": {
46
- "@availity/mui-form-utils": "^0.10.0",
47
- "@availity/mui-textfield": "^0.5.14",
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
+ };
@@ -5,6 +5,7 @@ import {
5
5
  AutocompleteRenderInputParams,
6
6
  AutocompletePropsSizeOverrides,
7
7
  } from '@mui/material/Autocomplete';
8
+ import CircularProgress from '@mui/material/CircularProgress';
8
9
  import { default as MuiIconButton, IconButtonProps as MuiIconButtonProps } from '@mui/material/IconButton';
9
10
  import { ChipTypeMap } from '@mui/material/Chip';
10
11
  import { OverridableStringUnion } from '@mui/types';
@@ -50,6 +51,10 @@ const PopupIndicatorWrapper = forwardRef<HTMLButtonElement, MuiIconButtonProps>(
50
51
  </>
51
52
  ));
52
53
 
54
+ const progressSx = { marginRight: '.5rem' };
55
+
56
+ const LoadingIndicator = () => <CircularProgress size={20} sx={progressSx} />;
57
+
53
58
  export const Autocomplete = <
54
59
  T,
55
60
  Multiple extends boolean | undefined = false,
@@ -71,6 +76,14 @@ export const Autocomplete = <
71
76
  InputProps: {
72
77
  ...FieldProps?.InputProps,
73
78
  ...params?.InputProps,
79
+ endAdornment: props.loading ? (
80
+ <>
81
+ {params?.InputProps.endAdornment || null}
82
+ <LoadingIndicator />
83
+ </>
84
+ ) : (
85
+ params?.InputProps.endAdornment || null
86
+ ),
74
87
  },
75
88
  inputProps: {
76
89
  ...FieldProps?.inputProps,