diff --git a/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx b/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx index b4dce630d9..881223f12f 100644 --- a/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx +++ b/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx @@ -232,4 +232,68 @@ describe('TooltipDialog', () => { expect(onCloseSpy).toHaveBeenCalled(); }); }); + + describe('keepMounted', () => { + it('keeps dialog mounted in the DOM while visually hidden', () => { + const { queryByRole, getByTestId } = render( + + ); + + // Closed (hidden) state: still mounted but visually hidden and aria-hidden on backdrop + const hiddenDialog = queryByRole('dialog', { hidden: true }); + + expect(hiddenDialog).not.toBeNull(); + expect(getByTestId('backdrop')).toHaveAttribute('aria-hidden', 'true'); + // Backdrop should have hideVisually styles applied (position: absolute, clip, etc.) + const backdropStyles = window.getComputedStyle(getByTestId('backdrop')); + expect(backdropStyles.position).toBe('absolute'); + expect(backdropStyles.width).toBe('1px'); + expect(backdropStyles.height).toBe('1px'); + }); + + it('toggles visibility and manages focus correctly when reopened', async () => { + const { getByText, getByRole, queryByRole, getByTestId } = render( + + ); + + const trigger = getByText('open'); + + // Initially hidden but mounted + const initiallyHiddenDialog = queryByRole('dialog', { hidden: true }); + expect(initiallyHiddenDialog).not.toBeNull(); + expect(getByTestId('backdrop')).toHaveAttribute('aria-hidden', 'true'); + + // Open + await act(async () => { + await user.click(trigger); + }); + + const openDialog = getByRole('dialog'); + expect(openDialog).toBeInTheDocument(); + expect(getByTestId('backdrop')).not.toHaveAttribute('aria-hidden'); + expect(openDialog).toHaveFocus(); + // Backdrop should NOT have hideVisually styles when visible + const visibleBackdropStyles = window.getComputedStyle(getByTestId('backdrop')); + expect(visibleBackdropStyles.position).toBe('fixed'); + + // Close (toggle button again) + await act(async () => { + await user.click(trigger); + }); + + // Dialog remains mounted but visually hidden again + const hiddenAgainDialog = queryByRole('dialog', { hidden: true }); + expect(hiddenAgainDialog).not.toBeNull(); + await waitFor(() => { + expect(getByTestId('backdrop')).toHaveAttribute('aria-hidden', 'true'); + // Backdrop should have hideVisually styles applied again + const hiddenBackdropStyles = window.getComputedStyle(getByTestId('backdrop')); + expect(hiddenBackdropStyles.position).toBe('absolute'); + expect(hiddenBackdropStyles.width).toBe('1px'); + expect(hiddenBackdropStyles.height).toBe('1px'); + }); + // Focus should return to trigger + expect(trigger).toHaveFocus(); + }); + }); }); diff --git a/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx b/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx index 81691d5bec..d14bed2a9a 100644 --- a/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx +++ b/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx @@ -36,6 +36,12 @@ import { createPortal } from 'react-dom'; const PLACEMENT_DEFAULT = 'top'; +/** + * 1. When content is kept mounted we must manually focus on re-open + * 2. Hide only at 'exited' so exit animations finish + * and floating-ui sizing/focus logic remain valid during 'exiting'. + * Earlier hiding would cut animation and risk focus/layout issues. + */ const TooltipDialogComponent = React.forwardRef( ( { @@ -46,6 +52,7 @@ const TooltipDialogComponent = React.forwardRef { + if (keepMounted && focusOnMount && modalRef.current) { + modalRef.current.focus(); // [1] + } + }} > {transitionState => { + const isHidden = keepMounted && transitionState === 'exited'; // [2] + return ( )} {...backdropProps} ref={transitionRef} + aria-hidden={isHidden ? true : undefined} > props['aria-hidden'] && hideVisually()} + ${componentStyles}; `; diff --git a/packages/modals/src/types/index.ts b/packages/modals/src/types/index.ts index 71583390b6..5bb231660f 100644 --- a/packages/modals/src/types/index.ts +++ b/packages/modals/src/types/index.ts @@ -83,6 +83,10 @@ export interface ITooltipDialogProps extends Omit