Skip to content

Commit 52aba50

Browse files
committed
chore(tooltip): skip async state updates in jsdom env
1 parent b99120a commit 52aba50

File tree

6 files changed

+53
-34
lines changed

6 files changed

+53
-34
lines changed

packages/lumx-react/src/components/popover/usePopoverStyle.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useEffect, useMemo, useState } from 'react';
2-
import { usePopper } from 'react-popper';
32
import memoize from 'lodash/memoize';
43
import { detectOverflow } from '@popperjs/core';
54

65
import { DOCUMENT, WINDOW } from '@lumx/react/constants';
76
import { PopoverProps } from '@lumx/react/components/popover/Popover';
7+
import { usePopper } from '@lumx/react/hooks/usePopper';
88
import { ARROW_SIZE, FitAnchorWidth, Placement } from './constants';
99

1010
/**
@@ -104,12 +104,6 @@ export function usePopoverStyle({
104104
}: Options): Output {
105105
const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
106106

107-
if (navigator.userAgent.includes('jsdom')) {
108-
// Skip all logic; we don't need popover positioning in jsdom.
109-
return { styles: {}, attributes: {}, isPositioned: true, popperElement, setPopperElement };
110-
}
111-
112-
// eslint-disable-next-line react-hooks/rules-of-hooks
113107
const [arrowElement, setArrowElement] = useState<null | HTMLElement>(null);
114108

115109
const actualOffset: [number, number] = [offset?.along ?? 0, (offset?.away ?? 0) + (hasArrow ? ARROW_SIZE : 0)];
@@ -142,16 +136,13 @@ export function usePopoverStyle({
142136
);
143137
}
144138

145-
// eslint-disable-next-line react-hooks/rules-of-hooks
146139
const { styles, attributes, state, update } = usePopper(anchorRef.current, popperElement, { placement, modifiers });
147-
// eslint-disable-next-line react-hooks/rules-of-hooks
148140
useEffect(() => {
149141
update?.();
150142
}, [children, update]);
151143

152144
const position = state?.placement ?? placement;
153145

154-
// eslint-disable-next-line react-hooks/rules-of-hooks
155146
const popoverStyle = useMemo(() => {
156147
const newStyles = { ...style, ...styles.popper, zIndex };
157148

packages/lumx-react/src/components/tooltip/Tooltip.test.tsx

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22

33
import { Button, IconButton } from '@lumx/react';
4-
import { screen, render, waitFor } from '@testing-library/react';
4+
import { screen, render } from '@testing-library/react';
55
import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
66
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
77
import userEvent from '@testing-library/user-event';
@@ -48,10 +48,23 @@ describe(`<${Tooltip.displayName}>`, () => {
4848
forceOpen: true,
4949
});
5050
expect(tooltip).toBeInTheDocument();
51+
// Default placement
52+
expect(tooltip).toHaveAttribute('data-popper-placement', 'bottom');
5153
expect(anchorWrapper).toBeInTheDocument();
5254
expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
5355
});
5456

57+
it('should render with custom placement', async () => {
58+
const { tooltip } = await setup({
59+
label: 'Tooltip label',
60+
children: 'Anchor',
61+
forceOpen: true,
62+
placement: 'top',
63+
});
64+
// Custom placement
65+
expect(tooltip).toHaveAttribute('data-popper-placement', 'top');
66+
});
67+
5568
it('should wrap unknown children and not add aria-describedby when closed', async () => {
5669
const { anchorWrapper } = await setup({
5770
label: 'Tooltip label',
@@ -159,17 +172,17 @@ describe(`<${Tooltip.displayName}>`, () => {
159172
expect(ref.current === element).toBe(true);
160173
});
161174

162-
it.only('should render in closeMode=hide', async () => {
163-
const { tooltip, anchorWrapper } = await setup({
175+
it('should render in closeMode=hide', async () => {
176+
const { tooltip } = await setup({
164177
label: 'Tooltip label',
165178
children: <Button>Anchor</Button>,
166179
closeMode: 'hide',
180+
forceOpen: false,
167181
});
168182
expect(tooltip).toBeInTheDocument();
169-
expect(anchorWrapper).toBeInTheDocument();
170-
expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
183+
expect(tooltip).toHaveClass('lumx-tooltip--is-hidden');
171184
const button = screen.queryByRole('button', { name: 'Anchor' });
172-
expect(button?.parentElement).toBe(anchorWrapper);
185+
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
173186
});
174187
});
175188

@@ -193,12 +206,11 @@ describe(`<${Tooltip.displayName}>`, () => {
193206
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
194207

195208
// Un-hover anchor button
196-
userEvent.unhover(button);
197-
await waitFor(() => {
198-
expect(button).not.toHaveFocus();
199-
// Tooltip closed
200-
expect(tooltip).not.toBeInTheDocument();
201-
});
209+
await userEvent.unhover(button);
210+
211+
expect(button).not.toHaveFocus();
212+
// Tooltip closed
213+
expect(tooltip).not.toBeInTheDocument();
202214
});
203215

204216
it('should activate on hover anchor and then tooltip', async () => {
@@ -225,12 +237,10 @@ describe(`<${Tooltip.displayName}>`, () => {
225237
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
226238

227239
// Un-hover tooltip
228-
userEvent.unhover(tooltip);
229-
await waitFor(() => {
230-
expect(button).not.toHaveFocus();
231-
// Tooltip closed
232-
expect(tooltip).not.toBeInTheDocument();
233-
});
240+
await userEvent.unhover(tooltip);
241+
expect(button).not.toHaveFocus();
242+
// Tooltip closed
243+
expect(tooltip).not.toBeInTheDocument();
234244
});
235245

236246
it('should activate on anchor focus visible and close on escape', async () => {

packages/lumx-react/src/components/tooltip/Tooltip.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
/* eslint-disable react-hooks/rules-of-hooks */
22
import React, { forwardRef, ReactNode, useState } from 'react';
33
import { createPortal } from 'react-dom';
4-
import { usePopper } from 'react-popper';
54

65
import classNames from 'classnames';
76

87
import { DOCUMENT } from '@lumx/react/constants';
98
import { Comp, GenericProps, HasCloseMode } from '@lumx/react/utils/type';
109
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
11-
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
10+
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
1211
import { Placement } from '@lumx/react/components/popover';
1312
import { TooltipContextProvider } from '@lumx/react/components/tooltip/context';
1413
import { useId } from '@lumx/react/hooks/useId';
1514

1615
import { useInjectTooltipRef } from './useInjectTooltipRef';
1716
import { useTooltipOpen } from './useTooltipOpen';
17+
import { usePopper } from '@lumx/react/hooks/usePopper';
1818

1919
/** Position of the tooltip relative to the anchor element. */
2020
export type TooltipPlacement = Extract<Placement, 'top' | 'right' | 'bottom' | 'left'>;
@@ -94,13 +94,14 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
9494

9595
const labelLines = label ? label.split('\n') : [];
9696

97+
const tooltipRef = useMergeRefs(ref, setPopperElement, onPopperMount);
9798
return (
9899
<>
99100
<TooltipContextProvider>{wrappedChildren}</TooltipContextProvider>
100101
{isMounted &&
101102
createPortal(
102103
<div
103-
ref={mergeRefs(ref, setPopperElement, onPopperMount)}
104+
ref={tooltipRef}
104105
{...forwardedProps}
105106
id={id}
106107
role="tooltip"

packages/lumx-react/src/components/tooltip/useTooltipOpen.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MutableRefObject, useEffect, useRef, useState } from 'react';
22
import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
3-
import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
3+
import { IS_BROWSER, TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
44
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
55
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
66

@@ -31,9 +31,12 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
3131
// Run timer to defer updating the isOpen state.
3232
const deferUpdate = (duration: number) => {
3333
if (timer) clearTimeout(timer);
34-
timer = setTimeout(() => {
34+
const update = () => {
3535
setIsOpen(!!shouldOpen);
36-
}, duration) as any;
36+
};
37+
// Skip timeout in fake browsers
38+
if (!IS_BROWSER) update();
39+
else timer = setTimeout(update, duration) as any;
3740
};
3841

3942
const hoverNotSupported = browserDoesNotSupportHover();

packages/lumx-react/src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ export const WINDOW = typeof window !== 'undefined' ? window : undefined;
1515
* Optional global `document` instance (not defined when running SSR).
1616
*/
1717
export const DOCUMENT = typeof document !== 'undefined' ? document : undefined;
18+
19+
/**
20+
* Check if we are running in a true browser
21+
*/
22+
export const IS_BROWSER = typeof navigator !== 'undefined' && !navigator.userAgent.includes('jsdom');
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { usePopper as usePopperHook } from 'react-popper';
2+
import { IS_BROWSER } from '@lumx/react/constants';
3+
4+
/** Stub usePopper for use outside of browsers */
5+
const useStubPopper: typeof usePopperHook = (_a, _p, { placement }: any) =>
6+
({ attributes: { popper: { 'data-popper-placement': placement } }, styles: {} }) as any;
7+
8+
/** Switch hook implementation between environment */
9+
export const usePopper: typeof usePopperHook = IS_BROWSER ? usePopperHook : useStubPopper;

0 commit comments

Comments
 (0)