@ssa-ui-kit/core 1.0.7 → 1.0.9
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/Typeahead/Typeahead.context.d.ts +1 -0
- package/dist/components/Typeahead/Typeahead.d.ts +1 -1
- package/dist/components/Typeahead/types.d.ts +2 -1
- package/dist/components/Typeahead/useTypeahead.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Typeahead/Typeahead.context.ts +3 -0
- package/src/components/Typeahead/Typeahead.spec.tsx +21 -7
- package/src/components/Typeahead/Typeahead.stories.tsx +43 -3
- package/src/components/Typeahead/Typeahead.tsx +2 -0
- package/src/components/Typeahead/components/MultipleTrigger.tsx +5 -3
- package/src/components/Typeahead/components/SingleTrigger.tsx +5 -3
- package/src/components/Typeahead/types.ts +2 -0
- package/src/components/Typeahead/useTypeahead.tsx +22 -10
- package/tsbuildcache +1 -1
package/package.json
CHANGED
|
@@ -60,7 +60,9 @@ describe('Typeahead', () => {
|
|
|
60
60
|
it('Renders without a selected item', async () => {
|
|
61
61
|
const { user, mockOnChange, getByRole, queryByRole, getByTestId } = setup();
|
|
62
62
|
|
|
63
|
-
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', undefined
|
|
63
|
+
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', undefined, {
|
|
64
|
+
shouldDirty: false,
|
|
65
|
+
});
|
|
64
66
|
|
|
65
67
|
const mainElement = getByTestId('typeahead');
|
|
66
68
|
|
|
@@ -104,7 +106,9 @@ describe('Typeahead', () => {
|
|
|
104
106
|
label: 'Label',
|
|
105
107
|
});
|
|
106
108
|
|
|
107
|
-
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs
|
|
109
|
+
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
|
|
110
|
+
shouldDirty: false,
|
|
111
|
+
});
|
|
108
112
|
|
|
109
113
|
let mainElement = getByTestId('typeahead');
|
|
110
114
|
|
|
@@ -164,7 +168,9 @@ describe('Typeahead', () => {
|
|
|
164
168
|
/>,
|
|
165
169
|
);
|
|
166
170
|
|
|
167
|
-
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', []
|
|
171
|
+
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', [], {
|
|
172
|
+
shouldDirty: false,
|
|
173
|
+
});
|
|
168
174
|
|
|
169
175
|
let mainElement = getByTestId('typeahead');
|
|
170
176
|
|
|
@@ -203,7 +209,9 @@ describe('Typeahead', () => {
|
|
|
203
209
|
label: 'Label',
|
|
204
210
|
});
|
|
205
211
|
|
|
206
|
-
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs
|
|
212
|
+
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
|
|
213
|
+
shouldDirty: false,
|
|
214
|
+
});
|
|
207
215
|
|
|
208
216
|
const mainElement = getByTestId('typeahead');
|
|
209
217
|
|
|
@@ -243,7 +251,9 @@ describe('Typeahead', () => {
|
|
|
243
251
|
label: 'Label',
|
|
244
252
|
});
|
|
245
253
|
|
|
246
|
-
expect(mockOnChange).lastCalledWith('typeahead-dropdown', selectedIDs[0]
|
|
254
|
+
expect(mockOnChange).lastCalledWith('typeahead-dropdown', selectedIDs[0], {
|
|
255
|
+
shouldDirty: true,
|
|
256
|
+
});
|
|
247
257
|
|
|
248
258
|
let inputEl = screen.queryByTestId('typeahead-input');
|
|
249
259
|
expect(inputEl).toHaveValue('First');
|
|
@@ -345,7 +355,9 @@ describe('Typeahead', () => {
|
|
|
345
355
|
label: 'Label',
|
|
346
356
|
});
|
|
347
357
|
|
|
348
|
-
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs
|
|
358
|
+
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
|
|
359
|
+
shouldDirty: false,
|
|
360
|
+
});
|
|
349
361
|
|
|
350
362
|
let mainElement = getByTestId('typeahead');
|
|
351
363
|
|
|
@@ -448,7 +460,9 @@ describe('Typeahead', () => {
|
|
|
448
460
|
label: 'Label',
|
|
449
461
|
});
|
|
450
462
|
|
|
451
|
-
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs
|
|
463
|
+
expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
|
|
464
|
+
shouldDirty: false,
|
|
465
|
+
});
|
|
452
466
|
|
|
453
467
|
let mainElement = getByTestId('typeahead');
|
|
454
468
|
let toggleElement = within(mainElement).getByRole('combobox');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
FieldError,
|
|
4
4
|
FieldValues,
|
|
@@ -63,6 +63,9 @@ export const Basic: StoryObj = (args: TypeaheadProps) => {
|
|
|
63
63
|
<Typeahead
|
|
64
64
|
initialSelectedItems={[items[2].id]}
|
|
65
65
|
isDisabled={args.isDisabled}
|
|
66
|
+
onBlur={() => {
|
|
67
|
+
console.log('>>>onBlur event');
|
|
68
|
+
}}
|
|
66
69
|
name={'typeahead-dropdown'}
|
|
67
70
|
label="Label"
|
|
68
71
|
helperText="Helper Text"
|
|
@@ -86,19 +89,56 @@ Basic.args = { isDisabled: false };
|
|
|
86
89
|
|
|
87
90
|
export const Multiple: StoryObj = (args: TypeaheadProps) => {
|
|
88
91
|
const useFormResult = useForm<FieldValues>();
|
|
89
|
-
const {
|
|
92
|
+
const {
|
|
93
|
+
handleSubmit,
|
|
94
|
+
register,
|
|
95
|
+
setValue,
|
|
96
|
+
setError,
|
|
97
|
+
clearErrors,
|
|
98
|
+
watch,
|
|
99
|
+
formState: { errors, isDirty },
|
|
100
|
+
} = useFormResult;
|
|
101
|
+
const fieldName = 'typeahead-dropdown';
|
|
102
|
+
const error = errors[fieldName]
|
|
103
|
+
? {
|
|
104
|
+
type: errors[fieldName].type,
|
|
105
|
+
message: errors[fieldName].message,
|
|
106
|
+
}
|
|
107
|
+
: undefined;
|
|
108
|
+
|
|
90
109
|
const onSubmit: SubmitHandler<FieldValues> = (data) => console.log(data);
|
|
110
|
+
const fieldWatch = watch(fieldName);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (isDirty) {
|
|
113
|
+
if (Array.isArray(fieldWatch) && !fieldWatch.length) {
|
|
114
|
+
setError(fieldName, {
|
|
115
|
+
message: 'Required field',
|
|
116
|
+
type: 'required',
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
clearErrors(fieldName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, [fieldWatch, isDirty]);
|
|
123
|
+
|
|
91
124
|
return (
|
|
92
125
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
93
126
|
<Typeahead
|
|
94
127
|
initialSelectedItems={[items[2].id, items[1].id]}
|
|
95
128
|
isMultiple
|
|
96
129
|
isDisabled={args.isDisabled}
|
|
130
|
+
onBlur={() => {
|
|
131
|
+
console.log('>>>onBlur event');
|
|
132
|
+
}}
|
|
97
133
|
label="Label"
|
|
98
134
|
helperText="Helper Text"
|
|
99
135
|
register={register}
|
|
100
136
|
setValue={setValue}
|
|
101
|
-
|
|
137
|
+
validationSchema={{
|
|
138
|
+
required: 'Required',
|
|
139
|
+
}}
|
|
140
|
+
name={fieldName}
|
|
141
|
+
error={error as FieldError}
|
|
102
142
|
renderOption={({ label, input }) => highlightInputMatch(label, input)}>
|
|
103
143
|
{items.map(({ label, value, id }) => (
|
|
104
144
|
<TypeaheadOption key={id} value={id} label={label || value}>
|
|
@@ -45,6 +45,7 @@ export const Typeahead = ({
|
|
|
45
45
|
setValue,
|
|
46
46
|
register,
|
|
47
47
|
onChange,
|
|
48
|
+
onBlur,
|
|
48
49
|
renderOption,
|
|
49
50
|
}: TypeaheadProps) => {
|
|
50
51
|
const theme = useTheme();
|
|
@@ -67,6 +68,7 @@ export const Typeahead = ({
|
|
|
67
68
|
setValue,
|
|
68
69
|
register,
|
|
69
70
|
onChange,
|
|
71
|
+
onBlur,
|
|
70
72
|
renderOption,
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -67,6 +67,7 @@ export const MultipleTrigger = () => {
|
|
|
67
67
|
onClick: context.handleInputClick,
|
|
68
68
|
onKeyDown: context.handleInputKeyDown,
|
|
69
69
|
onChange: context.handleInputChange,
|
|
70
|
+
onBlur: context.handleInputBlur,
|
|
70
71
|
value: context.inputValue,
|
|
71
72
|
autoComplete: 'off',
|
|
72
73
|
className: ['typeahead-input', S.TypeaheadInput(theme)].join(' '),
|
|
@@ -78,9 +79,10 @@ export const MultipleTrigger = () => {
|
|
|
78
79
|
<input
|
|
79
80
|
type="text"
|
|
80
81
|
data-testid="typeahead-input"
|
|
81
|
-
aria-hidden
|
|
82
|
+
aria-hidden={context.isOpen}
|
|
82
83
|
readOnly
|
|
83
84
|
value={context.firstSuggestion}
|
|
85
|
+
tabIndex={-1}
|
|
84
86
|
disabled={context.isDisabled}
|
|
85
87
|
className={[
|
|
86
88
|
'typeahead-input',
|
|
@@ -91,7 +93,6 @@ export const MultipleTrigger = () => {
|
|
|
91
93
|
/>
|
|
92
94
|
<input
|
|
93
95
|
type="hidden"
|
|
94
|
-
aria-hidden
|
|
95
96
|
readOnly
|
|
96
97
|
value={context.selectedItems as string[]}
|
|
97
98
|
{...context.register?.(context.name, context.validationSchema)}
|
|
@@ -103,7 +104,8 @@ export const MultipleTrigger = () => {
|
|
|
103
104
|
data-testid="remove-all-button"
|
|
104
105
|
endIcon={<Icon name="cross" size={8} tooltip="Remove all" />}
|
|
105
106
|
css={{
|
|
106
|
-
padding: '0
|
|
107
|
+
padding: '0 10px',
|
|
108
|
+
marginRight: 4,
|
|
107
109
|
position: 'absolute',
|
|
108
110
|
right: 0,
|
|
109
111
|
zIndex: 10,
|
|
@@ -26,6 +26,7 @@ export const SingleTrigger = () => {
|
|
|
26
26
|
onClick: context.handleInputClick,
|
|
27
27
|
onKeyDown: context.handleInputKeyDown,
|
|
28
28
|
onChange: context.handleInputChange,
|
|
29
|
+
onBlur: context.handleInputBlur,
|
|
29
30
|
value: context.inputValue,
|
|
30
31
|
autoComplete: 'off',
|
|
31
32
|
className: ['typeahead-input', S.TypeaheadInput(theme)].join(' '),
|
|
@@ -37,9 +38,10 @@ export const SingleTrigger = () => {
|
|
|
37
38
|
<input
|
|
38
39
|
type="text"
|
|
39
40
|
data-testid="typeahead-input"
|
|
40
|
-
aria-hidden
|
|
41
|
+
aria-hidden={context.isOpen}
|
|
41
42
|
readOnly
|
|
42
43
|
value={context.firstSuggestion}
|
|
44
|
+
tabIndex={-1}
|
|
43
45
|
className={[
|
|
44
46
|
'typeahead-input',
|
|
45
47
|
S.TypeaheadInput(theme),
|
|
@@ -49,7 +51,6 @@ export const SingleTrigger = () => {
|
|
|
49
51
|
/>
|
|
50
52
|
<input
|
|
51
53
|
type="hidden"
|
|
52
|
-
aria-hidden
|
|
53
54
|
readOnly
|
|
54
55
|
value={(context.selectedItems[0] || '') as string | undefined}
|
|
55
56
|
{...context.register?.(context.name, context.validationSchema)}
|
|
@@ -60,7 +61,8 @@ export const SingleTrigger = () => {
|
|
|
60
61
|
data-testid="remove-all-button"
|
|
61
62
|
endIcon={<Icon name="cross" size={8} tooltip="Remove" />}
|
|
62
63
|
css={{
|
|
63
|
-
padding: '0
|
|
64
|
+
padding: '0 10px',
|
|
65
|
+
marginRight: 4,
|
|
64
66
|
position: 'absolute',
|
|
65
67
|
right: -28,
|
|
66
68
|
zIndex: 10,
|
|
@@ -34,6 +34,7 @@ export interface TypeaheadProps {
|
|
|
34
34
|
setValue?: UseFormSetValue<FieldValues>;
|
|
35
35
|
register?: UseFormReturn['register'];
|
|
36
36
|
onChange?: (selectedItem: TypeaheadValue, isSelected: boolean) => void;
|
|
37
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
|
37
38
|
renderOption?: (data: {
|
|
38
39
|
value: string | number;
|
|
39
40
|
input: string;
|
|
@@ -48,6 +49,7 @@ export type UseTypeaheadProps = Pick<
|
|
|
48
49
|
| 'children'
|
|
49
50
|
| 'isMultiple'
|
|
50
51
|
| 'onChange'
|
|
52
|
+
| 'onBlur'
|
|
51
53
|
| 'renderOption'
|
|
52
54
|
| 'isOpen'
|
|
53
55
|
| 'className'
|
|
@@ -29,6 +29,7 @@ export const useTypeahead = ({
|
|
|
29
29
|
register,
|
|
30
30
|
setValue,
|
|
31
31
|
onChange,
|
|
32
|
+
onBlur,
|
|
32
33
|
renderOption,
|
|
33
34
|
}: UseTypeaheadProps) => {
|
|
34
35
|
const inputName = `${name}-text`;
|
|
@@ -39,6 +40,7 @@ export const useTypeahead = ({
|
|
|
39
40
|
const [optionsWithKey, setOptionsWithKey] = useState<
|
|
40
41
|
Record<number | string, Record<string, string | number>>
|
|
41
42
|
>({});
|
|
43
|
+
const [isFirstRender, setFirstRender] = useState<boolean>(true);
|
|
42
44
|
const [items, setItems] = useState<Array<React.ReactElement> | undefined>();
|
|
43
45
|
const [inputValue, setInputValue] = useState<string>('');
|
|
44
46
|
const [status, setStatus] = useState<'basic' | 'success' | 'error'>('basic');
|
|
@@ -58,11 +60,15 @@ export const useTypeahead = ({
|
|
|
58
60
|
|
|
59
61
|
useEffect(() => {
|
|
60
62
|
if (isMultiple) {
|
|
61
|
-
setValue?.(name, selected
|
|
63
|
+
setValue?.(name, selected, {
|
|
64
|
+
shouldDirty: !isFirstRender,
|
|
65
|
+
});
|
|
62
66
|
setInputValue('');
|
|
63
67
|
setFirstSuggestion('');
|
|
64
68
|
} else {
|
|
65
|
-
setValue?.(name, selected.length ? selected[0] : undefined
|
|
69
|
+
setValue?.(name, selected.length ? selected[0] : undefined, {
|
|
70
|
+
shouldDirty: !isFirstRender,
|
|
71
|
+
});
|
|
66
72
|
}
|
|
67
73
|
}, [selected]);
|
|
68
74
|
|
|
@@ -85,6 +91,7 @@ export const useTypeahead = ({
|
|
|
85
91
|
if (error) {
|
|
86
92
|
useFormResult.setError(name, error);
|
|
87
93
|
} else {
|
|
94
|
+
setStatus('basic');
|
|
88
95
|
useFormResult.resetField(name);
|
|
89
96
|
}
|
|
90
97
|
}, [error]);
|
|
@@ -108,6 +115,7 @@ export const useTypeahead = ({
|
|
|
108
115
|
});
|
|
109
116
|
setOptionsWithKey(keyedOptions);
|
|
110
117
|
setItems(childItems);
|
|
118
|
+
setFirstRender(false);
|
|
111
119
|
}, [initialSelectedItems, children]);
|
|
112
120
|
|
|
113
121
|
useEffect(() => {
|
|
@@ -216,7 +224,8 @@ export const useTypeahead = ({
|
|
|
216
224
|
setFirstSuggestion('');
|
|
217
225
|
inputRef.current?.focus();
|
|
218
226
|
setStatus('basic');
|
|
219
|
-
useFormResult.clearErrors();
|
|
227
|
+
useFormResult.clearErrors(name);
|
|
228
|
+
useFormResult.trigger(name);
|
|
220
229
|
onChange && onChange(changingValue, isNewSelected);
|
|
221
230
|
};
|
|
222
231
|
|
|
@@ -230,6 +239,7 @@ export const useTypeahead = ({
|
|
|
230
239
|
setInputValue('');
|
|
231
240
|
setIsOpen(false);
|
|
232
241
|
setFirstSuggestion('');
|
|
242
|
+
useFormResult.trigger(name);
|
|
233
243
|
inputRef.current?.focus();
|
|
234
244
|
};
|
|
235
245
|
|
|
@@ -247,13 +257,16 @@ export const useTypeahead = ({
|
|
|
247
257
|
const handleInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (
|
|
248
258
|
event,
|
|
249
259
|
) => {
|
|
250
|
-
if (['
|
|
260
|
+
if (['Space'].includes(event.code) && !firstSuggestion) {
|
|
251
261
|
setIsOpen(true);
|
|
252
262
|
inputRef.current?.focus();
|
|
253
263
|
event.stopPropagation();
|
|
254
264
|
event.preventDefault();
|
|
255
|
-
}
|
|
256
|
-
|
|
265
|
+
} else if (
|
|
266
|
+
['Tab', 'Enter'].includes(event.code) &&
|
|
267
|
+
firstSuggestion &&
|
|
268
|
+
firstSuggestion !== inputValue
|
|
269
|
+
) {
|
|
257
270
|
const foundItem = Object.values(optionsWithKey).find(
|
|
258
271
|
(item) =>
|
|
259
272
|
`${item.label}`.toLowerCase() === firstSuggestion.toLowerCase(),
|
|
@@ -264,8 +277,7 @@ export const useTypeahead = ({
|
|
|
264
277
|
}
|
|
265
278
|
event.preventDefault();
|
|
266
279
|
return false;
|
|
267
|
-
}
|
|
268
|
-
if (
|
|
280
|
+
} else if (
|
|
269
281
|
isMultiple &&
|
|
270
282
|
event.code === 'Backspace' &&
|
|
271
283
|
selected.length > 0 &&
|
|
@@ -274,8 +286,7 @@ export const useTypeahead = ({
|
|
|
274
286
|
handleChange(selected[selected.length - 1]);
|
|
275
287
|
event.preventDefault();
|
|
276
288
|
return false;
|
|
277
|
-
}
|
|
278
|
-
if (!isOpen) {
|
|
289
|
+
} else if (!isOpen && firstSuggestion !== inputValue) {
|
|
279
290
|
setIsOpen(true);
|
|
280
291
|
}
|
|
281
292
|
};
|
|
@@ -330,6 +341,7 @@ export const useTypeahead = ({
|
|
|
330
341
|
handleInputChange,
|
|
331
342
|
handleInputClick,
|
|
332
343
|
handleInputKeyDown,
|
|
344
|
+
handleInputBlur: onBlur,
|
|
333
345
|
handleSelectedClick,
|
|
334
346
|
handleRemoveSelectedClick,
|
|
335
347
|
};
|