@lumx/react 3.3.1-alpha.0 → 3.3.1

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.
Files changed (117) hide show
  1. package/_internal/ClickAwayProvider.js.map +1 -1
  2. package/_internal/types.d.ts +0 -2
  3. package/index.d.ts +2 -0
  4. package/index.js +347 -75
  5. package/index.js.map +1 -1
  6. package/package.json +23 -19
  7. package/src/components/autocomplete/Autocomplete.test.tsx +55 -142
  8. package/src/components/autocomplete/AutocompleteMultiple.test.tsx +37 -75
  9. package/src/components/autocomplete/__mockData__/index.ts +6 -1
  10. package/src/components/badge/Badge.test.tsx +20 -64
  11. package/src/components/button/Button.test.tsx +44 -121
  12. package/src/components/button/ButtonGroup.test.tsx +16 -39
  13. package/src/components/button/IconButton.stories.tsx +7 -0
  14. package/src/components/button/IconButton.test.tsx +37 -78
  15. package/src/components/button/IconButton.tsx +8 -1
  16. package/src/components/checkbox/Checkbox.test.tsx +62 -67
  17. package/src/components/chip/Chip.test.tsx +89 -139
  18. package/src/components/chip/ChipGroup.test.tsx +27 -10
  19. package/src/components/date-picker/DatePicker.test.tsx +15 -23
  20. package/src/components/date-picker/DatePickerControlled.test.tsx +24 -20
  21. package/src/components/date-picker/DatePickerField.test.tsx +43 -27
  22. package/src/components/dialog/Dialog.test.tsx +36 -35
  23. package/src/components/divider/Divider.test.tsx +23 -69
  24. package/src/components/dropdown/Dropdown.test.tsx +30 -61
  25. package/src/components/expansion-panel/ExpansionPanel.test.tsx +12 -8
  26. package/src/components/flag/Flag.test.tsx +28 -53
  27. package/src/components/generic-block/GenericBlock.test.tsx +93 -89
  28. package/src/components/grid-column/GridColumn.stories.tsx +3 -3
  29. package/src/components/icon/Icon.test.tsx +80 -64
  30. package/src/components/index.ts +0 -2
  31. package/src/components/inline-list/InlineList.test.tsx +30 -17
  32. package/src/components/input-helper/InputHelper.test.tsx +21 -81
  33. package/src/components/input-label/InputLabel.test.tsx +19 -61
  34. package/src/components/lightbox/Lightbox.test.tsx +3 -2
  35. package/src/components/link/Link.test.tsx +47 -31
  36. package/src/components/link-preview/LinkPreview.test.tsx +51 -51
  37. package/src/components/message/Message.test.tsx +31 -52
  38. package/src/components/mosaic/Mosaic.test.tsx +56 -72
  39. package/src/components/notification/Notification.test.tsx +51 -82
  40. package/src/components/popover/Popover.tsx +7 -9
  41. package/src/components/progress-tracker/ProgressTracker.test.tsx +20 -33
  42. package/src/components/progress-tracker/ProgressTrackerProvider.test.tsx +61 -36
  43. package/src/components/progress-tracker/ProgressTrackerStep.test.tsx +19 -109
  44. package/src/components/progress-tracker/ProgressTrackerStepPanel.test.tsx +21 -58
  45. package/src/components/progress-tracker/ProgressTrackerStepPanel.tsx +1 -1
  46. package/src/components/radio-button/RadioButton.test.tsx +78 -92
  47. package/src/components/radio-button/RadioGroup.test.tsx +13 -59
  48. package/src/components/select/Select.test.tsx +115 -284
  49. package/src/components/select/SelectMultiple.stories.tsx +105 -2
  50. package/src/components/select/SelectMultiple.test.tsx +126 -322
  51. package/src/components/select/WithSelectContext.tsx +10 -4
  52. package/src/components/side-navigation/SideNavigation.test.tsx +22 -35
  53. package/src/components/side-navigation/SideNavigationItem.test.tsx +72 -139
  54. package/src/components/switch/Switch.test.tsx +70 -149
  55. package/src/components/table/Table.test.tsx +2 -0
  56. package/src/components/table/TableBody.test.tsx +18 -8
  57. package/src/components/table/TableCell.test.tsx +34 -9
  58. package/src/components/table/TableHeader.test.tsx +18 -8
  59. package/src/components/table/TableRow.test.tsx +28 -8
  60. package/src/components/tabs/Tab.test.tsx +27 -96
  61. package/src/components/tabs/TabList.test.tsx +21 -56
  62. package/src/components/tabs/TabPanel.test.tsx +20 -55
  63. package/src/components/tabs/TabPanel.tsx +1 -1
  64. package/src/components/tabs/TabProvider.test.tsx +158 -37
  65. package/src/components/tabs/test-utils.ts +39 -0
  66. package/src/components/text-field/TextField.stories.tsx +14 -5
  67. package/src/components/text-field/TextField.test.tsx +54 -8
  68. package/src/components/text-field/TextField.tsx +49 -5
  69. package/src/components/tooltip/Tooltip.test.tsx +134 -75
  70. package/src/components/tooltip/useInjectTooltipRef.tsx +9 -2
  71. package/src/components/uploader/Uploader.test.tsx +60 -48
  72. package/src/components/user-block/UserBlock.test.tsx +69 -13
  73. package/src/hooks/useFocusTrap.ts +2 -2
  74. package/src/testing/utils/commonTestsSuiteRTL.ts +18 -8
  75. package/src/testing/utils/index.ts +0 -1
  76. package/src/utils/flattenChildren.ts +5 -0
  77. package/src/components/autocomplete/__snapshots__/Autocomplete.test.tsx.snap +0 -213
  78. package/src/components/autocomplete/__snapshots__/AutocompleteMultiple.test.tsx.snap +0 -88
  79. package/src/components/badge/__snapshots__/Badge.test.tsx.snap +0 -11
  80. package/src/components/button/ButtonRoot.test.tsx +0 -203
  81. package/src/components/button/__snapshots__/Button.test.tsx.snap +0 -96
  82. package/src/components/button/__snapshots__/ButtonGroup.test.tsx.snap +0 -22
  83. package/src/components/button/__snapshots__/ButtonRoot.test.tsx.snap +0 -160
  84. package/src/components/button/__snapshots__/IconButton.test.tsx.snap +0 -83
  85. package/src/components/checkbox/__snapshots__/Checkbox.test.tsx.snap +0 -141
  86. package/src/components/chip/__snapshots__/Chip.test.tsx.snap +0 -12
  87. package/src/components/chip/__snapshots__/ChipGroup.test.tsx.snap +0 -29
  88. package/src/components/date-picker/__snapshots__/DatePicker.test.tsx.snap +0 -22
  89. package/src/components/date-picker/__snapshots__/DatePickerControlled.test.tsx.snap +0 -597
  90. package/src/components/date-picker/__snapshots__/DatePickerField.test.tsx.snap +0 -43
  91. package/src/components/divider/__snapshots__/Divider.test.tsx.snap +0 -9
  92. package/src/components/dropdown/__snapshots__/Dropdown.test.tsx.snap +0 -35
  93. package/src/components/icon/__snapshots__/Icon.test.tsx.snap +0 -49
  94. package/src/components/input-helper/__snapshots__/InputHelper.test.tsx.snap +0 -9
  95. package/src/components/input-label/__snapshots__/InputLabel.test.tsx.snap +0 -10
  96. package/src/components/link/__snapshots__/Link.test.tsx.snap +0 -29
  97. package/src/components/message/__snapshots__/Message.test.tsx.snap +0 -15
  98. package/src/components/notification/__snapshots__/Notification.test.tsx.snap +0 -34
  99. package/src/components/progress-tracker/__snapshots__/ProgressTracker.test.tsx.snap +0 -41
  100. package/src/components/progress-tracker/__snapshots__/ProgressTrackerStep.test.tsx.snap +0 -141
  101. package/src/components/progress-tracker/__snapshots__/ProgressTrackerStepPanel.test.tsx.snap +0 -25
  102. package/src/components/radio-button/__snapshots__/RadioButton.test.tsx.snap +0 -113
  103. package/src/components/radio-button/__snapshots__/RadioGroup.test.tsx.snap +0 -26
  104. package/src/components/select/__snapshots__/Select.test.tsx.snap +0 -43
  105. package/src/components/select/__snapshots__/SelectMultiple.test.tsx.snap +0 -87
  106. package/src/components/side-navigation/__snapshots__/SideNavigation.test.tsx.snap +0 -7
  107. package/src/components/side-navigation/__snapshots__/SideNavigationItem.test.tsx.snap +0 -30
  108. package/src/components/switch/__snapshots__/Switch.test.tsx.snap +0 -179
  109. package/src/components/tabs/__snapshots__/Tab.test.tsx.snap +0 -62
  110. package/src/components/tabs/__snapshots__/TabList.test.tsx.snap +0 -22
  111. package/src/components/tabs/__snapshots__/TabPanel.test.tsx.snap +0 -25
  112. package/src/components/tabs/test.mocks.ts +0 -33
  113. package/src/components/text-field/__snapshots__/TextField.test.tsx.snap +0 -42
  114. package/src/components/tooltip/__snapshots__/Tooltip.test.tsx.snap +0 -233
  115. package/src/components/uploader/__snapshots__/Uploader.test.tsx.snap +0 -14
  116. package/src/testing/utils/commonTestsSuite.ts +0 -71
  117. package/src/utils/flattenChildren.test.tsx +0 -58
@@ -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
  };
@@ -49,11 +50,19 @@ export const LabelAndHelper = {
49
50
  /**
50
51
  * With clear button
51
52
  */
52
- export const Clearable = {
53
- args: {
54
- value: 'Some value',
55
- clearButtonProps: { label: 'Clear' },
56
- },
53
+ export const Clearable = () => {
54
+ const inputRef = React.useRef(null);
55
+ const [value, setValue] = React.useState('Some value');
56
+
57
+ return (
58
+ <TextField
59
+ value={value}
60
+ clearButtonProps={{ label: 'Clear' }}
61
+ onChange={setValue}
62
+ inputRef={inputRef}
63
+ {...TextField.defaultProps}
64
+ />
65
+ );
57
66
  };
58
67
 
59
68
  /**
@@ -4,9 +4,14 @@ import camelCase from 'lodash/camelCase';
4
4
 
5
5
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
6
6
  import { getBasicClass } from '@lumx/react/utils/className';
7
-
8
7
  import { render } from '@testing-library/react';
9
- import { getByClassName, getByTagName, queryAllByClassName, queryByTagName } from '@lumx/react/testing/utils/queries';
8
+ import {
9
+ getByClassName,
10
+ getByTagName,
11
+ queryAllByClassName,
12
+ queryByClassName,
13
+ queryByTagName,
14
+ } from '@lumx/react/testing/utils/queries';
10
15
  import partition from 'lodash/partition';
11
16
  import userEvent from '@testing-library/user-event';
12
17
 
@@ -27,9 +32,11 @@ const setup = (propsOverride: Partial<TextFieldProps> = {}) => {
27
32
  | HTMLInputElement;
28
33
  const helpers = queryAllByClassName(container, 'lumx-text-field__helper');
29
34
  const [[helper], [error]] = partition(helpers, (h) => !h.className.includes('lumx-input-helper--color-red'));
35
+ const clearButton = queryByClassName(container, 'lumx-text-field__input-clear');
30
36
 
31
37
  return {
32
38
  props,
39
+ clearButton,
33
40
  container,
34
41
  element,
35
42
  inputNative,
@@ -43,7 +50,7 @@ describe(`<${TextField.displayName}>`, () => {
43
50
  it('should render defaults', () => {
44
51
  const { element, inputNative } = setup({ id: 'fixedId' });
45
52
  expect(element).toBeInTheDocument();
46
- expect(element).toMatchSnapshot();
53
+
47
54
  expect(element).not.toHaveClass(`${CLASSNAME}--is-valid`);
48
55
  expect(element).not.toHaveClass(`${CLASSNAME}--has-error`);
49
56
  expect(element).not.toHaveClass(`${CLASSNAME}--has-label`);
@@ -59,12 +66,11 @@ describe(`<${TextField.displayName}>`, () => {
59
66
  it('should render textarea', () => {
60
67
  const { element, inputNative } = setup({ id: 'fixedId', multiline: true });
61
68
  expect(element).toBeInTheDocument();
62
- expect(element).toMatchSnapshot();
69
+
63
70
  expect(inputNative.tagName).toBe('TEXTAREA');
64
71
  });
65
72
  });
66
73
 
67
- // 2. Test defaultProps value and important props custom values.
68
74
  describe('Props', () => {
69
75
  it('should add all class names (except has-error)', () => {
70
76
  const modifiedProps = {
@@ -106,22 +112,27 @@ describe(`<${TextField.displayName}>`, () => {
106
112
  });
107
113
 
108
114
  it('should have helper text', () => {
109
- const { helper } = setup({
115
+ const { helper, inputNative } = setup({
110
116
  helper: 'helper',
111
117
  label: 'test',
112
118
  placeholder: 'test',
113
119
  });
120
+
114
121
  expect(helper).toHaveTextContent('helper');
122
+ expect(inputNative).toHaveAttribute('aria-describedby');
115
123
  });
116
124
 
117
125
  it('should have error text', () => {
118
- const { error } = setup({
126
+ const { error, inputNative } = setup({
119
127
  error: 'error',
120
128
  hasError: true,
121
129
  label: 'test',
122
130
  placeholder: 'test',
123
131
  });
132
+
124
133
  expect(error).toHaveTextContent('error');
134
+ expect(inputNative).toHaveAttribute('aria-invalid', 'true');
135
+ expect(inputNative).toHaveAttribute('aria-describedby');
125
136
  });
126
137
 
127
138
  it('should not have error text', () => {
@@ -147,7 +158,6 @@ describe(`<${TextField.displayName}>`, () => {
147
158
  });
148
159
  });
149
160
 
150
- // 3. Test events.
151
161
  describe('Events', () => {
152
162
  it('should trigger `onChange` when text field is changed', async () => {
153
163
  const onChange = jest.fn();
@@ -160,6 +170,41 @@ describe(`<${TextField.displayName}>`, () => {
160
170
 
161
171
  expect(onChange).toHaveBeenCalledWith('a', 'name', expect.objectContaining({}));
162
172
  });
173
+
174
+ it('should trigger `onChange` with empty value when text field is cleared', async () => {
175
+ const onChange = jest.fn();
176
+ const { clearButton } = setup({
177
+ value: 'initial value',
178
+ name: 'name',
179
+ clearButtonProps: { label: 'Clear' },
180
+ onChange,
181
+ });
182
+
183
+ expect(clearButton).toBeInTheDocument();
184
+
185
+ await userEvent.click(clearButton as HTMLElement);
186
+
187
+ expect(onChange).toHaveBeenCalledWith('');
188
+ });
189
+
190
+ it('should trigger `onChange` with empty value and `onClear` when text field is cleared', async () => {
191
+ const onChange = jest.fn();
192
+ const onClear = jest.fn();
193
+ const { clearButton } = setup({
194
+ value: 'initial value',
195
+ name: 'name',
196
+ clearButtonProps: { label: 'Clear' },
197
+ onChange,
198
+ onClear,
199
+ });
200
+
201
+ expect(clearButton).toBeInTheDocument();
202
+
203
+ await userEvent.click(clearButton as HTMLElement);
204
+
205
+ expect(onChange).toHaveBeenCalledWith('');
206
+ expect(onClear).toHaveBeenCalled();
207
+ });
163
208
  });
164
209
 
165
210
  // Common tests suite.
@@ -167,5 +212,6 @@ describe(`<${TextField.displayName}>`, () => {
167
212
  baseClassName: CLASSNAME,
168
213
  forwardClassName: 'element',
169
214
  forwardAttributes: 'inputNative',
215
+ forwardRef: 'element',
170
216
  });
171
217
  });
@@ -1,4 +1,14 @@
1
- import React, { forwardRef, ReactNode, Ref, SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ ReactNode,
4
+ Ref,
5
+ RefObject,
6
+ SyntheticEvent,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
2
12
 
3
13
  import classNames from 'classnames';
4
14
  import get from 'lodash/get';
@@ -61,6 +71,8 @@ export interface TextFieldProps extends GenericProps, HasTheme {
61
71
  onBlur?(event: React.FocusEvent): void;
62
72
  /** On change callback. */
63
73
  onChange(value: string, name?: string, event?: SyntheticEvent): void;
74
+ /** On clear callback. */
75
+ onClear?(event?: SyntheticEvent): void;
64
76
  /** On focus callback. */
65
77
  onFocus?(event: React.FocusEvent): void;
66
78
  }
@@ -153,6 +165,8 @@ interface InputNativeProps {
153
165
  onChange(value: string, name?: string, event?: SyntheticEvent): void;
154
166
  onFocus?(value: React.FocusEvent): void;
155
167
  onBlur?(value: React.FocusEvent): void;
168
+ hasError?: boolean;
169
+ describedById?: string;
156
170
  }
157
171
 
158
172
  const renderInputNative: React.FC<InputNativeProps> = (props) => {
@@ -172,6 +186,8 @@ const renderInputNative: React.FC<InputNativeProps> = (props) => {
172
186
  recomputeNumberOfRows,
173
187
  type,
174
188
  name,
189
+ hasError,
190
+ describedById,
175
191
  ...forwardedProps
176
192
  } = props;
177
193
  // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -214,6 +230,8 @@ const renderInputNative: React.FC<InputNativeProps> = (props) => {
214
230
  onFocus: onTextFieldFocus,
215
231
  onBlur: onTextFieldBlur,
216
232
  onChange: handleChange,
233
+ 'aria-invalid': hasError ? 'true' : undefined,
234
+ 'aria-describedby': describedById,
217
235
  ref: mergeRefs(inputRef as any, ref) as any,
218
236
  };
219
237
  if (multiline) {
@@ -254,6 +272,7 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
254
272
  name,
255
273
  onBlur,
256
274
  onChange,
275
+ onClear,
257
276
  onFocus,
258
277
  placeholder,
259
278
  textFieldRef,
@@ -264,6 +283,17 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
264
283
  ...forwardedProps
265
284
  } = props;
266
285
  const textFieldId = useMemo(() => id || `text-field-${uid()}`, [id]);
286
+ /**
287
+ * Generate unique ids for both the helper and error texts, in order to
288
+ * later on add them to the input native as aria-describedby. If both the error and the helper are present,
289
+ * we want to first use the most important one, which is the errorId. That way, screen readers will read first
290
+ * the error and then the helper
291
+ */
292
+ const helperId = helper ? `text-field-helper-${uid()}` : undefined;
293
+ const errorId = error ? `text-field-error-${uid()}` : undefined;
294
+ const describedByIds = [errorId, helperId].filter(Boolean);
295
+ const describedById = describedByIds.length === 0 ? undefined : describedByIds.join(' ');
296
+
267
297
  const [isFocus, setFocus] = useState(false);
268
298
  const { rows, recomputeNumberOfRows } = useComputeNumberOfRows(multiline ? minimumRows || DEFAULT_MIN_ROWS : 0);
269
299
  const valueLength = (value || '').length;
@@ -275,12 +305,22 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
275
305
  * and remove focus from the clear button.
276
306
  * @param evt On clear event.
277
307
  */
278
- const onClear = (evt: React.ChangeEvent) => {
308
+ const handleClear = (evt: React.ChangeEvent) => {
279
309
  evt.nativeEvent.preventDefault();
280
310
  evt.nativeEvent.stopPropagation();
281
311
  (evt.currentTarget as HTMLElement).blur();
282
312
 
283
313
  onChange('');
314
+
315
+ if (onClear) {
316
+ onClear(evt);
317
+ }
318
+
319
+ const inputElement = inputRef as RefObject<HTMLInputElement | HTMLTextAreaElement>;
320
+
321
+ if (inputElement && inputElement.current) {
322
+ inputElement.current.focus();
323
+ }
284
324
  };
285
325
 
286
326
  return (
@@ -359,6 +399,8 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
359
399
  type,
360
400
  value,
361
401
  name,
402
+ hasError,
403
+ describedById,
362
404
  ...forwardedProps,
363
405
  })}
364
406
  </div>
@@ -383,6 +425,8 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
383
425
  type,
384
426
  value,
385
427
  name,
428
+ hasError,
429
+ describedById,
386
430
  ...forwardedProps,
387
431
  })}
388
432
  </div>
@@ -405,7 +449,7 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
405
449
  emphasis={Emphasis.low}
406
450
  size={Size.s}
407
451
  theme={theme}
408
- onClick={onClear}
452
+ onClick={handleClear}
409
453
  type="button"
410
454
  />
411
455
  )}
@@ -414,13 +458,13 @@ export const TextField: Comp<TextFieldProps, HTMLDivElement> = forwardRef((props
414
458
  </div>
415
459
 
416
460
  {hasError && error && (
417
- <InputHelper className={`${CLASSNAME}__helper`} kind={Kind.error} theme={theme}>
461
+ <InputHelper className={`${CLASSNAME}__helper`} kind={Kind.error} theme={theme} id={errorId}>
418
462
  {error}
419
463
  </InputHelper>
420
464
  )}
421
465
 
422
466
  {helper && (
423
- <InputHelper className={`${CLASSNAME}__helper`} theme={theme}>
467
+ <InputHelper className={`${CLASSNAME}__helper`} theme={theme} id={helperId}>
424
468
  {helper}
425
469
  </InputHelper>
426
470
  )}
@@ -1,105 +1,164 @@
1
- import React, { ReactElement } from 'react';
1
+ import React from 'react';
2
+
3
+ import { Button } from '@lumx/react';
4
+ import { act, render, waitFor } from '@testing-library/react';
5
+ import {
6
+ findByClassName,
7
+ getByClassName,
8
+ queryAllByTagName,
9
+ queryByClassName,
10
+ } from '@lumx/react/testing/utils/queries';
11
+ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
12
+ import userEvent from '@testing-library/user-event';
2
13
 
3
- import { mount, shallow } from 'enzyme';
4
- import 'jest-enzyme';
5
-
6
- import { Wrapper, commonTestsSuite } from '@lumx/react/testing/utils';
7
-
8
- import { Button, Icon } from '@lumx/react';
9
14
  import { Tooltip, TooltipProps } from './Tooltip';
10
15
 
11
16
  const CLASSNAME = Tooltip.className as string;
12
17
 
13
18
  jest.mock('uid', () => ({ uid: () => 'uid' }));
14
19
 
15
- type SetupProps = Partial<TooltipProps>;
16
-
17
20
  /**
18
21
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
19
22
  */
20
- const setup = (propsOverride: SetupProps = {}, shallowRendering = true) => {
21
- const props: any = { ...propsOverride };
22
- const renderer: (el: ReactElement) => Wrapper = shallowRendering ? shallow : mount;
23
- const wrapper = renderer(
24
- <Tooltip label="Tooltip" {...props}>
25
- Anchor
26
- </Tooltip>,
23
+ const setup = async (propsOverride: Partial<TooltipProps> = {}) => {
24
+ const props: any = { forceOpen: true, ...propsOverride };
25
+ // Hack to remove act() warning in console
26
+ await (act as any)(() =>
27
+ Promise.resolve(
28
+ render(
29
+ <Tooltip label="Tooltip" {...props}>
30
+ {props.children || 'Anchor'}
31
+ </Tooltip>,
32
+ ),
33
+ ),
27
34
  );
28
- const tooltip = wrapper.find(`.${CLASSNAME}`);
29
-
30
- return {
31
- props,
32
- tooltip,
33
- wrapper,
34
- };
35
+ const tooltip = queryByClassName(document.body, CLASSNAME);
36
+ const anchorWrapper = queryByClassName(document.body, 'lumx-tooltip-anchor-wrapper');
37
+ return { props, tooltip, anchorWrapper };
35
38
  };
36
39
 
37
- jest.mock('./useTooltipOpen', () => ({ useTooltipOpen: () => true }));
38
-
39
40
  describe(`<${Tooltip.displayName}>`, () => {
40
- // 1. Test render via snapshot (default states of component).
41
- describe('Snapshots and structure', () => {
42
- // Here is an example of a basic rendering check, with snapshot.
43
-
44
- it('should render correctly', () => {
45
- const { tooltip, wrapper } = setup();
46
- expect(wrapper).toMatchSnapshot();
47
-
48
- expect(tooltip).toExist();
49
- expect(tooltip).toHaveClassName(CLASSNAME);
41
+ describe('render', () => {
42
+ it('should not render with empty label', async () => {
43
+ const { tooltip, anchorWrapper } = await setup({
44
+ label: undefined,
45
+ forceOpen: true,
46
+ });
47
+ expect(tooltip).not.toBeInTheDocument();
48
+ expect(anchorWrapper).not.toBeInTheDocument();
50
49
  });
51
50
 
52
- it('should return children when empty label', () => {
53
- const wrapper = shallow(
54
- <Tooltip label="">
55
- <Icon icon="icon" />
56
- </Tooltip>,
57
- );
58
- expect(wrapper).toMatchSnapshot();
51
+ it('should wrap unknown children', async () => {
52
+ const { tooltip, anchorWrapper } = await setup({
53
+ label: 'Tooltip label',
54
+ children: 'Anchor',
55
+ forceOpen: true,
56
+ });
57
+ expect(tooltip).toBeInTheDocument();
58
+ expect(anchorWrapper).toBeInTheDocument();
59
+ expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
59
60
  });
60
61
 
61
- it('should wrap children', () => {
62
- // Wrap string children
63
- const wrapper1 = shallow(<Tooltip label="tooltip1">children</Tooltip>);
64
- expect(wrapper1).toMatchSnapshot();
62
+ it('should not wrap Button', async () => {
63
+ const { tooltip, anchorWrapper } = await setup({
64
+ label: 'Tooltip label',
65
+ children: <Button>Anchor</Button>,
66
+ forceOpen: true,
67
+ });
68
+ expect(tooltip).toBeInTheDocument();
69
+ expect(anchorWrapper).not.toBeInTheDocument();
70
+ const button = queryByClassName(document.body, Button.className as string);
71
+ expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
72
+ });
65
73
 
66
- // Wrap fragment children
67
- const wrapper2 = shallow(
68
- <Tooltip label="tooltip1">
69
- <>children</>
70
- </Tooltip>,
71
- );
72
- expect(wrapper2).toMatchSnapshot();
74
+ it('should wrap disabled Button', async () => {
75
+ const { tooltip, anchorWrapper } = await setup({
76
+ label: 'Tooltip label',
77
+ children: <Button isDisabled>Anchor</Button>,
78
+ forceOpen: true,
79
+ });
80
+ expect(tooltip).toBeInTheDocument();
81
+ expect(anchorWrapper).toBeInTheDocument();
82
+ expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
83
+ const button = queryByClassName(document.body, Button.className as string);
84
+ expect(button?.parentElement).toBe(anchorWrapper);
73
85
  });
74
86
 
75
- it('should not wrap Icon', () => {
76
- const wrapper = shallow(
77
- <Tooltip label="tooltip1">
78
- <Icon icon="icon" />
79
- </Tooltip>,
80
- );
81
- expect(wrapper).toMatchSnapshot();
87
+ it('should render multiline', async () => {
88
+ const { tooltip } = await setup({
89
+ label: 'First line\nSecond line',
90
+ forceOpen: true,
91
+ });
92
+ expect(tooltip).toBeInTheDocument();
93
+ const lines = queryAllByTagName(tooltip as any, 'p');
94
+ expect(lines.length).toBe(2);
95
+ expect(lines[0]).toHaveTextContent('First line');
96
+ expect(lines[1]).toHaveTextContent('Second line');
82
97
  });
98
+ });
83
99
 
84
- it('should not wrap Button', () => {
85
- const wrapper = shallow(
86
- <Tooltip label="tooltip1">
87
- <Button>button</Button>
88
- </Tooltip>,
89
- );
90
- expect(wrapper).toMatchSnapshot();
100
+ describe('activation', () => {
101
+ it('should activate on anchor hover', async () => {
102
+ let { tooltip } = await setup({
103
+ label: 'Tooltip label',
104
+ children: <Button>Anchor</Button>,
105
+ forceOpen: false,
106
+ });
107
+
108
+ expect(tooltip).not.toBeInTheDocument();
109
+
110
+ // Hover anchor button
111
+ const button = getByClassName(document.body, Button.className as string);
112
+ await userEvent.hover(button);
113
+
114
+ // Tooltip opened
115
+ tooltip = await findByClassName(document.body, CLASSNAME);
116
+ expect(tooltip).toBeInTheDocument();
117
+ expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
118
+
119
+ // Un-hover anchor button
120
+ userEvent.unhover(button);
121
+ await waitFor(() => {
122
+ expect(button).not.toHaveFocus();
123
+ // Tooltip closed
124
+ expect(tooltip).not.toBeInTheDocument();
125
+ });
91
126
  });
92
127
 
93
- it('should work on disabled elements', () => {
94
- const wrapper = shallow(
95
- <Tooltip label="Tooltip on disabled button" forceOpen>
96
- <Button isDisabled>Empty</Button>
97
- </Tooltip>,
98
- );
99
- expect(wrapper).toMatchSnapshot();
128
+ it('should activate on anchor focus', async () => {
129
+ let { tooltip } = await setup({
130
+ label: 'Tooltip label',
131
+ children: <Button>Anchor</Button>,
132
+ forceOpen: false,
133
+ });
134
+
135
+ expect(tooltip).not.toBeInTheDocument();
136
+
137
+ // Focus anchor button
138
+ await userEvent.tab();
139
+ const button = getByClassName(document.body, Button.className as string);
140
+ expect(button).toHaveFocus();
141
+
142
+ // Tooltip opened
143
+ tooltip = await findByClassName(document.body, CLASSNAME);
144
+ expect(tooltip).toBeInTheDocument();
145
+ expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
146
+
147
+ // Focus next element
148
+ userEvent.tab();
149
+ await waitFor(() => {
150
+ expect(button).not.toHaveFocus();
151
+ // Tooltip closed
152
+ expect(tooltip).not.toBeInTheDocument();
153
+ });
100
154
  });
101
155
  });
102
156
 
103
157
  // Common tests suite.
104
- commonTestsSuite(setup, { className: 'tooltip', prop: 'tooltip' }, { className: CLASSNAME });
158
+ commonTestsSuiteRTL(setup, {
159
+ baseClassName: CLASSNAME,
160
+ forwardClassName: 'tooltip',
161
+ forwardAttributes: 'tooltip',
162
+ forwardRef: 'tooltip',
163
+ });
105
164
  });
@@ -1,7 +1,9 @@
1
- import { mergeRefs } from '@lumx/react/utils/mergeRefs';
2
1
  import get from 'lodash/get';
2
+ import isUndefined from 'lodash/isUndefined';
3
3
  import React, { cloneElement, ReactNode, useMemo } from 'react';
4
4
 
5
+ import { mergeRefs } from '@lumx/react/utils/mergeRefs';
6
+
5
7
  /**
6
8
  * Add ref and ARIA attribute(s) in tooltip children or wrapped children.
7
9
  * Button, IconButton, Icon and React HTML elements don't need to be wrapped but any other kind of children (array, fragment, custom components)
@@ -20,7 +22,12 @@ export const useInjectTooltipRef = (
20
22
  id: string,
21
23
  ): ReactNode => {
22
24
  return useMemo(() => {
23
- const ariaProps = { 'aria-describedby': isOpen ? id : undefined };
25
+ // Let the children remove the aria-describedby attribute by setting it to undefined
26
+ const childrenHasAriaProp = get(children, 'props')
27
+ ? 'aria-describedby' in get(children, 'props') && isUndefined(get(children, 'props.aria-describedby'))
28
+ : false;
29
+ const ariaProps = { 'aria-describedby': isOpen && !childrenHasAriaProp ? id : undefined };
30
+
24
31
  if (
25
32
  children &&
26
33
  get(children, '$$typeof') &&