@lumx/react 3.9.1 → 3.9.2-alpha.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/package.json CHANGED
@@ -6,8 +6,8 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@lumx/core": "^3.9.1",
10
- "@lumx/icons": "^3.9.1",
9
+ "@lumx/core": "^3.9.2-alpha.1",
10
+ "@lumx/icons": "^3.9.2-alpha.1",
11
11
  "@popperjs/core": "^2.5.4",
12
12
  "body-scroll-lock": "^3.1.5",
13
13
  "classnames": "^2.3.2",
@@ -110,5 +110,5 @@
110
110
  "build:storybook": "storybook build"
111
111
  },
112
112
  "sideEffects": false,
113
- "version": "3.9.1"
113
+ "version": "3.9.2-alpha.1"
114
114
  }
@@ -3,7 +3,7 @@ import { usePopper } from 'react-popper';
3
3
  import memoize from 'lodash/memoize';
4
4
  import { detectOverflow } from '@popperjs/core';
5
5
 
6
- import { DOCUMENT, WINDOW } from '@lumx/react/constants';
6
+ import { DOCUMENT, IS_JSDOM_ENV, WINDOW } from '@lumx/react/constants';
7
7
  import { PopoverProps } from '@lumx/react/components/popover/Popover';
8
8
  import { ARROW_SIZE, FitAnchorWidth, Placement } from './constants';
9
9
 
@@ -104,7 +104,7 @@ export function usePopoverStyle({
104
104
  }: Options): Output {
105
105
  const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
106
106
 
107
- if (navigator.userAgent.includes('jsdom')) {
107
+ if (IS_JSDOM_ENV) {
108
108
  // Skip all logic; we don't need popover positioning in jsdom.
109
109
  return { styles: {}, attributes: {}, isPositioned: true, popperElement, setPopperElement };
110
110
  }
@@ -7,7 +7,7 @@ import { mdiChevronDown, mdiChevronUp } from '@lumx/icons';
7
7
 
8
8
  import { Emphasis, Icon, Size, IconButton, IconButtonProps } from '@lumx/react';
9
9
 
10
- import { Comp, GenericProps, isComponent } from '@lumx/react/utils/type';
10
+ import { Comp, GenericProps, HasCloseMode, isComponent } from '@lumx/react/utils/type';
11
11
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
12
12
  import { renderLink } from '@lumx/react/utils/renderLink';
13
13
  import { renderButtonOrLink } from '@lumx/react/utils/renderButtonOrLink';
@@ -16,7 +16,7 @@ import { useId } from '@lumx/react/hooks/useId';
16
16
  /**
17
17
  * Defines the props of the component.
18
18
  */
19
- export interface SideNavigationItemProps extends GenericProps {
19
+ export interface SideNavigationItemProps extends GenericProps, HasCloseMode {
20
20
  /** SideNavigationItem elements. */
21
21
  children?: ReactNode;
22
22
  /** Emphasis variant. */
@@ -36,11 +36,6 @@ export interface SideNavigationItemProps extends GenericProps {
36
36
  /** Props to pass to the toggle button (minus those already set by the SideNavigationItem props). */
37
37
  toggleButtonProps: Pick<IconButtonProps, 'label'> &
38
38
  Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color' | 'size'>;
39
- /**
40
- * Choose how the children are hidden when closed
41
- * ('hide' keeps the children in DOM but hide them, 'unmount' remove the children from the DOM).
42
- */
43
- closeMode?: 'hide' | 'unmount';
44
39
  /** On action button click callback. */
45
40
  onActionClick?(evt: React.MouseEvent): void;
46
41
  /** On click callback. */
@@ -42,6 +42,14 @@ export const ForceOpen = {
42
42
  },
43
43
  };
44
44
 
45
+ /** Hide on close instead of unmounting */
46
+ export const CloseModeHide = {
47
+ args: {
48
+ ...OnAButton.args,
49
+ closeMode: 'hide',
50
+ },
51
+ };
52
+
45
53
  /** Display a multiline tooltip */
46
54
  export const MultilineTooltip = {
47
55
  args: {
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { Button, IconButton } from '@lumx/react';
4
- import { screen, render, waitFor } from '@testing-library/react';
4
+ import { screen, render } from '@testing-library/react';
5
5
  import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
6
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
7
7
  import userEvent from '@testing-library/user-event';
@@ -158,6 +158,19 @@ describe(`<${Tooltip.displayName}>`, () => {
158
158
  // Children ref is stable
159
159
  expect(ref.current === element).toBe(true);
160
160
  });
161
+
162
+ it('should render in closeMode=hide', async () => {
163
+ const { tooltip } = await setup({
164
+ label: 'Tooltip label',
165
+ children: <Button>Anchor</Button>,
166
+ closeMode: 'hide',
167
+ forceOpen: false,
168
+ });
169
+ expect(tooltip).toBeInTheDocument();
170
+ expect(tooltip).toHaveClass('lumx-tooltip--is-hidden');
171
+ const button = screen.queryByRole('button', { name: 'Anchor' });
172
+ expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
173
+ });
161
174
  });
162
175
 
163
176
  describe('activation', () => {
@@ -180,12 +193,11 @@ describe(`<${Tooltip.displayName}>`, () => {
180
193
  expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
181
194
 
182
195
  // Un-hover anchor button
183
- userEvent.unhover(button);
184
- await waitFor(() => {
185
- expect(button).not.toHaveFocus();
186
- // Tooltip closed
187
- expect(tooltip).not.toBeInTheDocument();
188
- });
196
+ await userEvent.unhover(button);
197
+
198
+ expect(button).not.toHaveFocus();
199
+ // Tooltip closed
200
+ expect(tooltip).not.toBeInTheDocument();
189
201
  });
190
202
 
191
203
  it('should activate on hover anchor and then tooltip', async () => {
@@ -212,12 +224,10 @@ describe(`<${Tooltip.displayName}>`, () => {
212
224
  expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
213
225
 
214
226
  // Un-hover tooltip
215
- userEvent.unhover(tooltip);
216
- await waitFor(() => {
217
- expect(button).not.toHaveFocus();
218
- // Tooltip closed
219
- expect(tooltip).not.toBeInTheDocument();
220
- });
227
+ await userEvent.unhover(tooltip);
228
+ expect(button).not.toHaveFocus();
229
+ // Tooltip closed
230
+ expect(tooltip).not.toBeInTheDocument();
221
231
  });
222
232
 
223
233
  it('should activate on anchor focus visible and close on escape', async () => {
@@ -5,10 +5,10 @@ import { usePopper } from 'react-popper';
5
5
 
6
6
  import classNames from 'classnames';
7
7
 
8
- import { DOCUMENT } from '@lumx/react/constants';
9
- import { Comp, GenericProps } from '@lumx/react/utils/type';
8
+ import { DOCUMENT, IS_JSDOM_ENV } from '@lumx/react/constants';
9
+ import { Comp, GenericProps, HasCloseMode } from '@lumx/react/utils/type';
10
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
11
- import { mergeRefs } from '@lumx/react/utils/mergeRefs';
11
+ import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
12
12
  import { Placement } from '@lumx/react/components/popover';
13
13
  import { TooltipContextProvider } from '@lumx/react/components/tooltip/context';
14
14
  import { useId } from '@lumx/react/hooks/useId';
@@ -22,7 +22,7 @@ export type TooltipPlacement = Extract<Placement, 'top' | 'right' | 'bottom' | '
22
22
  /**
23
23
  * Defines the props of the component.
24
24
  */
25
- export interface TooltipProps extends GenericProps {
25
+ export interface TooltipProps extends GenericProps, HasCloseMode {
26
26
  /** Anchor (element on which we activate the tooltip). */
27
27
  children: ReactNode;
28
28
  /** Delay (in ms) before closing the tooltip. */
@@ -50,6 +50,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
50
50
  */
51
51
  const DEFAULT_PROPS: Partial<TooltipProps> = {
52
52
  placement: Placement.BOTTOM,
53
+ closeMode: 'unmount',
53
54
  };
54
55
 
55
56
  /**
@@ -57,6 +58,9 @@ const DEFAULT_PROPS: Partial<TooltipProps> = {
57
58
  */
58
59
  const ARROW_SIZE = 8;
59
60
 
61
+ // Skip popper logic in jsdom env
62
+ const usePopperHook: typeof usePopper = IS_JSDOM_ENV ? () => ({}) as any : usePopper;
63
+
60
64
  /**
61
65
  * Tooltip component.
62
66
  *
@@ -65,9 +69,9 @@ const ARROW_SIZE = 8;
65
69
  * @return React element.
66
70
  */
67
71
  export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, ref) => {
68
- const { label, children, className, delay, placement, forceOpen, ...forwardedProps } = props;
72
+ const { label, children, className, delay, placement, forceOpen, closeMode, ...forwardedProps } = props;
69
73
  // Disable in SSR.
70
- if (!DOCUMENT) {
74
+ if (!DOCUMENT && closeMode === 'unmount') {
71
75
  return <>{children}</>;
72
76
  }
73
77
 
@@ -75,7 +79,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
75
79
 
76
80
  const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
77
81
  const [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);
78
- const { styles, attributes } = usePopper(anchorElement, popperElement, {
82
+ const { styles = {}, attributes = {} } = usePopperHook(anchorElement, popperElement, {
79
83
  placement,
80
84
  modifiers: [
81
85
  {
@@ -88,28 +92,39 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
88
92
  const position = attributes?.popper?.['data-popper-placement'] ?? placement;
89
93
  const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
90
94
  const isOpen = (isActivated || forceOpen) && !!label;
91
- const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id, label);
95
+ const isMounted = isOpen || closeMode === 'hide';
96
+ const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isMounted, id, label);
97
+
98
+ const labelLines = label ? label.split('\n') : [];
92
99
 
100
+ const tooltipRef = useMergeRefs(ref, setPopperElement, onPopperMount);
93
101
  return (
94
102
  <>
95
103
  <TooltipContextProvider>{wrappedChildren}</TooltipContextProvider>
96
- {isOpen &&
104
+ {isMounted &&
97
105
  createPortal(
98
106
  <div
99
- ref={mergeRefs(ref, setPopperElement, onPopperMount)}
107
+ ref={tooltipRef}
100
108
  {...forwardedProps}
101
109
  id={id}
102
110
  role="tooltip"
103
- aria-label={label}
104
- className={classNames(className, handleBasicClasses({ prefix: CLASSNAME, position }))}
111
+ aria-label={label || ''}
112
+ className={classNames(
113
+ className,
114
+ handleBasicClasses({
115
+ prefix: CLASSNAME,
116
+ position,
117
+ hidden: !isOpen && closeMode === 'hide',
118
+ }),
119
+ )}
105
120
  style={styles.popper}
106
121
  {...attributes.popper}
107
122
  >
108
123
  <div className={`${CLASSNAME}__arrow`} />
109
124
  <div className={`${CLASSNAME}__inner`}>
110
- {label.indexOf('\n') !== -1
111
- ? label.split('\n').map((sentence: string) => <p key={sentence}>{sentence}</p>)
112
- : label}
125
+ {labelLines.map((line) => (
126
+ <p key={line}>{line}</p>
127
+ ))}
113
128
  </div>
114
129
  </div>,
115
130
  document.body,
@@ -1,6 +1,6 @@
1
1
  import { MutableRefObject, useEffect, useRef, useState } from 'react';
2
2
  import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
3
- import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
3
+ import { IS_JSDOM_ENV, TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
4
4
  import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
5
5
  import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
6
6
 
@@ -31,9 +31,12 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
31
31
  // Run timer to defer updating the isOpen state.
32
32
  const deferUpdate = (duration: number) => {
33
33
  if (timer) clearTimeout(timer);
34
- timer = setTimeout(() => {
34
+ const update = () => {
35
35
  setIsOpen(!!shouldOpen);
36
- }, duration) as any;
36
+ };
37
+ // Skip timeout in jsdom env
38
+ if (IS_JSDOM_ENV) update();
39
+ else timer = setTimeout(update, duration) as any;
37
40
  };
38
41
 
39
42
  const hoverNotSupported = browserDoesNotSupportHover();
package/src/constants.ts CHANGED
@@ -15,3 +15,8 @@ export const WINDOW = typeof window !== 'undefined' ? window : undefined;
15
15
  * Optional global `document` instance (not defined when running SSR).
16
16
  */
17
17
  export const DOCUMENT = typeof document !== 'undefined' ? document : undefined;
18
+
19
+ /**
20
+ * Check if we are running in the simulated DOM jsdom environment
21
+ */
22
+ export const IS_JSDOM_ENV = typeof navigator !== 'undefined' && navigator.userAgent.includes('jsdom');
package/src/utils/type.ts CHANGED
@@ -59,6 +59,15 @@ export interface HasClassName {
59
59
  className?: string;
60
60
  }
61
61
 
62
+
63
+ export interface HasCloseMode {
64
+ /**
65
+ * Choose how the children are hidden when closed
66
+ * ('hide' keeps the children in DOM but hide them, 'unmount' remove the children from the DOM).
67
+ */
68
+ closeMode?: 'hide' | 'unmount';
69
+ }
70
+
62
71
  /**
63
72
  * Define a generic props types.
64
73
  */