@lumx/react 2.2.17 → 2.2.19
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/esm/_internal/SideNavigationItem.js +8 -4
- package/esm/_internal/SideNavigationItem.js.map +1 -1
- package/esm/_internal/Tooltip2.js +10 -12
- package/esm/_internal/Tooltip2.js.map +1 -1
- package/esm/_internal/UserBlock.js +9 -2
- package/esm/_internal/UserBlock.js.map +1 -1
- package/esm/_internal/useFocusTrap.js +22 -13
- package/esm/_internal/useFocusTrap.js.map +1 -1
- package/esm/_internal/user-block.js +1 -0
- package/esm/_internal/user-block.js.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/dialog/Dialog.stories.tsx +4 -1
- package/src/components/dialog/__snapshots__/Dialog.test.tsx.snap +85 -77
- package/src/components/side-navigation/SideNavigation.stories.tsx +26 -0
- package/src/components/side-navigation/SideNavigationItem.test.tsx +19 -2
- package/src/components/side-navigation/SideNavigationItem.tsx +10 -2
- package/src/components/side-navigation/__snapshots__/SideNavigationItem.test.tsx.snap +1 -1
- package/src/components/tooltip/Tooltip.tsx +2 -5
- package/src/components/tooltip/useTooltipOpen.tsx +7 -4
- package/src/components/user-block/UserBlock.stories.tsx +4 -4
- package/src/components/user-block/UserBlock.tsx +9 -3
- package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +51 -8
- package/src/hooks/useBooleanState.tsx +4 -10
- package/src/hooks/useFocusTrap.ts +2 -28
- package/src/stories/generated/Dialog/Demos.stories.tsx +1 -0
- package/src/utils/focus/getFirstAndLastFocusable.test.ts +128 -0
- package/src/utils/focus/getFirstAndLastFocusable.ts +27 -0
- package/types.d.ts +6 -1
|
@@ -1,82 +1,6 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
-
exports[`<Dialog> Snapshots and structure should render story
|
|
4
|
-
<Fragment>
|
|
5
|
-
<Button
|
|
6
|
-
emphasis="high"
|
|
7
|
-
onClick={[Function]}
|
|
8
|
-
size="m"
|
|
9
|
-
theme="light"
|
|
10
|
-
>
|
|
11
|
-
Open dialog
|
|
12
|
-
</Button>
|
|
13
|
-
<Dialog
|
|
14
|
-
isOpen={true}
|
|
15
|
-
onClose={[Function]}
|
|
16
|
-
parentElement={
|
|
17
|
-
Object {
|
|
18
|
-
"current": undefined,
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
size="big"
|
|
22
|
-
>
|
|
23
|
-
<div
|
|
24
|
-
className="lumx-spacing-padding"
|
|
25
|
-
>
|
|
26
|
-
|
|
27
|
-
Nihil hic munitissimus habendi senatus locus, nihil horum? At nos hinc posthac, sitientis piros
|
|
28
|
-
Afros. Magna pars studiorum, prodita quaerimus. Integer legentibus erat a ante historiarum
|
|
29
|
-
dapibus. Praeterea iter est quasdam res quas ex communi. Ullamco laboris nisi ut aliquid ex ea
|
|
30
|
-
commodi consequat. Inmensae subtilitatis, obscuris et malesuada fames. Me non paenitet nullum
|
|
31
|
-
festiviorem excogitasse ad hoc. Cum ceteris in veneratione tui montes, nascetur mus. Etiam
|
|
32
|
-
habebis sem dicantur magna mollis euismod. Quis aute iure reprehenderit in voluptate velit esse.
|
|
33
|
-
Phasellus laoreet lorem vel dolor tempus vehicula. Ambitioni dedisse scripsisse iudicaretur.
|
|
34
|
-
Paullum deliquit, ponderibus modulisque suis ratio utitur. Ab illo tempore, ab est sed
|
|
35
|
-
immemorabili. Nec dubitamus multa iter quae et nos invenerat. Tu quoque, Brute, fili mi, nihil
|
|
36
|
-
timor populi, nihil! Morbi fringilla convallis sapien, id pulvinar odio volutpat. Cras mattis
|
|
37
|
-
iudicium purus sit amet fermentum. Vivamus sagittis lacus vel augue laoreet rutrum faucibus.
|
|
38
|
-
Quisque ut dolor gravida, placerat libero vel, euismod. Unam incolunt Belgae, aliam Aquitani,
|
|
39
|
-
tertiam. Cras mattis iudicium purus sit amet fermentum
|
|
40
|
-
</div>
|
|
41
|
-
<footer>
|
|
42
|
-
<Toolbar
|
|
43
|
-
after={
|
|
44
|
-
<Button
|
|
45
|
-
emphasis="low"
|
|
46
|
-
onClick={[Function]}
|
|
47
|
-
size="m"
|
|
48
|
-
theme="light"
|
|
49
|
-
>
|
|
50
|
-
Close
|
|
51
|
-
</Button>
|
|
52
|
-
}
|
|
53
|
-
/>
|
|
54
|
-
</footer>
|
|
55
|
-
</Dialog>
|
|
56
|
-
<AlertDialog
|
|
57
|
-
confirmProps={
|
|
58
|
-
Object {
|
|
59
|
-
"label": "Confirm",
|
|
60
|
-
"onClick": [Function],
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
isOpen={false}
|
|
64
|
-
kind="info"
|
|
65
|
-
onClose={[Function]}
|
|
66
|
-
parentElement={
|
|
67
|
-
Object {
|
|
68
|
-
"current": undefined,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
size="tiny"
|
|
72
|
-
title="Default (info)"
|
|
73
|
-
>
|
|
74
|
-
Consequat deserunt officia aute laborum tempor anim sint est.
|
|
75
|
-
</AlertDialog>
|
|
76
|
-
</Fragment>
|
|
77
|
-
`;
|
|
78
|
-
|
|
79
|
-
exports[`<Dialog> Snapshots and structure should render story DialogWithFocusableElements 1`] = `
|
|
3
|
+
exports[`<Dialog> Snapshots and structure should render story DialogFocusTrap 1`] = `
|
|
80
4
|
<Fragment>
|
|
81
5
|
<Button
|
|
82
6
|
emphasis="high"
|
|
@@ -226,11 +150,95 @@ exports[`<Dialog> Snapshots and structure should render story DialogWithFocusabl
|
|
|
226
150
|
>
|
|
227
151
|
Focus div
|
|
228
152
|
</div>
|
|
153
|
+
<Button
|
|
154
|
+
emphasis="high"
|
|
155
|
+
isDisabled={false}
|
|
156
|
+
size="m"
|
|
157
|
+
theme="light"
|
|
158
|
+
>
|
|
159
|
+
Button explicitly not disabled (should focus)
|
|
160
|
+
</Button>
|
|
229
161
|
</div>
|
|
230
162
|
</Dialog>
|
|
231
163
|
</Fragment>
|
|
232
164
|
`;
|
|
233
165
|
|
|
166
|
+
exports[`<Dialog> Snapshots and structure should render story DialogWithAlertDialog 1`] = `
|
|
167
|
+
<Fragment>
|
|
168
|
+
<Button
|
|
169
|
+
emphasis="high"
|
|
170
|
+
onClick={[Function]}
|
|
171
|
+
size="m"
|
|
172
|
+
theme="light"
|
|
173
|
+
>
|
|
174
|
+
Open dialog
|
|
175
|
+
</Button>
|
|
176
|
+
<Dialog
|
|
177
|
+
isOpen={true}
|
|
178
|
+
onClose={[Function]}
|
|
179
|
+
parentElement={
|
|
180
|
+
Object {
|
|
181
|
+
"current": undefined,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
size="big"
|
|
185
|
+
>
|
|
186
|
+
<div
|
|
187
|
+
className="lumx-spacing-padding"
|
|
188
|
+
>
|
|
189
|
+
|
|
190
|
+
Nihil hic munitissimus habendi senatus locus, nihil horum? At nos hinc posthac, sitientis piros
|
|
191
|
+
Afros. Magna pars studiorum, prodita quaerimus. Integer legentibus erat a ante historiarum
|
|
192
|
+
dapibus. Praeterea iter est quasdam res quas ex communi. Ullamco laboris nisi ut aliquid ex ea
|
|
193
|
+
commodi consequat. Inmensae subtilitatis, obscuris et malesuada fames. Me non paenitet nullum
|
|
194
|
+
festiviorem excogitasse ad hoc. Cum ceteris in veneratione tui montes, nascetur mus. Etiam
|
|
195
|
+
habebis sem dicantur magna mollis euismod. Quis aute iure reprehenderit in voluptate velit esse.
|
|
196
|
+
Phasellus laoreet lorem vel dolor tempus vehicula. Ambitioni dedisse scripsisse iudicaretur.
|
|
197
|
+
Paullum deliquit, ponderibus modulisque suis ratio utitur. Ab illo tempore, ab est sed
|
|
198
|
+
immemorabili. Nec dubitamus multa iter quae et nos invenerat. Tu quoque, Brute, fili mi, nihil
|
|
199
|
+
timor populi, nihil! Morbi fringilla convallis sapien, id pulvinar odio volutpat. Cras mattis
|
|
200
|
+
iudicium purus sit amet fermentum. Vivamus sagittis lacus vel augue laoreet rutrum faucibus.
|
|
201
|
+
Quisque ut dolor gravida, placerat libero vel, euismod. Unam incolunt Belgae, aliam Aquitani,
|
|
202
|
+
tertiam. Cras mattis iudicium purus sit amet fermentum
|
|
203
|
+
</div>
|
|
204
|
+
<footer>
|
|
205
|
+
<Toolbar
|
|
206
|
+
after={
|
|
207
|
+
<Button
|
|
208
|
+
emphasis="low"
|
|
209
|
+
onClick={[Function]}
|
|
210
|
+
size="m"
|
|
211
|
+
theme="light"
|
|
212
|
+
>
|
|
213
|
+
Close
|
|
214
|
+
</Button>
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
217
|
+
</footer>
|
|
218
|
+
</Dialog>
|
|
219
|
+
<AlertDialog
|
|
220
|
+
confirmProps={
|
|
221
|
+
Object {
|
|
222
|
+
"label": "Confirm",
|
|
223
|
+
"onClick": [Function],
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
isOpen={false}
|
|
227
|
+
kind="info"
|
|
228
|
+
onClose={[Function]}
|
|
229
|
+
parentElement={
|
|
230
|
+
Object {
|
|
231
|
+
"current": undefined,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
size="tiny"
|
|
235
|
+
title="Default (info)"
|
|
236
|
+
>
|
|
237
|
+
Consequat deserunt officia aute laborum tempor anim sint est.
|
|
238
|
+
</AlertDialog>
|
|
239
|
+
</Fragment>
|
|
240
|
+
`;
|
|
241
|
+
|
|
234
242
|
exports[`<Dialog> Snapshots and structure should render story DialogWithHeaderFooterAndDivider 1`] = `
|
|
235
243
|
<Fragment>
|
|
236
244
|
<Button
|
|
@@ -163,3 +163,29 @@ export const With3LevelsAndMultiActions = () => {
|
|
|
163
163
|
</SideNavigation>
|
|
164
164
|
);
|
|
165
165
|
};
|
|
166
|
+
|
|
167
|
+
/** Using closeMode="hide" keeps children in DOM on close */
|
|
168
|
+
export const CloseModeHide = () => {
|
|
169
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
170
|
+
const toggleL1 = () => setIsOpen(!isOpen);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<SideNavigation>
|
|
174
|
+
<SideNavigationItem
|
|
175
|
+
closeMode="hide"
|
|
176
|
+
label="Level 1"
|
|
177
|
+
emphasis={Emphasis.high}
|
|
178
|
+
isOpen={isOpen}
|
|
179
|
+
onClick={toggleL1}
|
|
180
|
+
toggleButtonProps={{ label: 'Toggle' }}
|
|
181
|
+
>
|
|
182
|
+
<SideNavigationItem
|
|
183
|
+
closeMode="hide"
|
|
184
|
+
label="Level 2"
|
|
185
|
+
emphasis={Emphasis.medium}
|
|
186
|
+
toggleButtonProps={{ label: 'Toggle' }}
|
|
187
|
+
/>
|
|
188
|
+
</SideNavigationItem>
|
|
189
|
+
</SideNavigation>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
@@ -2,6 +2,7 @@ import React, { ReactElement } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { mount, shallow } from 'enzyme';
|
|
4
4
|
import 'jest-enzyme';
|
|
5
|
+
import without from 'lodash/without';
|
|
5
6
|
|
|
6
7
|
import { commonTestsSuite, Wrapper } from '@lumx/react/testing/utils';
|
|
7
8
|
import { getBasicClass } from '@lumx/react/utils';
|
|
@@ -51,13 +52,28 @@ describe(`<${SideNavigationItem.displayName}>`, () => {
|
|
|
51
52
|
expect(root).toHaveClassName(CLASSNAME);
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
it('should render correctly with
|
|
55
|
+
it('should render correctly with split actions', () => {
|
|
55
56
|
const { root, wrapper } = setup({ linkProps: { href: 'http://toto.com' }, onClick: () => null });
|
|
56
57
|
expect(wrapper).toMatchSnapshot();
|
|
57
58
|
|
|
58
59
|
expect(root).toExist();
|
|
59
60
|
expect(root).toHaveClassName(CLASSNAME);
|
|
60
61
|
});
|
|
62
|
+
|
|
63
|
+
it('should unmount children by default when closed', () => {
|
|
64
|
+
const { children } = setup({
|
|
65
|
+
children: <SideNavigationItem label="Child 1" toggleButtonProps={{ label: 'Toggle' }} />,
|
|
66
|
+
});
|
|
67
|
+
expect(children).not.toExist();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should keep children in DOM when closed and with closeMode="hide"', () => {
|
|
71
|
+
const { children } = setup({
|
|
72
|
+
closeMode: 'hide',
|
|
73
|
+
children: <SideNavigationItem key="1" label="Child 1" toggleButtonProps={{ label: 'Toggle' }} />,
|
|
74
|
+
});
|
|
75
|
+
expect(children).toExist();
|
|
76
|
+
});
|
|
61
77
|
});
|
|
62
78
|
|
|
63
79
|
// 2. Test defaultProps value and important props custom values.
|
|
@@ -67,7 +83,8 @@ describe(`<${SideNavigationItem.displayName}>`, () => {
|
|
|
67
83
|
it('should use default props', () => {
|
|
68
84
|
const { root } = setup();
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
const propNames = without(Object.keys(DEFAULT_PROPS), 'closeMode');
|
|
87
|
+
for (const prop of propNames) {
|
|
71
88
|
const className = getBasicClass({ prefix: CLASSNAME, type: prop, value: DEFAULT_PROPS[prop] });
|
|
72
89
|
if (className) {
|
|
73
90
|
expect(root).toHaveClassName(className);
|
|
@@ -41,6 +41,11 @@ export interface SideNavigationItemProps extends GenericProps {
|
|
|
41
41
|
/** Props to pass to the toggle button (minus those already set by the SideNavigationItem props). */
|
|
42
42
|
toggleButtonProps: Pick<IconButtonProps, 'label'> &
|
|
43
43
|
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color' | 'size'>;
|
|
44
|
+
/**
|
|
45
|
+
* Choose how the children are hidden when closed
|
|
46
|
+
* ('hide' keeps the children in DOM but hide them, 'unmount' remove the children from the DOM).
|
|
47
|
+
*/
|
|
48
|
+
closeMode?: 'hide' | 'unmount';
|
|
44
49
|
/** On action button click callback. */
|
|
45
50
|
onActionClick?(evt: React.MouseEvent): void;
|
|
46
51
|
/** On click callback. */
|
|
@@ -62,6 +67,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
62
67
|
*/
|
|
63
68
|
const DEFAULT_PROPS: Partial<SideNavigationItemProps> = {
|
|
64
69
|
emphasis: Emphasis.high,
|
|
70
|
+
closeMode: 'unmount',
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
/**
|
|
@@ -85,12 +91,14 @@ export const SideNavigationItem: Comp<SideNavigationItemProps, HTMLLIElement> =
|
|
|
85
91
|
onActionClick,
|
|
86
92
|
onClick,
|
|
87
93
|
toggleButtonProps,
|
|
94
|
+
closeMode = 'unmount',
|
|
88
95
|
...forwardedProps
|
|
89
96
|
} = props;
|
|
90
97
|
|
|
91
98
|
const content = children && Children.toArray(children).filter(isComponent(SideNavigationItem));
|
|
92
99
|
const hasContent = !isEmpty(content);
|
|
93
100
|
const shouldSplitActions = Boolean(onActionClick);
|
|
101
|
+
const showChildren = hasContent && isOpen;
|
|
94
102
|
|
|
95
103
|
return (
|
|
96
104
|
<li
|
|
@@ -100,7 +108,7 @@ export const SideNavigationItem: Comp<SideNavigationItemProps, HTMLLIElement> =
|
|
|
100
108
|
className,
|
|
101
109
|
handleBasicClasses({
|
|
102
110
|
emphasis,
|
|
103
|
-
isOpen,
|
|
111
|
+
isOpen: showChildren,
|
|
104
112
|
isSelected,
|
|
105
113
|
prefix: CLASSNAME,
|
|
106
114
|
}),
|
|
@@ -151,7 +159,7 @@ export const SideNavigationItem: Comp<SideNavigationItemProps, HTMLLIElement> =
|
|
|
151
159
|
)
|
|
152
160
|
)}
|
|
153
161
|
|
|
154
|
-
{
|
|
162
|
+
{(closeMode === 'hide' || showChildren) && <ul className={`${CLASSNAME}__children`}>{content}</ul>}
|
|
155
163
|
</li>
|
|
156
164
|
);
|
|
157
165
|
});
|
|
@@ -13,7 +13,7 @@ exports[`<SideNavigationItem> Snapshots and structure should render correctly 1`
|
|
|
13
13
|
</li>
|
|
14
14
|
`;
|
|
15
15
|
|
|
16
|
-
exports[`<SideNavigationItem> Snapshots and structure should render correctly with
|
|
16
|
+
exports[`<SideNavigationItem> Snapshots and structure should render correctly with split actions 1`] = `
|
|
17
17
|
<li
|
|
18
18
|
className="lumx-side-navigation-item lumx-side-navigation-item--emphasis-high"
|
|
19
19
|
>
|
|
@@ -65,12 +65,9 @@ const ARROW_SIZE = 8;
|
|
|
65
65
|
* @return React element.
|
|
66
66
|
*/
|
|
67
67
|
export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
68
|
-
if (!DOCUMENT) {
|
|
69
|
-
// Can't render in SSR.
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
68
|
const { label, children, className, delay, placement, forceOpen, ...forwardedProps } = props;
|
|
73
|
-
|
|
69
|
+
// Disable in SSR or without a label.
|
|
70
|
+
if (!DOCUMENT || !label) {
|
|
74
71
|
return <>{children}</>;
|
|
75
72
|
}
|
|
76
73
|
|
|
@@ -95,13 +95,16 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
|
|
|
95
95
|
);
|
|
96
96
|
|
|
97
97
|
// Attach events
|
|
98
|
-
for (const [node, eventType,
|
|
99
|
-
node.addEventListener(eventType,
|
|
98
|
+
for (const [node, eventType, eventHandler] of events) {
|
|
99
|
+
node.addEventListener(eventType, eventHandler);
|
|
100
100
|
}
|
|
101
101
|
return () => {
|
|
102
|
+
// Clear pending timers.
|
|
103
|
+
if (timer) clearTimeout(timer);
|
|
104
|
+
|
|
102
105
|
// Detach events.
|
|
103
|
-
for (const [node, eventType,
|
|
104
|
-
node.removeEventListener(eventType,
|
|
106
|
+
for (const [node, eventType, eventHandler] of events) {
|
|
107
|
+
node.removeEventListener(eventType, eventHandler);
|
|
105
108
|
}
|
|
106
109
|
};
|
|
107
110
|
}, [anchorElement, delay]);
|
|
@@ -17,7 +17,7 @@ export const Default = ({ theme }: any) => (
|
|
|
17
17
|
theme={theme}
|
|
18
18
|
name="Emmitt O. Lum"
|
|
19
19
|
fields={['Creative developer', 'Denpasar']}
|
|
20
|
-
avatarProps={{ image: avatarImageKnob()
|
|
20
|
+
avatarProps={{ image: avatarImageKnob() }}
|
|
21
21
|
onMouseEnter={logAction('Mouse entered')}
|
|
22
22
|
onMouseLeave={logAction('Mouse left')}
|
|
23
23
|
/>
|
|
@@ -30,7 +30,7 @@ export const Sizes = ({ theme }: any) =>
|
|
|
30
30
|
theme={theme}
|
|
31
31
|
name="Emmitt O. Lum"
|
|
32
32
|
fields={['Creative developer', 'Denpasar']}
|
|
33
|
-
avatarProps={{ image: avatarImageKnob()
|
|
33
|
+
avatarProps={{ image: avatarImageKnob() }}
|
|
34
34
|
size={size}
|
|
35
35
|
onMouseEnter={logAction('Mouse entered')}
|
|
36
36
|
onMouseLeave={logAction('Mouse left')}
|
|
@@ -41,8 +41,9 @@ export const Clickable = ({ theme }: any) => {
|
|
|
41
41
|
const baseProps = {
|
|
42
42
|
theme,
|
|
43
43
|
name: 'Emmitt O. Lum',
|
|
44
|
+
nameProps: { 'aria-label': 'Emmitt O. Lum - open user profile' },
|
|
44
45
|
fields: ['Creative developer', 'Denpasar'],
|
|
45
|
-
avatarProps: { image: avatarImageKnob()
|
|
46
|
+
avatarProps: { image: avatarImageKnob() },
|
|
46
47
|
} as any;
|
|
47
48
|
return (
|
|
48
49
|
<>
|
|
@@ -65,7 +66,6 @@ export const WithBadge = ({ theme }: any) => (
|
|
|
65
66
|
fields={['Creative developer', 'Denpasar']}
|
|
66
67
|
avatarProps={{
|
|
67
68
|
image: avatarImageKnob(),
|
|
68
|
-
alt: 'Avatar',
|
|
69
69
|
badge: (
|
|
70
70
|
<Badge color={ColorPalette.blue}>
|
|
71
71
|
<Icon icon={mdiStar} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { forwardRef, ReactNode } from 'react';
|
|
2
2
|
import isEmpty from 'lodash/isEmpty';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
+
import set from 'lodash/set';
|
|
4
5
|
|
|
5
6
|
import { Avatar, ColorPalette, Link, Orientation, Size, Theme } from '@lumx/react';
|
|
6
7
|
import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
|
|
@@ -17,7 +18,7 @@ export type UserBlockSize = Extract<Size, 's' | 'm' | 'l'>;
|
|
|
17
18
|
*/
|
|
18
19
|
export interface UserBlockProps extends GenericProps {
|
|
19
20
|
/** Props to pass to the avatar. */
|
|
20
|
-
avatarProps?: AvatarProps
|
|
21
|
+
avatarProps?: Omit<AvatarProps, 'alt'>;
|
|
21
22
|
/** Additional fields used to describe the user. */
|
|
22
23
|
fields?: string[];
|
|
23
24
|
/** Props to pass to the link wrapping the avatar thumbnail. */
|
|
@@ -121,8 +122,12 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
|
|
|
121
122
|
color: ColorPalette.dark,
|
|
122
123
|
});
|
|
123
124
|
}
|
|
125
|
+
// Disable avatar focus since the name block is the same link / same button.
|
|
126
|
+
if (avatarProps) {
|
|
127
|
+
set(avatarProps, ['thumbnailProps', 'tabIndex'], -1);
|
|
128
|
+
}
|
|
124
129
|
return <NameComponent {...nProps}>{name}</NameComponent>;
|
|
125
|
-
}, [isClickable, linkAs, linkProps, name, nameProps, onClick]);
|
|
130
|
+
}, [avatarProps, isClickable, linkAs, linkProps, name, nameProps, onClick]);
|
|
126
131
|
|
|
127
132
|
const fieldsBlock: ReactNode = fields && componentSize !== Size.s && (
|
|
128
133
|
<div className={`${CLASSNAME}__fields`}>
|
|
@@ -149,7 +154,8 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
|
|
|
149
154
|
<Avatar
|
|
150
155
|
linkAs={linkAs}
|
|
151
156
|
linkProps={linkProps}
|
|
152
|
-
|
|
157
|
+
alt=""
|
|
158
|
+
{...(avatarProps as any)}
|
|
153
159
|
className={classNames(`${CLASSNAME}__avatar`, avatarProps.className)}
|
|
154
160
|
size={componentSize}
|
|
155
161
|
onClick={onClick}
|
|
@@ -6,17 +6,23 @@ Array [
|
|
|
6
6
|
className="lumx-user-block lumx-user-block--orientation-horizontal lumx-user-block--size-m lumx-user-block--theme-light lumx-user-block--is-clickable"
|
|
7
7
|
>
|
|
8
8
|
<Avatar
|
|
9
|
-
alt="
|
|
9
|
+
alt=""
|
|
10
10
|
className="lumx-user-block__avatar"
|
|
11
11
|
image="/demo-assets/avatar1.jpg"
|
|
12
12
|
onClick={[Function]}
|
|
13
13
|
size="m"
|
|
14
14
|
theme="light"
|
|
15
|
+
thumbnailProps={
|
|
16
|
+
Object {
|
|
17
|
+
"tabIndex": -1,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
15
20
|
/>
|
|
16
21
|
<div
|
|
17
22
|
className="lumx-user-block__wrapper"
|
|
18
23
|
>
|
|
19
24
|
<Link
|
|
25
|
+
aria-label="Emmitt O. Lum - open user profile"
|
|
20
26
|
className="lumx-user-block__name"
|
|
21
27
|
color="dark"
|
|
22
28
|
onClick={[Function]}
|
|
@@ -45,7 +51,7 @@ Array [
|
|
|
45
51
|
className="lumx-user-block lumx-user-block--orientation-horizontal lumx-user-block--size-m lumx-user-block--theme-light lumx-user-block--is-clickable"
|
|
46
52
|
>
|
|
47
53
|
<Avatar
|
|
48
|
-
alt="
|
|
54
|
+
alt=""
|
|
49
55
|
className="lumx-user-block__avatar"
|
|
50
56
|
image="/demo-assets/avatar1.jpg"
|
|
51
57
|
linkProps={
|
|
@@ -55,11 +61,17 @@ Array [
|
|
|
55
61
|
}
|
|
56
62
|
size="m"
|
|
57
63
|
theme="light"
|
|
64
|
+
thumbnailProps={
|
|
65
|
+
Object {
|
|
66
|
+
"tabIndex": -1,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
58
69
|
/>
|
|
59
70
|
<div
|
|
60
71
|
className="lumx-user-block__wrapper"
|
|
61
72
|
>
|
|
62
73
|
<Link
|
|
74
|
+
aria-label="Emmitt O. Lum - open user profile"
|
|
63
75
|
className="lumx-user-block__name"
|
|
64
76
|
color="dark"
|
|
65
77
|
href="https://example.com"
|
|
@@ -88,17 +100,23 @@ Array [
|
|
|
88
100
|
className="lumx-user-block lumx-user-block--orientation-horizontal lumx-user-block--size-m lumx-user-block--theme-light lumx-user-block--is-clickable"
|
|
89
101
|
>
|
|
90
102
|
<Avatar
|
|
91
|
-
alt="
|
|
103
|
+
alt=""
|
|
92
104
|
className="lumx-user-block__avatar"
|
|
93
105
|
image="/demo-assets/avatar1.jpg"
|
|
94
106
|
linkAs={[Function]}
|
|
95
107
|
size="m"
|
|
96
108
|
theme="light"
|
|
109
|
+
thumbnailProps={
|
|
110
|
+
Object {
|
|
111
|
+
"tabIndex": -1,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
97
114
|
/>
|
|
98
115
|
<div
|
|
99
116
|
className="lumx-user-block__wrapper"
|
|
100
117
|
>
|
|
101
118
|
<Link
|
|
119
|
+
aria-label="Emmitt O. Lum - open user profile"
|
|
102
120
|
className="lumx-user-block__name"
|
|
103
121
|
color="dark"
|
|
104
122
|
linkAs={[Function]}
|
|
@@ -133,11 +151,16 @@ exports[`<UserBlock> Snapshots and structure should render story 'Default' 1`] =
|
|
|
133
151
|
onMouseLeave={[Function]}
|
|
134
152
|
>
|
|
135
153
|
<Avatar
|
|
136
|
-
alt="
|
|
154
|
+
alt=""
|
|
137
155
|
className="lumx-user-block__avatar"
|
|
138
156
|
image="/demo-assets/avatar1.jpg"
|
|
139
157
|
size="m"
|
|
140
158
|
theme="light"
|
|
159
|
+
thumbnailProps={
|
|
160
|
+
Object {
|
|
161
|
+
"tabIndex": -1,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
141
164
|
/>
|
|
142
165
|
<div
|
|
143
166
|
className="lumx-user-block__wrapper"
|
|
@@ -175,11 +198,16 @@ Array [
|
|
|
175
198
|
onMouseLeave={[Function]}
|
|
176
199
|
>
|
|
177
200
|
<Avatar
|
|
178
|
-
alt="
|
|
201
|
+
alt=""
|
|
179
202
|
className="lumx-user-block__avatar"
|
|
180
203
|
image="/demo-assets/avatar1.jpg"
|
|
181
204
|
size="s"
|
|
182
205
|
theme="light"
|
|
206
|
+
thumbnailProps={
|
|
207
|
+
Object {
|
|
208
|
+
"tabIndex": -1,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
183
211
|
/>
|
|
184
212
|
<div
|
|
185
213
|
className="lumx-user-block__wrapper"
|
|
@@ -197,11 +225,16 @@ Array [
|
|
|
197
225
|
onMouseLeave={[Function]}
|
|
198
226
|
>
|
|
199
227
|
<Avatar
|
|
200
|
-
alt="
|
|
228
|
+
alt=""
|
|
201
229
|
className="lumx-user-block__avatar"
|
|
202
230
|
image="/demo-assets/avatar1.jpg"
|
|
203
231
|
size="m"
|
|
204
232
|
theme="light"
|
|
233
|
+
thumbnailProps={
|
|
234
|
+
Object {
|
|
235
|
+
"tabIndex": -1,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
205
238
|
/>
|
|
206
239
|
<div
|
|
207
240
|
className="lumx-user-block__wrapper"
|
|
@@ -235,11 +268,16 @@ Array [
|
|
|
235
268
|
onMouseLeave={[Function]}
|
|
236
269
|
>
|
|
237
270
|
<Avatar
|
|
238
|
-
alt="
|
|
271
|
+
alt=""
|
|
239
272
|
className="lumx-user-block__avatar"
|
|
240
273
|
image="/demo-assets/avatar1.jpg"
|
|
241
274
|
size="l"
|
|
242
275
|
theme="light"
|
|
276
|
+
thumbnailProps={
|
|
277
|
+
Object {
|
|
278
|
+
"tabIndex": -1,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
243
281
|
/>
|
|
244
282
|
<div
|
|
245
283
|
className="lumx-user-block__wrapper"
|
|
@@ -275,7 +313,7 @@ exports[`<UserBlock> Snapshots and structure should render story 'WithBadge' 1`]
|
|
|
275
313
|
className="lumx-user-block lumx-user-block--orientation-horizontal lumx-user-block--size-m lumx-user-block--theme-light"
|
|
276
314
|
>
|
|
277
315
|
<Avatar
|
|
278
|
-
alt="
|
|
316
|
+
alt=""
|
|
279
317
|
badge={
|
|
280
318
|
<Badge
|
|
281
319
|
color="blue"
|
|
@@ -289,6 +327,11 @@ exports[`<UserBlock> Snapshots and structure should render story 'WithBadge' 1`]
|
|
|
289
327
|
image="/demo-assets/avatar1.jpg"
|
|
290
328
|
size="m"
|
|
291
329
|
theme="light"
|
|
330
|
+
thumbnailProps={
|
|
331
|
+
Object {
|
|
332
|
+
"tabIndex": -1,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
292
335
|
/>
|
|
293
336
|
<div
|
|
294
337
|
className="lumx-user-block__wrapper"
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
export const useBooleanState = (defaultValue: boolean): [boolean, () => void, () => void, () => void] => {
|
|
4
4
|
const [booleanValue, setBoolean] = useState<boolean>(defaultValue);
|
|
5
5
|
|
|
6
|
-
const setToFalse = () =>
|
|
7
|
-
setBoolean(false);
|
|
8
|
-
};
|
|
6
|
+
const setToFalse = useCallback(() => setBoolean(false), []);
|
|
9
7
|
|
|
10
|
-
const setToTrue = () =>
|
|
11
|
-
setBoolean(true);
|
|
12
|
-
};
|
|
8
|
+
const setToTrue = useCallback(() => setBoolean(true), []);
|
|
13
9
|
|
|
14
|
-
const toggleBoolean = () =>
|
|
15
|
-
setBoolean(!booleanValue);
|
|
16
|
-
};
|
|
10
|
+
const toggleBoolean = useCallback(() => setBoolean((previousValue) => !previousValue), []);
|
|
17
11
|
|
|
18
12
|
return [booleanValue, setToFalse, setToTrue, toggleBoolean];
|
|
19
13
|
};
|
|
@@ -1,33 +1,7 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
import { DOCUMENT } from '@lumx/react/constants';
|
|
4
|
-
|
|
5
|
-
/** CSS selector listing all tabbable elements. */
|
|
6
|
-
const TABBABLE_ELEMENTS_SELECTOR = `a[href]:not([tabindex="-1"], [disabled], [aria-disabled]),
|
|
7
|
-
button:not([tabindex="-1"], [disabled], [aria-disabled]),
|
|
8
|
-
textarea:not([tabindex="-1"], [disabled], [aria-disabled]),
|
|
9
|
-
input[type="text"]:not([tabindex="-1"], [disabled], [aria-disabled]),
|
|
10
|
-
input[type="radio"]:not([tabindex="-1"], [disabled], [aria-disabled]),
|
|
11
|
-
input[type="checkbox"]:not([tabindex="-1"], [disabled], [aria-disabled]),
|
|
12
|
-
[tabindex]:not([tabindex="-1"], [disabled], [aria-disabled])`;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Get first and last elements focusable in an element.
|
|
16
|
-
*
|
|
17
|
-
* @param parentElement The element in which to search focusable elements.
|
|
18
|
-
* @return first and last focusable elements
|
|
19
|
-
*/
|
|
20
|
-
function getFocusable(parentElement: HTMLElement) {
|
|
21
|
-
const focusableElements = parentElement.querySelectorAll<HTMLElement>(TABBABLE_ELEMENTS_SELECTOR);
|
|
22
|
-
|
|
23
|
-
if (focusableElements.length <= 0) {
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const first = focusableElements[0];
|
|
28
|
-
const last = focusableElements[focusableElements.length - 1];
|
|
29
|
-
return { first, last };
|
|
30
|
-
}
|
|
4
|
+
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
31
5
|
|
|
32
6
|
/**
|
|
33
7
|
* Add a key down event handler to the given root element (document.body by default) to trap the move of focus
|
|
@@ -55,7 +29,7 @@ export function useFocusTrap(
|
|
|
55
29
|
if (key !== 'Tab') {
|
|
56
30
|
return;
|
|
57
31
|
}
|
|
58
|
-
const focusable =
|
|
32
|
+
const focusable = getFirstAndLastFocusable(focusZoneElement);
|
|
59
33
|
|
|
60
34
|
// Prevent focus switch if no focusable available.
|
|
61
35
|
if (!focusable.first) {
|
|
@@ -6,4 +6,5 @@ export default { title: 'LumX components/dialog/Dialog Demos' };
|
|
|
6
6
|
export { App as Alert } from './alert';
|
|
7
7
|
export { App as Confirm } from './confirm';
|
|
8
8
|
export { App as Default } from './default';
|
|
9
|
+
export { App as Isloading } from './isloading';
|
|
9
10
|
export { App as Sizes } from './sizes';
|