@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.
- package/_internal/ClickAwayProvider.js.map +1 -1
- package/_internal/types.d.ts +0 -2
- package/index.d.ts +2 -0
- package/index.js +347 -75
- package/index.js.map +1 -1
- package/package.json +23 -19
- package/src/components/autocomplete/Autocomplete.test.tsx +55 -142
- package/src/components/autocomplete/AutocompleteMultiple.test.tsx +37 -75
- package/src/components/autocomplete/__mockData__/index.ts +6 -1
- package/src/components/badge/Badge.test.tsx +20 -64
- package/src/components/button/Button.test.tsx +44 -121
- package/src/components/button/ButtonGroup.test.tsx +16 -39
- package/src/components/button/IconButton.stories.tsx +7 -0
- package/src/components/button/IconButton.test.tsx +37 -78
- package/src/components/button/IconButton.tsx +8 -1
- package/src/components/checkbox/Checkbox.test.tsx +62 -67
- package/src/components/chip/Chip.test.tsx +89 -139
- package/src/components/chip/ChipGroup.test.tsx +27 -10
- package/src/components/date-picker/DatePicker.test.tsx +15 -23
- package/src/components/date-picker/DatePickerControlled.test.tsx +24 -20
- package/src/components/date-picker/DatePickerField.test.tsx +43 -27
- package/src/components/dialog/Dialog.test.tsx +36 -35
- package/src/components/divider/Divider.test.tsx +23 -69
- package/src/components/dropdown/Dropdown.test.tsx +30 -61
- package/src/components/expansion-panel/ExpansionPanel.test.tsx +12 -8
- package/src/components/flag/Flag.test.tsx +28 -53
- package/src/components/generic-block/GenericBlock.test.tsx +93 -89
- package/src/components/grid-column/GridColumn.stories.tsx +3 -3
- package/src/components/icon/Icon.test.tsx +80 -64
- package/src/components/index.ts +0 -2
- package/src/components/inline-list/InlineList.test.tsx +30 -17
- package/src/components/input-helper/InputHelper.test.tsx +21 -81
- package/src/components/input-label/InputLabel.test.tsx +19 -61
- package/src/components/lightbox/Lightbox.test.tsx +3 -2
- package/src/components/link/Link.test.tsx +47 -31
- package/src/components/link-preview/LinkPreview.test.tsx +51 -51
- package/src/components/message/Message.test.tsx +31 -52
- package/src/components/mosaic/Mosaic.test.tsx +56 -72
- package/src/components/notification/Notification.test.tsx +51 -82
- package/src/components/popover/Popover.tsx +7 -9
- package/src/components/progress-tracker/ProgressTracker.test.tsx +20 -33
- package/src/components/progress-tracker/ProgressTrackerProvider.test.tsx +61 -36
- package/src/components/progress-tracker/ProgressTrackerStep.test.tsx +19 -109
- package/src/components/progress-tracker/ProgressTrackerStepPanel.test.tsx +21 -58
- package/src/components/progress-tracker/ProgressTrackerStepPanel.tsx +1 -1
- package/src/components/radio-button/RadioButton.test.tsx +78 -92
- package/src/components/radio-button/RadioGroup.test.tsx +13 -59
- package/src/components/select/Select.test.tsx +115 -284
- package/src/components/select/SelectMultiple.stories.tsx +105 -2
- package/src/components/select/SelectMultiple.test.tsx +126 -322
- package/src/components/select/WithSelectContext.tsx +10 -4
- package/src/components/side-navigation/SideNavigation.test.tsx +22 -35
- package/src/components/side-navigation/SideNavigationItem.test.tsx +72 -139
- package/src/components/switch/Switch.test.tsx +70 -149
- package/src/components/table/Table.test.tsx +2 -0
- package/src/components/table/TableBody.test.tsx +18 -8
- package/src/components/table/TableCell.test.tsx +34 -9
- package/src/components/table/TableHeader.test.tsx +18 -8
- package/src/components/table/TableRow.test.tsx +28 -8
- package/src/components/tabs/Tab.test.tsx +27 -96
- package/src/components/tabs/TabList.test.tsx +21 -56
- package/src/components/tabs/TabPanel.test.tsx +20 -55
- package/src/components/tabs/TabPanel.tsx +1 -1
- package/src/components/tabs/TabProvider.test.tsx +158 -37
- package/src/components/tabs/test-utils.ts +39 -0
- package/src/components/text-field/TextField.stories.tsx +14 -5
- package/src/components/text-field/TextField.test.tsx +54 -8
- package/src/components/text-field/TextField.tsx +49 -5
- package/src/components/tooltip/Tooltip.test.tsx +134 -75
- package/src/components/tooltip/useInjectTooltipRef.tsx +9 -2
- package/src/components/uploader/Uploader.test.tsx +60 -48
- package/src/components/user-block/UserBlock.test.tsx +69 -13
- package/src/hooks/useFocusTrap.ts +2 -2
- package/src/testing/utils/commonTestsSuiteRTL.ts +18 -8
- package/src/testing/utils/index.ts +0 -1
- package/src/utils/flattenChildren.ts +5 -0
- package/src/components/autocomplete/__snapshots__/Autocomplete.test.tsx.snap +0 -213
- package/src/components/autocomplete/__snapshots__/AutocompleteMultiple.test.tsx.snap +0 -88
- package/src/components/badge/__snapshots__/Badge.test.tsx.snap +0 -11
- package/src/components/button/ButtonRoot.test.tsx +0 -203
- package/src/components/button/__snapshots__/Button.test.tsx.snap +0 -96
- package/src/components/button/__snapshots__/ButtonGroup.test.tsx.snap +0 -22
- package/src/components/button/__snapshots__/ButtonRoot.test.tsx.snap +0 -160
- package/src/components/button/__snapshots__/IconButton.test.tsx.snap +0 -83
- package/src/components/checkbox/__snapshots__/Checkbox.test.tsx.snap +0 -141
- package/src/components/chip/__snapshots__/Chip.test.tsx.snap +0 -12
- package/src/components/chip/__snapshots__/ChipGroup.test.tsx.snap +0 -29
- package/src/components/date-picker/__snapshots__/DatePicker.test.tsx.snap +0 -22
- package/src/components/date-picker/__snapshots__/DatePickerControlled.test.tsx.snap +0 -597
- package/src/components/date-picker/__snapshots__/DatePickerField.test.tsx.snap +0 -43
- package/src/components/divider/__snapshots__/Divider.test.tsx.snap +0 -9
- package/src/components/dropdown/__snapshots__/Dropdown.test.tsx.snap +0 -35
- package/src/components/icon/__snapshots__/Icon.test.tsx.snap +0 -49
- package/src/components/input-helper/__snapshots__/InputHelper.test.tsx.snap +0 -9
- package/src/components/input-label/__snapshots__/InputLabel.test.tsx.snap +0 -10
- package/src/components/link/__snapshots__/Link.test.tsx.snap +0 -29
- package/src/components/message/__snapshots__/Message.test.tsx.snap +0 -15
- package/src/components/notification/__snapshots__/Notification.test.tsx.snap +0 -34
- package/src/components/progress-tracker/__snapshots__/ProgressTracker.test.tsx.snap +0 -41
- package/src/components/progress-tracker/__snapshots__/ProgressTrackerStep.test.tsx.snap +0 -141
- package/src/components/progress-tracker/__snapshots__/ProgressTrackerStepPanel.test.tsx.snap +0 -25
- package/src/components/radio-button/__snapshots__/RadioButton.test.tsx.snap +0 -113
- package/src/components/radio-button/__snapshots__/RadioGroup.test.tsx.snap +0 -26
- package/src/components/select/__snapshots__/Select.test.tsx.snap +0 -43
- package/src/components/select/__snapshots__/SelectMultiple.test.tsx.snap +0 -87
- package/src/components/side-navigation/__snapshots__/SideNavigation.test.tsx.snap +0 -7
- package/src/components/side-navigation/__snapshots__/SideNavigationItem.test.tsx.snap +0 -30
- package/src/components/switch/__snapshots__/Switch.test.tsx.snap +0 -179
- package/src/components/tabs/__snapshots__/Tab.test.tsx.snap +0 -62
- package/src/components/tabs/__snapshots__/TabList.test.tsx.snap +0 -22
- package/src/components/tabs/__snapshots__/TabPanel.test.tsx.snap +0 -25
- package/src/components/tabs/test.mocks.ts +0 -33
- package/src/components/text-field/__snapshots__/TextField.test.tsx.snap +0 -42
- package/src/components/tooltip/__snapshots__/Tooltip.test.tsx.snap +0 -233
- package/src/components/uploader/__snapshots__/Uploader.test.tsx.snap +0 -14
- package/src/testing/utils/commonTestsSuite.ts +0 -71
- 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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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={
|
|
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
|
|
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:
|
|
21
|
-
const props: any = { ...propsOverride };
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(
|
|
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
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
58
|
-
expect(
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
expect(
|
|
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
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
);
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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') &&
|