@lumx/react 3.2.1 → 3.3.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
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.2.1",
11
- "@lumx/icons": "^3.2.1",
10
+ "@lumx/core": "^3.3.0",
11
+ "@lumx/icons": "^3.3.0",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.2.6",
@@ -113,5 +113,5 @@
113
113
  "build:storybook": "cd storybook && ./build"
114
114
  },
115
115
  "sideEffects": false,
116
- "version": "3.2.1"
116
+ "version": "3.3.0"
117
117
  }
@@ -12,6 +12,7 @@ export default {
12
12
  clearButtonProps: { control: false },
13
13
  chips: { control: false },
14
14
  afterElement: { control: false },
15
+ onClear: { action: true },
15
16
  },
16
17
  decorators: [withValueOnChange({})],
17
18
  };
@@ -5,8 +5,14 @@ import camelCase from 'lodash/camelCase';
5
5
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
6
6
  import { getBasicClass } from '@lumx/react/utils/className';
7
7
 
8
- import { render } from '@testing-library/react';
9
- import { getByClassName, getByTagName, queryAllByClassName, queryByTagName } from '@lumx/react/testing/utils/queries';
8
+ import { render, screen } from '@testing-library/react';
9
+ import {
10
+ getByClassName,
11
+ getByTagName,
12
+ queryAllByClassName,
13
+ queryByClassName,
14
+ queryByTagName,
15
+ } from '@lumx/react/testing/utils/queries';
10
16
  import partition from 'lodash/partition';
11
17
  import userEvent from '@testing-library/user-event';
12
18
 
@@ -27,9 +33,11 @@ const setup = (propsOverride: Partial<TextFieldProps> = {}) => {
27
33
  | HTMLInputElement;
28
34
  const helpers = queryAllByClassName(container, 'lumx-text-field__helper');
29
35
  const [[helper], [error]] = partition(helpers, (h) => !h.className.includes('lumx-input-helper--color-red'));
36
+ const clearButton = queryByClassName(container, 'lumx-text-field__input-clear');
30
37
 
31
38
  return {
32
39
  props,
40
+ clearButton,
33
41
  container,
34
42
  element,
35
43
  inputNative,
@@ -106,22 +114,27 @@ describe(`<${TextField.displayName}>`, () => {
106
114
  });
107
115
 
108
116
  it('should have helper text', () => {
109
- const { helper } = setup({
117
+ const { helper, inputNative } = setup({
110
118
  helper: 'helper',
111
119
  label: 'test',
112
120
  placeholder: 'test',
113
121
  });
122
+
114
123
  expect(helper).toHaveTextContent('helper');
124
+ expect(inputNative).toHaveAttribute('aria-describedby');
115
125
  });
116
126
 
117
127
  it('should have error text', () => {
118
- const { error } = setup({
128
+ const { error, inputNative } = setup({
119
129
  error: 'error',
120
130
  hasError: true,
121
131
  label: 'test',
122
132
  placeholder: 'test',
123
133
  });
134
+
124
135
  expect(error).toHaveTextContent('error');
136
+ expect(inputNative).toHaveAttribute('aria-invalid', 'true');
137
+ expect(inputNative).toHaveAttribute('aria-describedby');
125
138
  });
126
139
 
127
140
  it('should not have error text', () => {
@@ -160,6 +173,41 @@ describe(`<${TextField.displayName}>`, () => {
160
173
 
161
174
  expect(onChange).toHaveBeenCalledWith('a', 'name', expect.objectContaining({}));
162
175
  });
176
+
177
+ it('should trigger `onChange` with empty value when text field is cleared', async () => {
178
+ const onChange = jest.fn();
179
+ const { clearButton } = setup({
180
+ value: 'initial value',
181
+ name: 'name',
182
+ clearButtonProps: { label: 'Clear' },
183
+ onChange,
184
+ });
185
+
186
+ expect(clearButton).toBeInTheDocument();
187
+
188
+ await userEvent.click(clearButton as HTMLElement);
189
+
190
+ expect(onChange).toHaveBeenCalledWith('');
191
+ });
192
+
193
+ it('should trigger `onChange` with empty value and `onClear` when text field is cleared', async () => {
194
+ const onChange = jest.fn();
195
+ const onClear = jest.fn();
196
+ const { clearButton } = setup({
197
+ value: 'initial value',
198
+ name: 'name',
199
+ clearButtonProps: { label: 'Clear' },
200
+ onChange,
201
+ onClear,
202
+ });
203
+
204
+ expect(clearButton).toBeInTheDocument();
205
+
206
+ await userEvent.click(clearButton as HTMLElement);
207
+
208
+ expect(onChange).toHaveBeenCalledWith('');
209
+ expect(onClear).toHaveBeenCalled();
210
+ });
163
211
  });
164
212
 
165
213
  // Common tests suite.
@@ -61,6 +61,8 @@ export interface TextFieldProps extends GenericProps, HasTheme {
61
61
  onBlur?(event: React.FocusEvent): void;
62
62
  /** On change callback. */
63
63
  onChange(value: string, name?: string, event?: SyntheticEvent): void;
64
+ /** On clear callback. */
65
+ onClear?(event?: SyntheticEvent): void;
64
66
  /** On focus callback. */
65
67
  onFocus?(event: React.FocusEvent): void;
66
68
  }
@@ -153,6 +155,8 @@ interface InputNativeProps {
153
155
  onChange(value: string, name?: string, event?: SyntheticEvent): void;
154
156
  onFocus?(value: React.FocusEvent): void;
155
157
  onBlur?(value: React.FocusEvent): void;
158
+ hasError?: boolean;
159
+ describedById?: string;
156
160
  }
157
161
 
158
162
  const renderInputNative: React.FC<InputNativeProps> = (props) => {
@@ -172,6 +176,8 @@ const renderInputNative: React.FC<InputNativeProps> = (props) => {
172
176
  recomputeNumberOfRows,
173
177
  type,
174
178
  name,
179
+ hasError,
180
+ describedById,
175
181
  ...forwardedProps
176
182
  } = props;
177
183
  // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -214,6 +220,8 @@ const renderInputNative: React.FC<InputNativeProps> = (props) => {
214
220
  onFocus: onTextFieldFocus,
215
221
  onBlur: onTextFieldBlur,
216
222
  onChange: handleChange,
223
+ 'aria-invalid': hasError ? 'true' : undefined,
224
+ 'aria-describedby': describedById,
217
225
  ref: mergeRefs(inputRef as any, ref) as any,
218
226
  };
219
227
  if (multiline) {
@@ -254,6 +262,7 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
254
262
  name,
255
263
  onBlur,
256
264
  onChange,
265
+ onClear,
257
266
  onFocus,
258
267
  placeholder,
259
268
  textFieldRef,
@@ -264,6 +273,17 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
264
273
  ...forwardedProps
265
274
  } = props;
266
275
  const textFieldId = useMemo(() => id || `text-field-${uid()}`, [id]);
276
+ /**
277
+ * Generate unique ids for both the helper and error texts, in order to
278
+ * later on add them to the input native as aria-describedby. If both the error and the helper are present,
279
+ * we want to first use the most important one, which is the errorId. That way, screen readers will read first
280
+ * the error and then the helper
281
+ */
282
+ const helperId = helper ? `text-field-helper-${uid()}` : undefined;
283
+ const errorId = error ? `text-field-error-${uid()}` : undefined;
284
+ const describedByIds = [errorId, helperId].filter(Boolean);
285
+ const describedById = describedByIds.length === 0 ? undefined : describedByIds.join(' ');
286
+
267
287
  const [isFocus, setFocus] = useState(false);
268
288
  const { rows, recomputeNumberOfRows } = useComputeNumberOfRows(multiline ? minimumRows || DEFAULT_MIN_ROWS : 0);
269
289
  const valueLength = (value || '').length;
@@ -275,12 +295,16 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
275
295
  * and remove focus from the clear button.
276
296
  * @param evt On clear event.
277
297
  */
278
- const onClear = (evt: React.ChangeEvent) => {
298
+ const handleClear = (evt: React.ChangeEvent) => {
279
299
  evt.nativeEvent.preventDefault();
280
300
  evt.nativeEvent.stopPropagation();
281
301
  (evt.currentTarget as HTMLElement).blur();
282
302
 
283
303
  onChange('');
304
+
305
+ if (onClear) {
306
+ onClear(evt);
307
+ }
284
308
  };
285
309
 
286
310
  return (
@@ -359,6 +383,8 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
359
383
  type,
360
384
  value,
361
385
  name,
386
+ hasError,
387
+ describedById,
362
388
  ...forwardedProps,
363
389
  })}
364
390
  </div>
@@ -383,6 +409,8 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
383
409
  type,
384
410
  value,
385
411
  name,
412
+ hasError,
413
+ describedById,
386
414
  ...forwardedProps,
387
415
  })}
388
416
  </div>
@@ -405,7 +433,7 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
405
433
  emphasis={Emphasis.low}
406
434
  size={Size.s}
407
435
  theme={theme}
408
- onClick={onClear}
436
+ onClick={handleClear}
409
437
  type="button"
410
438
  />
411
439
  )}
@@ -414,13 +442,13 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
414
442
  </div>
415
443
 
416
444
  {hasError && error && (
417
- <InputHelper className={`${CLASSNAME}__helper`} kind={Kind.error} theme={theme}>
445
+ <InputHelper className={`${CLASSNAME}__helper`} kind={Kind.error} theme={theme} id={errorId}>
418
446
  {error}
419
447
  </InputHelper>
420
448
  )}
421
449
 
422
450
  {helper && (
423
- <InputHelper className={`${CLASSNAME}__helper`} theme={theme}>
451
+ <InputHelper className={`${CLASSNAME}__helper`} theme={theme} id={helperId}>
424
452
  {helper}
425
453
  </InputHelper>
426
454
  )}