@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/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.4",
11
- "@lumx/icons": "^3.6.4",
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.4"
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 { mergeRefs } from '@lumx/react/utils/mergeRefs';
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
- // 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 };
28
+ // Non-disabled element
29
+ if (element && element.props?.disabled !== true && element.props?.isDisabled !== true) {
30
+ const props = { ...element.props, ref };
30
31
 
31
- if (
32
- children &&
33
- get(children, '$$typeof') &&
34
- get(children, 'props.disabled') !== true &&
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={setAnchorElement} {...ariaProps}>
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, children, setAnchorElement]);
46
+ }, [element, children, setAnchorElement, isOpen, id, ref, label]);
51
47
  };