@lumx/react 3.6.4 → 3.6.5-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +24 -20
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/button/IconButton.tsx +1 -8
- package/src/components/popover-dialog/PopoverDialog.stories.tsx +28 -1
- package/src/components/popover-dialog/PopoverDialog.test.tsx +31 -0
- package/src/components/tooltip/Tooltip.test.tsx +30 -1
- package/src/components/tooltip/Tooltip.tsx +1 -1
- package/src/components/tooltip/useInjectTooltipRef.tsx +18 -22
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@juggle/resize-observer": "^3.2.0",
|
|
10
|
-
"@lumx/core": "^3.6.
|
|
11
|
-
"@lumx/icons": "^3.6.
|
|
10
|
+
"@lumx/core": "^3.6.5-alpha.0",
|
|
11
|
+
"@lumx/icons": "^3.6.5-alpha.0",
|
|
12
12
|
"@popperjs/core": "^2.5.4",
|
|
13
13
|
"body-scroll-lock": "^3.1.5",
|
|
14
14
|
"classnames": "^2.3.2",
|
|
@@ -112,5 +112,5 @@
|
|
|
112
112
|
"build:storybook": "storybook build"
|
|
113
113
|
},
|
|
114
114
|
"sideEffects": false,
|
|
115
|
-
"version": "3.6.
|
|
115
|
+
"version": "3.6.5-alpha.0"
|
|
116
116
|
}
|
|
@@ -61,14 +61,7 @@ export const IconButton: Comp<IconButtonProps, HTMLButtonElement> = forwardRef((
|
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
63
|
<Tooltip label={hideTooltip ? '' : label} {...tooltipProps}>
|
|
64
|
-
<ButtonRoot
|
|
65
|
-
ref={ref}
|
|
66
|
-
{...{ emphasis, size, theme, ...forwardedProps }}
|
|
67
|
-
aria-label={label}
|
|
68
|
-
variant="icon"
|
|
69
|
-
// Remove the aria-describedby added by the tooltip when it is the same text as the aria-label
|
|
70
|
-
aria-describedby={tooltipProps?.label && tooltipProps?.label === label && undefined}
|
|
71
|
-
>
|
|
64
|
+
<ButtonRoot ref={ref} {...{ emphasis, size, theme, ...forwardedProps }} aria-label={label} variant="icon">
|
|
72
65
|
{image ? (
|
|
73
66
|
<img
|
|
74
67
|
// no need to set alt as an aria-label is already set on the button
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
|
|
3
|
+
import { mdiMenuDown } from '@lumx/icons';
|
|
3
4
|
import { PopoverDialog } from '.';
|
|
4
|
-
import { Button } from '../button';
|
|
5
|
+
import { Button, IconButton } from '../button';
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
title: 'LumX components/popover-dialog/PopoverDialog',
|
|
8
9
|
component: PopoverDialog,
|
|
10
|
+
parameters: { chromatic: { disableSnapshot: true } },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Example PopoverDialog using an IconButton as a trigger
|
|
15
|
+
*/
|
|
16
|
+
export const WithIconButtonTrigger = () => {
|
|
17
|
+
const anchorRef = React.useRef(null);
|
|
18
|
+
const [isOpen, close, open] = useBooleanState(false);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<IconButton id="trigger-button-1" label="Open popover" ref={anchorRef} onClick={open} icon={mdiMenuDown} />
|
|
23
|
+
<PopoverDialog
|
|
24
|
+
aria-labelledby="trigger-button-1"
|
|
25
|
+
anchorRef={anchorRef}
|
|
26
|
+
isOpen={isOpen}
|
|
27
|
+
onClose={close}
|
|
28
|
+
placement="bottom"
|
|
29
|
+
>
|
|
30
|
+
<Button className="lumx-spacing-margin-huge" onClick={close}>
|
|
31
|
+
Close
|
|
32
|
+
</Button>
|
|
33
|
+
</PopoverDialog>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
9
36
|
};
|
|
10
37
|
|
|
11
38
|
/**
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import React, { useRef, useState } from 'react';
|
|
2
2
|
import { render, screen, within } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
|
|
4
5
|
|
|
6
|
+
import { WithIconButtonTrigger } from './PopoverDialog.stories';
|
|
5
7
|
import { PopoverDialog } from './PopoverDialog';
|
|
6
8
|
|
|
9
|
+
jest.mock('@lumx/react/utils/isFocusVisible');
|
|
10
|
+
|
|
7
11
|
const DialogWithButton = (forwardedProps: any) => {
|
|
8
12
|
const anchorRef = useRef(null);
|
|
9
13
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
@@ -25,6 +29,8 @@ const DialogWithButton = (forwardedProps: any) => {
|
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
describe(`<${PopoverDialog.displayName}>`, () => {
|
|
32
|
+
(isFocusVisible as jest.Mock).mockReturnValue(false);
|
|
33
|
+
|
|
28
34
|
it('should behave like a dialog', async () => {
|
|
29
35
|
const label = 'Test Label';
|
|
30
36
|
|
|
@@ -62,4 +68,29 @@ describe(`<${PopoverDialog.displayName}>`, () => {
|
|
|
62
68
|
/** Anchor should retrieve the focus */
|
|
63
69
|
expect(triggerElement).toHaveFocus();
|
|
64
70
|
});
|
|
71
|
+
|
|
72
|
+
it('should work on icon button', async () => {
|
|
73
|
+
const label = 'Open popover';
|
|
74
|
+
render(<WithIconButtonTrigger />);
|
|
75
|
+
|
|
76
|
+
/** Open the popover */
|
|
77
|
+
const triggerElement = screen.getByRole('button', { name: label });
|
|
78
|
+
await userEvent.click(triggerElement);
|
|
79
|
+
|
|
80
|
+
const dialog = await screen.findByRole('dialog', { name: label });
|
|
81
|
+
const withinDialog = within(dialog);
|
|
82
|
+
|
|
83
|
+
/** Get buttons within dialog */
|
|
84
|
+
const dialogButtons = withinDialog.getAllByRole('button');
|
|
85
|
+
|
|
86
|
+
// First button should have focus by default on opening
|
|
87
|
+
expect(dialogButtons[0]).toHaveFocus();
|
|
88
|
+
|
|
89
|
+
// Close the popover
|
|
90
|
+
await userEvent.keyboard('{escape}');
|
|
91
|
+
|
|
92
|
+
expect(screen.queryByRole('dialog', { name: label })).not.toBeInTheDocument();
|
|
93
|
+
/** Anchor should retrieve the focus */
|
|
94
|
+
expect(triggerElement).toHaveFocus();
|
|
95
|
+
});
|
|
65
96
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { Button } from '@lumx/react';
|
|
3
|
+
import { Button, IconButton } from '@lumx/react';
|
|
4
4
|
import { screen, render, waitFor } from '@testing-library/react';
|
|
5
5
|
import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
6
6
|
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
@@ -64,6 +64,35 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
64
64
|
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it('should not add aria-describedby if button label is the same as tooltip label', async () => {
|
|
68
|
+
const label = 'Tooltip label';
|
|
69
|
+
render(<IconButton label={label} tooltipProps={{ forceOpen: true }} />);
|
|
70
|
+
const tooltip = screen.queryByRole('tooltip', { name: label });
|
|
71
|
+
expect(tooltip).toBeInTheDocument();
|
|
72
|
+
const button = screen.queryByRole('button', { name: label });
|
|
73
|
+
expect(button).not.toHaveAttribute('aria-describedby');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should keep anchor aria-describedby if button label is the same as tooltip label', async () => {
|
|
77
|
+
const label = 'Tooltip label';
|
|
78
|
+
render(<IconButton label={label} aria-describedby=":header-1:" tooltipProps={{ forceOpen: true }} />);
|
|
79
|
+
const tooltip = screen.queryByRole('tooltip', { name: label });
|
|
80
|
+
expect(tooltip).toBeInTheDocument();
|
|
81
|
+
const button = screen.queryByRole('button', { name: label });
|
|
82
|
+
expect(button).toHaveAttribute('aria-describedby', ':header-1:');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should concat aria-describedby if already exists', async () => {
|
|
86
|
+
const { tooltip } = await setup({
|
|
87
|
+
label: 'Tooltip label',
|
|
88
|
+
children: <Button aria-describedby=":header-1:">Anchor</Button>,
|
|
89
|
+
forceOpen: true,
|
|
90
|
+
});
|
|
91
|
+
expect(tooltip).toBeInTheDocument();
|
|
92
|
+
const button = screen.queryByRole('button', { name: 'Anchor' });
|
|
93
|
+
expect(button).toHaveAttribute('aria-describedby', `:header-1: ${tooltip?.id}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
67
96
|
it('should wrap disabled Button', async () => {
|
|
68
97
|
const { tooltip, anchorWrapper } = await setup({
|
|
69
98
|
label: 'Tooltip label',
|
|
@@ -87,7 +87,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
87
87
|
const position = attributes?.popper?.['data-popper-placement'] ?? placement;
|
|
88
88
|
const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
|
|
89
89
|
const isOpen = isActivated || forceOpen;
|
|
90
|
-
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id);
|
|
90
|
+
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id, label);
|
|
91
91
|
|
|
92
92
|
return (
|
|
93
93
|
<>
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import get from 'lodash/get';
|
|
2
|
-
import isUndefined from 'lodash/isUndefined';
|
|
3
1
|
import React, { cloneElement, ReactNode, useMemo } from 'react';
|
|
4
2
|
|
|
5
|
-
import {
|
|
3
|
+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* Add ref and ARIA attribute(s) in tooltip children or wrapped children.
|
|
@@ -13,6 +11,7 @@ import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
|
13
11
|
* @param setAnchorElement Set tooltip anchor element.
|
|
14
12
|
* @param isOpen Whether the tooltip is open or not.
|
|
15
13
|
* @param id Tooltip id.
|
|
14
|
+
* @param label Tooltip label.
|
|
16
15
|
* @return tooltip anchor.
|
|
17
16
|
*/
|
|
18
17
|
export const useInjectTooltipRef = (
|
|
@@ -20,32 +19,29 @@ export const useInjectTooltipRef = (
|
|
|
20
19
|
setAnchorElement: (e: HTMLDivElement) => void,
|
|
21
20
|
isOpen: boolean | undefined,
|
|
22
21
|
id: string,
|
|
22
|
+
label: string,
|
|
23
23
|
): ReactNode => {
|
|
24
|
+
const element = React.isValidElement(children) ? (children as any) : null;
|
|
25
|
+
const ref = useMergeRefs(element?.ref, setAnchorElement);
|
|
26
|
+
|
|
24
27
|
return useMemo(() => {
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
: false;
|
|
29
|
-
const ariaProps = { 'aria-describedby': isOpen && !childrenHasAriaProp ? id : undefined };
|
|
28
|
+
// Non-disabled element
|
|
29
|
+
if (element && element.props?.disabled !== true && element.props?.isDisabled !== true) {
|
|
30
|
+
const props = { ...element.props, ref };
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
get(children, 'props.isDisabled') !== true
|
|
36
|
-
) {
|
|
37
|
-
const element = children as any;
|
|
32
|
+
// Add current tooltip to the aria-describedby if the label is not already present
|
|
33
|
+
if (label !== props['aria-label']) {
|
|
34
|
+
props['aria-describedby'] = [props['aria-describedby'], id].filter(Boolean).join(' ');
|
|
35
|
+
}
|
|
38
36
|
|
|
39
|
-
return cloneElement(element,
|
|
40
|
-
...element.props,
|
|
41
|
-
...ariaProps,
|
|
42
|
-
ref: mergeRefs(element.ref, setAnchorElement),
|
|
43
|
-
});
|
|
37
|
+
return cloneElement(element, props);
|
|
44
38
|
}
|
|
39
|
+
|
|
40
|
+
// Else add a wrapper around the children
|
|
45
41
|
return (
|
|
46
|
-
<div className="lumx-tooltip-anchor-wrapper" ref={
|
|
42
|
+
<div className="lumx-tooltip-anchor-wrapper" ref={ref} aria-describedby={isOpen ? id : undefined}>
|
|
47
43
|
{children}
|
|
48
44
|
</div>
|
|
49
45
|
);
|
|
50
|
-
}, [isOpen, id,
|
|
46
|
+
}, [element, children, setAnchorElement, isOpen, id, ref, label]);
|
|
51
47
|
};
|