Skip to content

Commit

Permalink
chore(tooltip): skip async state updates in jsdom env
Browse files Browse the repository at this point in the history
  • Loading branch information
gcornut committed Sep 25, 2024
1 parent 25b795a commit c449e77
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { usePopper } from 'react-popper';
import memoize from 'lodash/memoize';
import { detectOverflow } from '@popperjs/core';

import { DOCUMENT, WINDOW } from '@lumx/react/constants';
import { DOCUMENT, IS_JSDOM_ENV, WINDOW } from '@lumx/react/constants';
import { PopoverProps } from '@lumx/react/components/popover/Popover';
import { ARROW_SIZE, FitAnchorWidth, Placement } from './constants';

Expand Down Expand Up @@ -104,7 +104,7 @@ export function usePopoverStyle({
}: Options): Output {
const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);

if (navigator.userAgent.includes('jsdom')) {
if (IS_JSDOM_ENV) {
// Skip all logic; we don't need popover positioning in jsdom.
return { styles: {}, attributes: {}, isPositioned: true, popperElement, setPopperElement };
}
Expand Down
33 changes: 15 additions & 18 deletions packages/lumx-react/src/components/tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { Button, IconButton } from '@lumx/react';
import { screen, render, waitFor } from '@testing-library/react';
import { screen, render } from '@testing-library/react';
import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -159,17 +159,17 @@ describe(`<${Tooltip.displayName}>`, () => {
expect(ref.current === element).toBe(true);
});

it.only('should render in closeMode=hide', async () => {
const { tooltip, anchorWrapper } = await setup({
it('should render in closeMode=hide', async () => {
const { tooltip } = await setup({
label: 'Tooltip label',
children: <Button>Anchor</Button>,
closeMode: 'hide',
forceOpen: false,
});
expect(tooltip).toBeInTheDocument();
expect(anchorWrapper).toBeInTheDocument();
expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
expect(tooltip).toHaveClass('lumx-tooltip--is-hidden');
const button = screen.queryByRole('button', { name: 'Anchor' });
expect(button?.parentElement).toBe(anchorWrapper);
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
});
});

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

// Un-hover anchor button
userEvent.unhover(button);
await waitFor(() => {
expect(button).not.toHaveFocus();
// Tooltip closed
expect(tooltip).not.toBeInTheDocument();
});
await userEvent.unhover(button);

expect(button).not.toHaveFocus();
// Tooltip closed
expect(tooltip).not.toBeInTheDocument();
});

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

// Un-hover tooltip
userEvent.unhover(tooltip);
await waitFor(() => {
expect(button).not.toHaveFocus();
// Tooltip closed
expect(tooltip).not.toBeInTheDocument();
});
await userEvent.unhover(tooltip);
expect(button).not.toHaveFocus();
// Tooltip closed
expect(tooltip).not.toBeInTheDocument();
});

it('should activate on anchor focus visible and close on escape', async () => {
Expand Down
12 changes: 8 additions & 4 deletions packages/lumx-react/src/components/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { usePopper } from 'react-popper';

import classNames from 'classnames';

import { DOCUMENT } from '@lumx/react/constants';
import { DOCUMENT, IS_JSDOM_ENV } from '@lumx/react/constants';
import { Comp, GenericProps, HasCloseMode } from '@lumx/react/utils/type';
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
import { Placement } from '@lumx/react/components/popover';
import { TooltipContextProvider } from '@lumx/react/components/tooltip/context';
import { useId } from '@lumx/react/hooks/useId';
Expand Down Expand Up @@ -58,6 +58,9 @@ const DEFAULT_PROPS: Partial<TooltipProps> = {
*/
const ARROW_SIZE = 8;

// Skip popper logic in jsdom env
const usePopperHook: typeof usePopper = IS_JSDOM_ENV ? () => ({}) as any : usePopper;

/**
* Tooltip component.
*
Expand All @@ -76,7 +79,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re

const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
const [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);
const { styles, attributes } = usePopper(anchorElement, popperElement, {
const { styles = {}, attributes = {} } = usePopperHook(anchorElement, popperElement, {
placement,
modifiers: [
{
Expand All @@ -94,13 +97,14 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re

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

const tooltipRef = useMergeRefs(ref, setPopperElement, onPopperMount);
return (
<>
<TooltipContextProvider>{wrappedChildren}</TooltipContextProvider>
{isMounted &&
createPortal(
<div
ref={mergeRefs(ref, setPopperElement, onPopperMount)}
ref={tooltipRef}
{...forwardedProps}
id={id}
role="tooltip"
Expand Down
9 changes: 6 additions & 3 deletions packages/lumx-react/src/components/tooltip/useTooltipOpen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MutableRefObject, useEffect, useRef, useState } from 'react';
import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
import { IS_JSDOM_ENV, TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';

Expand Down Expand Up @@ -31,9 +31,12 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
// Run timer to defer updating the isOpen state.
const deferUpdate = (duration: number) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
const update = () => {
setIsOpen(!!shouldOpen);
}, duration) as any;
};
// Skip timeout in jsdom env
if (IS_JSDOM_ENV) update();
else timer = setTimeout(update, duration) as any;
};

const hoverNotSupported = browserDoesNotSupportHover();
Expand Down
5 changes: 5 additions & 0 deletions packages/lumx-react/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ export const WINDOW = typeof window !== 'undefined' ? window : undefined;
* Optional global `document` instance (not defined when running SSR).
*/
export const DOCUMENT = typeof document !== 'undefined' ? document : undefined;

/**
* Check if we are running in the simulated DOM jsdom environment
*/
export const IS_JSDOM_ENV = typeof navigator !== 'undefined' && navigator.userAgent.includes('jsdom');

0 comments on commit c449e77

Please sign in to comment.