@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/dist/components/SelectInput/SelectInput.d.ts +32 -6
- package/dist/components/SelectInput/SelectInput.stories.d.ts +2 -0
- package/dist/hyphen-components.cjs.development.js +54 -47
- package/dist/hyphen-components.cjs.development.js.map +1 -1
- package/dist/hyphen-components.cjs.production.min.js +1 -1
- package/dist/hyphen-components.cjs.production.min.js.map +1 -1
- package/dist/hyphen-components.esm.js +54 -47
- package/dist/hyphen-components.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/SelectInput/SelectInput.mdx +12 -0
- package/src/components/SelectInput/SelectInput.stories.tsx +86 -0
- package/src/components/SelectInput/SelectInput.test.tsx +32 -1
- package/src/components/SelectInput/SelectInput.tsx +74 -33
package/package.json
CHANGED
|
@@ -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 {
|
|
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, {
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 =
|
|
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
|
+
}
|