@hyphen/hyphen-components 2.15.6 → 2.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "2.15.6",
3
+ "version": "2.16.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -69,6 +69,18 @@ Use the `isMulti` and `isCreatable` props to allow the selection of multiple opt
69
69
 
70
70
  <Canvas of={Stories.MultiSelectCreatable} />
71
71
 
72
+ ## Async Select
73
+
74
+ Use the `options` prop to pass a function that returns a promise to load options asynchronously.
75
+
76
+ <Canvas of={Stories.AsyncSelect} />
77
+
78
+ ## Async Creatable Select
79
+
80
+ Use the `isCreatable` prop to allow the creation of new options and the `options` prop to pass a function that returns a promise to load options asynchronously.
81
+
82
+ <Canvas of={Stories.AsyncCreatableSelect} />
83
+
72
84
  ## Autofocus
73
85
 
74
86
  Use the `autoFocus` prop to autofocus a SelectInput.
@@ -143,6 +143,92 @@ export const CreatableSelect = () => {
143
143
  );
144
144
  };
145
145
 
146
+ export const AsyncSelect = () => {
147
+ type Option = {
148
+ value: string;
149
+ label: string;
150
+ };
151
+
152
+ const [value, setValue] = useState(null);
153
+ const options = [
154
+ { value: 'chocolate', label: 'Chocolate' },
155
+ { value: 'strawberry', label: 'Strawberry' },
156
+ ];
157
+
158
+ const filterOptions = (inputValue: string) => {
159
+ return options.filter((i) =>
160
+ i.label.toLowerCase().includes(inputValue.toLowerCase())
161
+ );
162
+ };
163
+ const loadOptions = (inputValue: string) => {
164
+ return new Promise<Option[]>((resolve) => {
165
+ setTimeout(() => {
166
+ resolve(filterOptions(inputValue));
167
+ }, 1000);
168
+ });
169
+ };
170
+
171
+ return (
172
+ <div style={{ height: '200px' }}>
173
+ <SelectInput
174
+ id="asyncSelect"
175
+ label="Label"
176
+ value={value}
177
+ // @ts-ignore - TS is not recognizing the value as a valid option
178
+ onChange={(event) => setValue(event.target.value)}
179
+ options={loadOptions}
180
+ defaultOptions
181
+ cacheOptions
182
+ isAsync
183
+ />
184
+ </div>
185
+ );
186
+ };
187
+
188
+ export const AsyncCreatableSelect = () => {
189
+ type Option = {
190
+ value: string;
191
+ label: string;
192
+ };
193
+
194
+ const options = [
195
+ { value: 'chocolate', label: 'Chocolate' },
196
+ { value: 'strawberry', label: 'Strawberry' },
197
+ ];
198
+
199
+ const [value, setValue] = useState(null);
200
+
201
+ const filterOptions = (inputValue: string) => {
202
+ return options.filter((i) =>
203
+ i.label.toLowerCase().includes(inputValue.toLowerCase())
204
+ );
205
+ };
206
+ const loadOptions = (inputValue: string) => {
207
+ return new Promise<Option[]>((resolve) => {
208
+ setTimeout(() => {
209
+ resolve(filterOptions(inputValue));
210
+ }, 1000);
211
+ });
212
+ };
213
+
214
+ return (
215
+ <div style={{ height: '200px' }}>
216
+ <SelectInput
217
+ id="asyncCreateSelect"
218
+ label="Label"
219
+ value={value}
220
+ // @ts-ignore - TS is not recognizing the value as a valid option
221
+ onChange={(event) => setValue(event.target.value)}
222
+ options={loadOptions}
223
+ isCreatable
224
+ defaultOptions
225
+ cacheOptions
226
+ isAsync
227
+ />
228
+ </div>
229
+ );
230
+ };
231
+
146
232
  export const MultiSelect = () => {
147
233
  const [value, setValue] = useState(null);
148
234
  const options = [
@@ -1,5 +1,11 @@
1
1
  import React from 'react';
2
- import { render, fireEvent, screen, Matcher } from '@testing-library/react';
2
+ import {
3
+ render,
4
+ fireEvent,
5
+ screen,
6
+ Matcher,
7
+ waitFor,
8
+ } from '@testing-library/react';
3
9
  import selectEvent from 'react-select-event';
4
10
  import { SelectInput, TextInputSize } from './SelectInput';
5
11
 
@@ -200,6 +206,31 @@ describe('SelectInput', () => {
200
206
  });
201
207
  });
202
208
 
209
+ describe('Async select', () => {
210
+ it('it renders with loading state', async () => {
211
+ const mockedHandleChange = jest.fn();
212
+ const loadOptions = jest.fn(() => Promise.resolve([]));
213
+
214
+ const { getByLabelText } = render(
215
+ <SelectInput
216
+ id="testId"
217
+ onChange={mockedHandleChange}
218
+ label="Select Label"
219
+ options={loadOptions}
220
+ value={''}
221
+ isAsync
222
+ />
223
+ );
224
+
225
+ const inputElement = getByLabelText('Select Label');
226
+ fireEvent.change(inputElement, { target: { value: 'test' } });
227
+
228
+ await waitFor(() => {
229
+ expect(loadOptions).toHaveBeenCalledTimes(1);
230
+ });
231
+ });
232
+ });
233
+
203
234
  describe('Multi select, no selection', () => {
204
235
  it('it renders input with a label, and with a default placeholder', () => {
205
236
  const mockedHandleChange = jest.fn();
@@ -1,4 +1,4 @@
1
- import React, { FC, FocusEvent, ReactNode, FocusEventHandler } from 'react';
1
+ import React, { FocusEvent, ReactNode, FocusEventHandler } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import Select, {
4
4
  components,
@@ -6,6 +6,8 @@ import Select, {
6
6
  OptionsOrGroups,
7
7
  OnChangeValue,
8
8
  } from 'react-select';
9
+ import AsyncCreatableSelect from 'react-select/async-creatable';
10
+ import AsyncSelect from 'react-select/async';
9
11
  import CreatableSelect from 'react-select/creatable';
10
12
  import { ResponsiveProp } from '../../types';
11
13
  import { generateResponsiveClasses, Z_INDEX_VALUES } from '../../lib';
@@ -49,10 +51,6 @@ export interface SelectInputProps {
49
51
  * Callback function to call on change event.
50
52
  */
51
53
  onChange: (event: SimulatedEventPayloadType) => void;
52
- /**
53
- * Options for dropdown list.
54
- */
55
- options: SelectInputOptions;
56
54
  /**
57
55
  * The value(s) of select.
58
56
  */
@@ -135,31 +133,67 @@ export interface SelectInputProps {
135
133
  [x: string]: any; // eslint-disable-line
136
134
  }
137
135
 
138
- export const SelectInput: FC<SelectInputProps> = ({
139
- id,
140
- label,
141
- onChange,
142
- options,
143
- value,
144
- autoFocus = false,
145
- className = '',
146
- error = false,
147
- helpText,
148
- hideLabel = false,
149
- isClearable = false,
150
- isCreatable = false,
151
- isDisabled = false,
152
- isMulti = false,
153
- isRequired = false,
154
- menuPortalTarget = null,
155
- name = '',
156
- onFocus = null,
157
- onBlur = null,
158
- placeholder = undefined,
159
- requiredIndicator = ' *',
160
- size = 'md',
161
- ...restProps
162
- }) => {
136
+ type AsyncOptions = (inputValue: string) => Promise<SelectInputOptions>;
137
+ type AsyncSelectInputProps = SelectInputProps & {
138
+ /**
139
+ * Load the input asynchronously.
140
+ */
141
+ isAsync: true;
142
+ /**
143
+ * Load options asynchronously.
144
+ */
145
+ options: AsyncOptions;
146
+ /**
147
+ * If cacheOptions is passed, then the loaded data will be cached.
148
+ */
149
+ cacheOptions?: boolean;
150
+ /**
151
+ * The default set of options to show before the user starts searching.
152
+ */
153
+ defaultOptions?: boolean;
154
+ };
155
+
156
+ type SyncSelectInputProps = SelectInputProps & {
157
+ /**
158
+ * Load the input synchronously.
159
+ */
160
+ isAsync?: false;
161
+ /**
162
+ * Options for dropdown list.
163
+ */
164
+ options: SelectInputOptions;
165
+ };
166
+
167
+ export function SelectInput(props: AsyncSelectInputProps): JSX.Element;
168
+ export function SelectInput(props: SyncSelectInputProps): JSX.Element;
169
+ export function SelectInput(props: SelectInputProps): JSX.Element {
170
+ const {
171
+ id,
172
+ label,
173
+ onChange,
174
+ options,
175
+ value,
176
+ autoFocus = false,
177
+ className = '',
178
+ error = false,
179
+ helpText,
180
+ hideLabel = false,
181
+ isClearable = false,
182
+ isAsync = false,
183
+ isCreatable = false,
184
+ isDisabled = false,
185
+ isMulti = false,
186
+ isRequired = false,
187
+ menuPortalTarget = null,
188
+ name = '',
189
+ onFocus = null,
190
+ onBlur = null,
191
+ placeholder = undefined,
192
+ requiredIndicator = ' *',
193
+ size = 'md',
194
+ ...restProps
195
+ } = props;
196
+
163
197
  const handleChange = (values: OnChangeValue<SelectInputOptions, boolean>) => {
164
198
  const simulatedEventPayloadType: SimulatedEventPayloadType = {
165
199
  target: {
@@ -212,13 +246,21 @@ export const SelectInput: FC<SelectInputProps> = ({
212
246
  </components.ClearIndicator>
213
247
  );
214
248
 
215
- const Component = isCreatable ? CreatableSelect : Select;
249
+ const Component =
250
+ isCreatable && isAsync
251
+ ? AsyncCreatableSelect
252
+ : isCreatable
253
+ ? CreatableSelect
254
+ : isAsync
255
+ ? AsyncSelect
256
+ : Select;
216
257
 
217
258
  return (
218
259
  <Box width="100%" className={wrapperClasses}>
219
260
  {label && !hideLabel && <FormLabel {...labelProps}>{label}</FormLabel>}
220
261
  <Component
221
262
  {...restProps}
263
+ {...(isAsync ? { loadOptions: options } : { options })}
222
264
  inputId={id}
223
265
  aria-label={label}
224
266
  components={{ ClearIndicator }}
@@ -232,7 +274,6 @@ export const SelectInput: FC<SelectInputProps> = ({
232
274
  menuPortalTarget={menuPortalTarget}
233
275
  name={name}
234
276
  autoFocus={autoFocus}
235
- options={options}
236
277
  onChange={handleChange}
237
278
  onFocus={handleFocus}
238
279
  onBlur={handleBlur}
@@ -249,4 +290,4 @@ export const SelectInput: FC<SelectInputProps> = ({
249
290
  )}
250
291
  </Box>
251
292
  );
252
- };
293
+ }