@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssa-ui-kit/core",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "private": false,
@@ -52,6 +52,9 @@ export const TypeaheadContext = React.createContext<UseTypeaheadResult>({
52
52
  handleRemoveSelectedClick: () => () => {
53
53
  /* no-op */
54
54
  },
55
+ handleInputBlur: () => {
56
+ /* no-op */
57
+ },
55
58
  handleSelectedClick: () => {
56
59
  /* no-op */
57
60
  },
@@ -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 { handleSubmit, register, setValue } = useFormResult;
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
- name={'typeahead-dropdown'}
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 14px 0 10px',
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 14px 0 10px',
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 (['Tab', 'Space'].includes(event.code) && !firstSuggestion) {
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
- if (['Tab', 'Enter'].includes(event.code) && firstSuggestion) {
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
  };