@onewelcome/react-lib-components 0.1.4-alpha → 0.1.7-alpha
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/Button/IconButton.d.ts +2 -1
- package/dist/ContextMenu/ContextMenu.d.ts +2 -3
- package/dist/ContextMenu/ContextMenuItem.d.ts +10 -3
- package/dist/Form/Checkbox/Checkbox.d.ts +2 -2
- package/dist/Form/Toggle/Toggle.d.ts +1 -1
- package/dist/Link/Link.d.ts +4 -3
- package/dist/Notifications/BaseModal/BaseModal.d.ts +4 -2
- package/dist/Notifications/SlideInModal/SlideInModal.d.ts +4 -0
- package/dist/StatusIndicator/StatusIndicator.d.ts +9 -0
- package/dist/index.d.ts +43 -42
- package/dist/react-lib-components.cjs.development.js +2208 -2033
- package/dist/react-lib-components.cjs.development.js.map +1 -1
- package/dist/react-lib-components.cjs.production.min.js +1 -1
- package/dist/react-lib-components.cjs.production.min.js.map +1 -1
- package/dist/react-lib-components.esm.js +2207 -2034
- package/dist/react-lib-components.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/Button/BaseButton.module.scss +3 -18
- package/src/Button/Button.module.scss +4 -311
- package/src/Button/IconButton.module.scss +21 -128
- package/src/Button/IconButton.test.tsx +24 -0
- package/src/Button/IconButton.tsx +6 -1
- package/src/ContextMenu/ContextMenu.test.tsx +121 -6
- package/src/ContextMenu/ContextMenu.tsx +84 -6
- package/src/ContextMenu/ContextMenuItem.tsx +57 -9
- package/src/Form/Checkbox/Checkbox.test.tsx +144 -8
- package/src/Form/Checkbox/Checkbox.tsx +8 -8
- package/src/Form/Toggle/Toggle.test.tsx +45 -19
- package/src/Form/Toggle/Toggle.tsx +3 -3
- package/src/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.test.tsx +1 -1
- package/src/Link/Link.module.scss +20 -0
- package/src/Link/Link.test.tsx +33 -0
- package/src/Link/Link.tsx +8 -2
- package/src/Notifications/BaseModal/BaseModal.test.tsx +75 -11
- package/src/Notifications/BaseModal/BaseModal.tsx +27 -6
- package/src/Notifications/Dialog/Dialog.tsx +1 -1
- package/src/Notifications/SlideInModal/SlideInModal.module.scss +36 -0
- package/src/Notifications/SlideInModal/SlideInModal.test.tsx +69 -0
- package/src/Notifications/SlideInModal/SlideInModal.tsx +31 -0
- package/src/StatusIndicator/StatusIndicator.module.scss +27 -0
- package/src/StatusIndicator/StatusIndicator.test.tsx +127 -0
- package/src/StatusIndicator/StatusIndicator.tsx +25 -0
- package/src/Tiles/Tile.module.scss +1 -1
- package/src/Tiles/Tile.test.tsx +4 -4
- package/src/index.ts +79 -47
- package/src/mixins.module.scss +171 -0
- package/src/readyclasses.module.scss +0 -30
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { ContextMenu, Props } from './ContextMenu';
|
|
3
|
-
import { render
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
4
|
import { Button } from '../Button/Button';
|
|
5
5
|
import { ContextMenuItem } from './ContextMenuItem';
|
|
6
6
|
import userEvent from '@testing-library/user-event';
|
|
@@ -15,8 +15,12 @@ const defaultParams: Props = {
|
|
|
15
15
|
<ContextMenuItem onClick={onClick} data-testid="contextmenuitem" key="1">
|
|
16
16
|
Example item 1
|
|
17
17
|
</ContextMenuItem>,
|
|
18
|
-
<ContextMenuItem key="2">
|
|
19
|
-
|
|
18
|
+
<ContextMenuItem onClick={onClick} data-testid="contextmenuitem2" key="2">
|
|
19
|
+
Example item 2
|
|
20
|
+
</ContextMenuItem>,
|
|
21
|
+
<ContextMenuItem onClick={onClick} data-testid="contextmenuitem3" key="3">
|
|
22
|
+
Example item 3
|
|
23
|
+
</ContextMenuItem>,
|
|
20
24
|
],
|
|
21
25
|
show: false,
|
|
22
26
|
onShow: onShow,
|
|
@@ -61,12 +65,10 @@ describe('ContextMenu should render', () => {
|
|
|
61
65
|
show: true,
|
|
62
66
|
}));
|
|
63
67
|
|
|
64
|
-
const
|
|
65
|
-
const button = getByRole(contextmenuitem, 'button');
|
|
68
|
+
const button = getByTestId('contextmenuitem');
|
|
66
69
|
|
|
67
70
|
userEvent.click(button);
|
|
68
71
|
|
|
69
|
-
expect(contextmenuitem).toBeTruthy();
|
|
70
72
|
expect(onClick).toHaveBeenCalled();
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -117,3 +119,116 @@ describe('ref should work', () => {
|
|
|
117
119
|
render(<ExampleComponent propagateRef={refCheck} />);
|
|
118
120
|
});
|
|
119
121
|
});
|
|
122
|
+
|
|
123
|
+
describe('accessibility controls', () => {
|
|
124
|
+
it('opening works correctly with arrow key down, then navigation should work with arrow keys', () => {
|
|
125
|
+
const { getByTestId, trigger } = createContextMenu();
|
|
126
|
+
const firstContextMenuItem = getByTestId('contextmenuitem');
|
|
127
|
+
const secondContextMenuItem = getByTestId('contextmenuitem2');
|
|
128
|
+
const thirdContextMenuItem = getByTestId('contextmenuitem3');
|
|
129
|
+
|
|
130
|
+
userEvent.keyboard('{arrowdown}');
|
|
131
|
+
userEvent.keyboard('{arrowdown}');
|
|
132
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
133
|
+
expect(firstContextMenuItem).toHaveFocus();
|
|
134
|
+
|
|
135
|
+
userEvent.keyboard('{arrowdown}');
|
|
136
|
+
expect(secondContextMenuItem).toHaveFocus();
|
|
137
|
+
|
|
138
|
+
userEvent.keyboard('{arrowdown}');
|
|
139
|
+
expect(thirdContextMenuItem).toHaveFocus();
|
|
140
|
+
|
|
141
|
+
userEvent.keyboard('{arrowdown}');
|
|
142
|
+
expect(firstContextMenuItem).toHaveFocus();
|
|
143
|
+
|
|
144
|
+
userEvent.keyboard('{arrowup}');
|
|
145
|
+
expect(thirdContextMenuItem).toHaveFocus();
|
|
146
|
+
|
|
147
|
+
userEvent.keyboard('{arrowup}');
|
|
148
|
+
expect(secondContextMenuItem).toHaveFocus();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('opens correctly with enter key, closing works with escape key.', async () => {
|
|
152
|
+
const { trigger } = createContextMenu();
|
|
153
|
+
|
|
154
|
+
userEvent.keyboard('{enter}');
|
|
155
|
+
|
|
156
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
157
|
+
|
|
158
|
+
userEvent.keyboard('{escape}');
|
|
159
|
+
|
|
160
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('opens correctly with space, home and end buttons work', () => {
|
|
164
|
+
const { trigger, getByTestId } = createContextMenu();
|
|
165
|
+
const firstContextMenuItem = getByTestId('contextmenuitem');
|
|
166
|
+
const thirdContextMenuItem = getByTestId('contextmenuitem3');
|
|
167
|
+
|
|
168
|
+
userEvent.keyboard('{space}');
|
|
169
|
+
|
|
170
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
171
|
+
|
|
172
|
+
userEvent.keyboard('{end}');
|
|
173
|
+
|
|
174
|
+
expect(thirdContextMenuItem).toHaveFocus();
|
|
175
|
+
|
|
176
|
+
userEvent.keyboard('{home}');
|
|
177
|
+
|
|
178
|
+
expect(firstContextMenuItem).toHaveFocus();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('opens correctly with space, navigate with arrow keys, select with enter', () => {
|
|
182
|
+
onClick.mockImplementation((e) => {
|
|
183
|
+
expect(e.target.getAttribute('data-testid')).toBe('contextmenuitem3');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const { trigger, getByTestId } = createContextMenu((defaultParams) => ({
|
|
187
|
+
...defaultParams,
|
|
188
|
+
}));
|
|
189
|
+
const thirdContextMenuItem = getByTestId('contextmenuitem3');
|
|
190
|
+
|
|
191
|
+
userEvent.keyboard('{space}');
|
|
192
|
+
|
|
193
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
194
|
+
|
|
195
|
+
userEvent.keyboard('{arrowdown}');
|
|
196
|
+
userEvent.keyboard('{arrowdown}');
|
|
197
|
+
userEvent.keyboard('{arrowdown}');
|
|
198
|
+
|
|
199
|
+
expect(thirdContextMenuItem).toHaveFocus();
|
|
200
|
+
|
|
201
|
+
userEvent.keyboard('{enter}');
|
|
202
|
+
|
|
203
|
+
expect(onClick).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('opens correctly with enter, navigate with arrow keys, select with space', () => {
|
|
207
|
+
onClick.mockImplementation((e) => {
|
|
208
|
+
expect(e.target.getAttribute('data-testid')).toBe('contextmenuitem3');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const { trigger, getByTestId } = createContextMenu((defaultParams) => ({
|
|
212
|
+
...defaultParams,
|
|
213
|
+
}));
|
|
214
|
+
const thirdContextMenuItem = getByTestId('contextmenuitem3');
|
|
215
|
+
|
|
216
|
+
userEvent.keyboard('{enter}');
|
|
217
|
+
|
|
218
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
219
|
+
|
|
220
|
+
userEvent.keyboard('{arrowdown}');
|
|
221
|
+
userEvent.keyboard('{arrowdown}');
|
|
222
|
+
userEvent.keyboard('{arrowdown}');
|
|
223
|
+
|
|
224
|
+
expect(thirdContextMenuItem).toHaveFocus();
|
|
225
|
+
|
|
226
|
+
userEvent.keyboard('{space}');
|
|
227
|
+
|
|
228
|
+
expect(onClick).toHaveBeenCalled();
|
|
229
|
+
|
|
230
|
+
userEvent.keyboard('{space}');
|
|
231
|
+
|
|
232
|
+
expect(thirdContextMenuItem).toHaveFocus();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
ComponentPropsWithRef,
|
|
3
|
+
ReactElement,
|
|
4
|
+
ReactNode,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { Props as ButtonProps } from '../Button/Button';
|
|
3
10
|
import { Props as IconButtonProps } from '../Button/IconButton';
|
|
4
11
|
import { Popover } from '../Popover/Popover';
|
|
5
12
|
import { Placement, Offset } from '../hooks/usePosition';
|
|
6
|
-
import { Props as ContextMenuItemProps } from './ContextMenuItem';
|
|
7
13
|
import classes from './ContextMenu.module.scss';
|
|
8
14
|
import { useBodyClick } from '../hooks/useBodyClick';
|
|
9
15
|
import { createPortal } from 'react-dom';
|
|
10
16
|
|
|
11
17
|
export interface Props extends ComponentPropsWithRef<'div'> {
|
|
12
18
|
trigger: ReactElement<ButtonProps> | ReactElement<IconButtonProps>;
|
|
13
|
-
children:
|
|
19
|
+
children: ReactNode;
|
|
14
20
|
placement?: Placement;
|
|
15
21
|
transformOrigin?: Placement;
|
|
16
22
|
offset?: Offset;
|
|
@@ -40,11 +46,63 @@ export const ContextMenu = React.forwardRef<HTMLDivElement, Props>(
|
|
|
40
46
|
) => {
|
|
41
47
|
const anchorEl = useRef<HTMLButtonElement>(null);
|
|
42
48
|
const [showContextMenu, setShowContextMenu] = useState(show);
|
|
49
|
+
const [selectedContextMenuItem, setSelectedContextMenuItem] = useState(-1);
|
|
50
|
+
const [focusedContextMenuItem, setFocusedContextMenuItem] = useState(-1);
|
|
51
|
+
const [shouldClick, setShouldClick] =
|
|
52
|
+
useState(
|
|
53
|
+
false
|
|
54
|
+
); /** We need this, because whenever we use the arrow keys to select the contextmenu item, and we focus the currently selected item it fires the "click" listener in ContextMenuItem component. Instead, we only want this to fire if we press "enter" or "spacebar" so we set this to true whenever that is the case, and back to false when it has been executed. */
|
|
55
|
+
const [childrenCount] = useState(React.Children.count(children));
|
|
43
56
|
|
|
44
57
|
if (!id) {
|
|
45
58
|
throw new Error('You need to provide an ID to the context menu');
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
const onArrowNavigation = (event: React.KeyboardEvent) => {
|
|
62
|
+
if (focusedContextMenuItem === -1 && selectedContextMenuItem !== -1) {
|
|
63
|
+
setFocusedContextMenuItem(selectedContextMenuItem);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
switch (event.code) {
|
|
68
|
+
case 'ArrowDown':
|
|
69
|
+
if (!showContextMenu) {
|
|
70
|
+
setShowContextMenu(true);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setFocusedContextMenuItem((prevState) => {
|
|
74
|
+
return prevState + 1 > childrenCount - 1 ? 0 : prevState + 1;
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
case 'ArrowUp':
|
|
78
|
+
setFocusedContextMenuItem((prevState) => {
|
|
79
|
+
return prevState - 1 < 0 ? childrenCount - 1 : prevState - 1;
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
case 'Enter':
|
|
83
|
+
case 'Space':
|
|
84
|
+
if (!showContextMenu) {
|
|
85
|
+
setShowContextMenu(true);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setShouldClick(true);
|
|
90
|
+
setSelectedContextMenuItem(focusedContextMenuItem);
|
|
91
|
+
setShowContextMenu(false);
|
|
92
|
+
return;
|
|
93
|
+
case 'Tab':
|
|
94
|
+
case 'Escape':
|
|
95
|
+
setShowContextMenu(false);
|
|
96
|
+
return;
|
|
97
|
+
case 'End':
|
|
98
|
+
setFocusedContextMenuItem(childrenCount - 1);
|
|
99
|
+
return;
|
|
100
|
+
case 'Home':
|
|
101
|
+
setFocusedContextMenuItem(0);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
48
106
|
useBodyClick(
|
|
49
107
|
(event) => {
|
|
50
108
|
return showContextMenu && anchorEl.current !== event.target;
|
|
@@ -60,6 +118,8 @@ export const ContextMenu = React.forwardRef<HTMLDivElement, Props>(
|
|
|
60
118
|
onShow && onShow();
|
|
61
119
|
} else {
|
|
62
120
|
onClose && onClose();
|
|
121
|
+
setFocusedContextMenuItem(-1);
|
|
122
|
+
anchorEl.current && anchorEl.current.focus();
|
|
63
123
|
}
|
|
64
124
|
}, [showContextMenu]);
|
|
65
125
|
|
|
@@ -68,13 +128,31 @@ export const ContextMenu = React.forwardRef<HTMLDivElement, Props>(
|
|
|
68
128
|
id: id,
|
|
69
129
|
'aria-haspopup': 'true',
|
|
70
130
|
'aria-controls': `${id}-menu`,
|
|
71
|
-
'aria-expanded':
|
|
131
|
+
'aria-expanded': showContextMenu,
|
|
72
132
|
onClick: () => setShowContextMenu(!showContextMenu),
|
|
133
|
+
tabIndex: 0,
|
|
73
134
|
ref: anchorEl,
|
|
74
135
|
});
|
|
75
136
|
|
|
137
|
+
const renderChildren = () => {
|
|
138
|
+
return React.Children.map(children, (child, index) => {
|
|
139
|
+
return React.cloneElement(child as ReactElement, {
|
|
140
|
+
onFocusChange: (childIndex: number) => setFocusedContextMenuItem(childIndex),
|
|
141
|
+
onSelectedChange: (childIndex: number) => {
|
|
142
|
+
setSelectedContextMenuItem(childIndex);
|
|
143
|
+
setShouldClick(false);
|
|
144
|
+
},
|
|
145
|
+
childIndex: index,
|
|
146
|
+
hasFocus: focusedContextMenuItem === index,
|
|
147
|
+
isSelected: selectedContextMenuItem === index,
|
|
148
|
+
contextMenuOpened: showContextMenu,
|
|
149
|
+
shouldClick: shouldClick,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
76
154
|
return (
|
|
77
|
-
<div {...rest} ref={ref} className={classes['context-menu']}>
|
|
155
|
+
<div {...rest} ref={ref} onKeyDown={onArrowNavigation} className={classes['context-menu']}>
|
|
78
156
|
{renderTrigger()}
|
|
79
157
|
{createPortal(
|
|
80
158
|
<Popover
|
|
@@ -85,7 +163,7 @@ export const ContextMenu = React.forwardRef<HTMLDivElement, Props>(
|
|
|
85
163
|
show={showContextMenu}
|
|
86
164
|
>
|
|
87
165
|
<ul className={classes.menu} id={`${id}-menu`} aria-describedby={id} role="menu">
|
|
88
|
-
{
|
|
166
|
+
{renderChildren()}
|
|
89
167
|
</ul>
|
|
90
168
|
</Popover>,
|
|
91
169
|
domRoot
|
|
@@ -1,15 +1,63 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { ComponentPropsWithRef, createRef, RefObject, useEffect } from 'react';
|
|
2
2
|
import classes from './ContextMenuItem.module.scss';
|
|
3
3
|
|
|
4
|
-
export interface Props extends Omit<
|
|
4
|
+
export interface Props extends Omit<ComponentPropsWithRef<'button'>, 'onClick'> {
|
|
5
5
|
children?: string;
|
|
6
|
+
hasFocus?: boolean;
|
|
7
|
+
isSelected?: boolean;
|
|
8
|
+
childIndex?: number;
|
|
9
|
+
shouldClick?: boolean;
|
|
10
|
+
contextMenuOpened?: boolean;
|
|
6
11
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
12
|
+
onFocusChange?: (childIndex: number) => void;
|
|
13
|
+
onSelectedChange?: (childIndex: number) => void;
|
|
7
14
|
}
|
|
8
15
|
|
|
9
|
-
export const ContextMenuItem =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
export const ContextMenuItem = React.forwardRef<HTMLButtonElement, Props>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
children,
|
|
20
|
+
onClick,
|
|
21
|
+
onFocusChange,
|
|
22
|
+
onSelectedChange,
|
|
23
|
+
hasFocus,
|
|
24
|
+
isSelected,
|
|
25
|
+
childIndex,
|
|
26
|
+
contextMenuOpened,
|
|
27
|
+
shouldClick,
|
|
28
|
+
...rest
|
|
29
|
+
}: Props,
|
|
30
|
+
ref
|
|
31
|
+
) => {
|
|
32
|
+
let innerButtonRef = (ref as RefObject<HTMLButtonElement>) || createRef<HTMLButtonElement>();
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (isSelected && innerButtonRef.current && shouldClick) {
|
|
36
|
+
innerButtonRef.current.click();
|
|
37
|
+
}
|
|
38
|
+
}, [isSelected, shouldClick]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (innerButtonRef.current && hasFocus && contextMenuOpened) {
|
|
42
|
+
onFocusChange && childIndex && onFocusChange(childIndex);
|
|
43
|
+
innerButtonRef.current.focus();
|
|
44
|
+
}
|
|
45
|
+
}, [hasFocus, innerButtonRef, contextMenuOpened]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<li role="menuitem" className={classes['context-menu-item']}>
|
|
49
|
+
<button
|
|
50
|
+
{...rest}
|
|
51
|
+
ref={innerButtonRef}
|
|
52
|
+
data-focus={hasFocus}
|
|
53
|
+
onClick={(event) => {
|
|
54
|
+
onClick && onClick(event);
|
|
55
|
+
onSelectedChange && childIndex && onSelectedChange(childIndex);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</button>
|
|
60
|
+
</li>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
|
-
import { Checkbox,
|
|
2
|
+
import { Checkbox, Props } from './Checkbox';
|
|
3
3
|
import { render } from '@testing-library/react';
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { Toggle } from '../Toggle/Toggle';
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const onChangeHandler = jest.fn();
|
|
7
8
|
|
|
8
9
|
const defaultParams: Props = {
|
|
9
10
|
name: 'Testing',
|
|
10
11
|
children: 'checkbox content',
|
|
11
12
|
helperText: 'example helper',
|
|
12
|
-
onChange:
|
|
13
|
+
onChange: onChangeHandler,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
const createCheckbox = (params?: (defaultParams: Props) => Props) => {
|
|
@@ -83,9 +84,35 @@ describe('Checkbox should have proper attributes', () => {
|
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
it('should be disabled', () => {
|
|
86
|
-
const
|
|
87
|
+
const onChangeHandler = jest.fn();
|
|
88
|
+
const { checkbox } = createCheckbox((defaultParams) => ({
|
|
89
|
+
...defaultParams,
|
|
90
|
+
onChange: onChangeHandler,
|
|
91
|
+
disabled: true,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
expect(checkbox).toBeDisabled();
|
|
95
|
+
|
|
96
|
+
userEvent.click(checkbox);
|
|
97
|
+
|
|
98
|
+
expect(onChangeHandler).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('nested checkbox should be disabled', () => {
|
|
102
|
+
const { getByTestId } = createCheckbox((defaultParams) => ({
|
|
103
|
+
...defaultParams,
|
|
104
|
+
indeterminate: false,
|
|
105
|
+
label: 'test',
|
|
106
|
+
children: (
|
|
107
|
+
<Checkbox data-testid="nested-checkbox" name="test" disabled={true}>
|
|
108
|
+
test
|
|
109
|
+
</Checkbox>
|
|
110
|
+
),
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
const nestedCheckbox = getByTestId('nested-checkbox');
|
|
87
114
|
|
|
88
|
-
expect(
|
|
115
|
+
expect(nestedCheckbox).toBeDisabled();
|
|
89
116
|
});
|
|
90
117
|
|
|
91
118
|
it('should have helpertext rendered', () => {
|
|
@@ -131,15 +158,124 @@ describe('Checkbox should be interactive', () => {
|
|
|
131
158
|
it('should call onChange when clicked', () => {
|
|
132
159
|
const { checkbox } = createCheckbox();
|
|
133
160
|
|
|
134
|
-
expect(
|
|
161
|
+
expect(onChangeHandler).not.toBeCalled();
|
|
135
162
|
userEvent.click(checkbox);
|
|
136
|
-
expect(
|
|
163
|
+
expect(onChangeHandler).toBeCalledTimes(1);
|
|
137
164
|
});
|
|
138
165
|
|
|
139
166
|
it('should not call onChange when disabled', () => {
|
|
140
167
|
const { checkbox } = createCheckbox((defaultParams) => ({ ...defaultParams, disabled: true }));
|
|
141
168
|
|
|
142
169
|
userEvent.click(checkbox);
|
|
143
|
-
expect(
|
|
170
|
+
expect(onChangeHandler).not.toBeCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('toggle version', () => {
|
|
175
|
+
it('should turn into a toggle', () => {
|
|
176
|
+
const { container } = render(<Toggle name="toggle">Test</Toggle>);
|
|
177
|
+
|
|
178
|
+
expect(container.querySelector('[data-toggle]')).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('missing attributes gets us errors', () => {
|
|
183
|
+
it('throws an error for missing label prop', () => {
|
|
184
|
+
// Prevent throwing an error in the console when this test is executed. We fix this and the end of this test.
|
|
185
|
+
const err = console.error;
|
|
186
|
+
console.error = jest.fn();
|
|
187
|
+
|
|
188
|
+
let actual;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// @ts-ignore: mandatory props (test for non-typescript react projects)
|
|
192
|
+
createCheckbox((defaultParams) => ({
|
|
193
|
+
...defaultParams,
|
|
194
|
+
name: 'testing',
|
|
195
|
+
children: <Checkbox name="test">Test</Checkbox>,
|
|
196
|
+
}));
|
|
197
|
+
} catch (e: any) {
|
|
198
|
+
actual = e.message;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const expected =
|
|
202
|
+
'If you pass Checkboxes as a child component (to create nested checkbox tree) you need to pass a label to the parent checkbox.';
|
|
203
|
+
|
|
204
|
+
expect(actual).toEqual(expected);
|
|
205
|
+
|
|
206
|
+
console.error = err;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('throws an error for indeterminate prop', () => {
|
|
210
|
+
// Prevent throwing an error in the console when this test is executed. We fix this and the end of this test.
|
|
211
|
+
const err = console.error;
|
|
212
|
+
console.error = jest.fn();
|
|
213
|
+
|
|
214
|
+
let actual;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// @ts-ignore: mandatory props (test for non-typescript react projects)
|
|
218
|
+
createCheckbox((defaultParams) => ({
|
|
219
|
+
...defaultParams,
|
|
220
|
+
label: 'testing',
|
|
221
|
+
children: <Checkbox name="test">Test</Checkbox>,
|
|
222
|
+
}));
|
|
223
|
+
} catch (e: any) {
|
|
224
|
+
actual = e.message;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const expected =
|
|
228
|
+
'If you have nested checkboxes you have to manage the indeterminate state by passing a boolean to the `indeterminate` prop.';
|
|
229
|
+
|
|
230
|
+
expect(actual).toEqual(expected);
|
|
231
|
+
|
|
232
|
+
console.error = err;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('throws an error for incorrect children prop', () => {
|
|
236
|
+
// Prevent throwing an error in the console when this test is executed. We fix this and the end of this test.
|
|
237
|
+
const err = console.error;
|
|
238
|
+
console.error = jest.fn();
|
|
239
|
+
|
|
240
|
+
let actual;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// @ts-ignore: mandatory props (test for non-typescript react projects)
|
|
244
|
+
createCheckbox((defaultParams) => ({
|
|
245
|
+
name: undefined,
|
|
246
|
+
}));
|
|
247
|
+
} catch (e: any) {
|
|
248
|
+
actual = e.message;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const expected =
|
|
252
|
+
'Please make sure to pass either a string or more Checkbox components as a child of your Checkbox component.';
|
|
253
|
+
|
|
254
|
+
expect(actual).toEqual(expected);
|
|
255
|
+
|
|
256
|
+
console.error = err;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('throws an error for missing name prop', () => {
|
|
260
|
+
// Prevent throwing an error in the console when this test is executed. We fix this and the end of this test.
|
|
261
|
+
const err = console.error;
|
|
262
|
+
console.error = jest.fn();
|
|
263
|
+
|
|
264
|
+
let actual;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
// @ts-ignore: mandatory props (test for non-typescript react projects)
|
|
268
|
+
createCheckbox((defaultParams) => ({
|
|
269
|
+
children: 'test',
|
|
270
|
+
}));
|
|
271
|
+
} catch (e: any) {
|
|
272
|
+
actual = e.message;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const expected = "Please pass a 'name' prop to your <Checkbox> component.";
|
|
276
|
+
|
|
277
|
+
expect(actual).toEqual(expected);
|
|
278
|
+
|
|
279
|
+
console.error = err;
|
|
144
280
|
});
|
|
145
281
|
});
|
|
@@ -11,7 +11,7 @@ import { FormSelector } from '../form.interfaces';
|
|
|
11
11
|
|
|
12
12
|
const isToggle = (children: ReactNode) => (children as ReactElement)?.props?.['data-toggle'];
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface Props extends ComponentPropsWithRef<'input'>, FormSelector {
|
|
15
15
|
children: ReactNode;
|
|
16
16
|
label?: string;
|
|
17
17
|
indeterminate?: boolean;
|
|
@@ -20,7 +20,7 @@ export interface CheckboxProps extends ComponentPropsWithRef<'input'>, FormSelec
|
|
|
20
20
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const Checkbox = React.forwardRef<HTMLInputElement,
|
|
23
|
+
export const Checkbox = React.forwardRef<HTMLInputElement, Props>(
|
|
24
24
|
(
|
|
25
25
|
{
|
|
26
26
|
children,
|
|
@@ -39,7 +39,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|
|
39
39
|
formSelectorWrapperProps,
|
|
40
40
|
onChange,
|
|
41
41
|
...rest
|
|
42
|
-
}:
|
|
42
|
+
}: Props,
|
|
43
43
|
ref
|
|
44
44
|
) => {
|
|
45
45
|
const { errorId, identifier, describedBy } = useFormSelector({
|
|
@@ -53,7 +53,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|
|
53
53
|
|
|
54
54
|
useEffect(() => {
|
|
55
55
|
if (!name) {
|
|
56
|
-
|
|
56
|
+
throw new Error("Please pass a 'name' prop to your <Checkbox> component.");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (typeof children === 'object' && !isToggle(children) && indeterminate === undefined) {
|
|
@@ -83,17 +83,17 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|
|
83
83
|
|
|
84
84
|
const renderNestedCheckboxes = () => (
|
|
85
85
|
<ul className={classes['checkbox-list']}>
|
|
86
|
-
{React.Children.map(children as
|
|
86
|
+
{React.Children.map(children as ReactElement[], (child) => {
|
|
87
87
|
return (
|
|
88
88
|
<li>
|
|
89
89
|
<Checkbox
|
|
90
|
-
{...
|
|
90
|
+
{...child.props}
|
|
91
91
|
parentHelperId={parentHelperId}
|
|
92
92
|
parentErrorId={parentErrorId}
|
|
93
93
|
error={error}
|
|
94
|
-
disabled={disabled ? disabled :
|
|
94
|
+
disabled={child.props.disabled ? child.props.disabled : disabled}
|
|
95
95
|
>
|
|
96
|
-
{
|
|
96
|
+
{child.props.children}
|
|
97
97
|
</Checkbox>
|
|
98
98
|
</li>
|
|
99
99
|
);
|