From 8b164066e9394c22610f2232ccb53ed7d0cc8ee3 Mon Sep 17 00:00:00 2001 From: Florent Mathieu Date: Thu, 16 Oct 2025 08:01:51 -1000 Subject: [PATCH 1/3] feat(TooltipDialog): support hiding (not unmounting) dialog content --- .../modals/demo/tooltipDialog.stories.mdx | 2 + .../TooltipDialog/TooltipDialog.spec.tsx | 49 +++++++++++++++++++ .../elements/TooltipDialog/TooltipDialog.tsx | 24 ++++++++- packages/modals/src/types/index.ts | 4 ++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/modals/demo/tooltipDialog.stories.mdx b/packages/modals/demo/tooltipDialog.stories.mdx index 4cb12e38ac6..6a6cf170c64 100644 --- a/packages/modals/demo/tooltipDialog.stories.mdx +++ b/packages/modals/demo/tooltipDialog.stories.mdx @@ -31,6 +31,7 @@ import README from '../README.md'; focusOnMount: true, hasArrow: true, restoreFocus: true, + keepMounted: false, hasBody: true, body: BODY, hasClose: true, @@ -43,6 +44,7 @@ import README from '../README.md'; argTypes={{ referenceElement: { control: false }, fallbackPlacements: { control: 'multi-select', options: PLACEMENT.filter(p => p !== 'auto') }, + keepMounted: { control: 'boolean' }, hasBody: { name: 'TooltipDialog.Body', table: { category: 'Story' } }, hasClose: { name: 'TooltipDialog.Close', table: { category: 'Story' } }, hasFooter: { name: 'TooltipDialog.Footer', table: { category: 'Story' } }, diff --git a/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx b/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx index b4dce630d9d..e26cb84025c 100644 --- a/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx +++ b/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx @@ -232,4 +232,53 @@ describe('TooltipDialog', () => { expect(onCloseSpy).toHaveBeenCalled(); }); }); + + describe('keepMounted', () => { + it('keeps dialog mounted in the DOM while hidden', () => { + const { queryByRole, getByTestId } = render( + + ); + + // Closed (hidden) state: still mounted but aria-hidden on backdrop + const hiddenDialog = queryByRole('dialog', { hidden: true }); + + expect(hiddenDialog).not.toBeNull(); + expect(getByTestId('backdrop')).toHaveAttribute('aria-hidden', 'true'); + }); + + 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(); + + // Close (toggle button again) + await act(async () => { + await user.click(trigger); + }); + + // Dialog remains mounted but hidden again + const hiddenAgainDialog = queryByRole('dialog', { hidden: true }); + expect(hiddenAgainDialog).not.toBeNull(); + await waitFor(() => expect(getByTestId('backdrop')).toHaveAttribute('aria-hidden', 'true')); + // 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 81691d5bece..cab10adc50d 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} + style={{ + ...backdropProps?.style, + ...(isHidden + ? { visibility: 'hidden', pointerEvents: 'none' } + : { visibility: undefined }) + }} + aria-hidden={isHidden ? true : undefined} > Date: Sun, 19 Oct 2025 17:21:20 -1000 Subject: [PATCH 2/3] chore: PR feedback --- .../modals/demo/tooltipDialog.stories.mdx | 2 -- .../TooltipDialog/TooltipDialog.spec.tsx | 23 +++++++++++++++---- .../elements/TooltipDialog/TooltipDialog.tsx | 10 +++----- .../src/styled/StyledTooltipDialogBackdrop.ts | 9 +++++++- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/modals/demo/tooltipDialog.stories.mdx b/packages/modals/demo/tooltipDialog.stories.mdx index 6a6cf170c64..4cb12e38ac6 100644 --- a/packages/modals/demo/tooltipDialog.stories.mdx +++ b/packages/modals/demo/tooltipDialog.stories.mdx @@ -31,7 +31,6 @@ import README from '../README.md'; focusOnMount: true, hasArrow: true, restoreFocus: true, - keepMounted: false, hasBody: true, body: BODY, hasClose: true, @@ -44,7 +43,6 @@ import README from '../README.md'; argTypes={{ referenceElement: { control: false }, fallbackPlacements: { control: 'multi-select', options: PLACEMENT.filter(p => p !== 'auto') }, - keepMounted: { control: 'boolean' }, hasBody: { name: 'TooltipDialog.Body', table: { category: 'Story' } }, hasClose: { name: 'TooltipDialog.Close', table: { category: 'Story' } }, hasFooter: { name: 'TooltipDialog.Footer', table: { category: 'Story' } }, diff --git a/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx b/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx index e26cb84025c..881223f12f2 100644 --- a/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx +++ b/packages/modals/src/elements/TooltipDialog/TooltipDialog.spec.tsx @@ -234,16 +234,21 @@ describe('TooltipDialog', () => { }); describe('keepMounted', () => { - it('keeps dialog mounted in the DOM while hidden', () => { + it('keeps dialog mounted in the DOM while visually hidden', () => { const { queryByRole, getByTestId } = render( ); - // Closed (hidden) state: still mounted but aria-hidden on backdrop + // 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 () => { @@ -267,16 +272,26 @@ describe('TooltipDialog', () => { 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 hidden again + // 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')); + 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 cab10adc50d..d6a5dff34be 100644 --- a/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx +++ b/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx @@ -52,7 +52,7 @@ const TooltipDialogComponent = React.forwardRef)} {...backdropProps} ref={transitionRef} - style={{ - ...backdropProps?.style, - ...(isHidden - ? { visibility: 'hidden', pointerEvents: 'none' } - : { visibility: undefined }) - }} + style={backdropProps?.style} + $isHidden={isHidden} aria-hidden={isHidden ? true : undefined} > ` position: fixed; inset: 0; z-index: 400; @@ -34,5 +39,7 @@ export const StyledTooltipDialogBackdrop = styled.div.attrs({ opacity: 0; } + ${props => props.$isHidden && hideVisually()} + ${componentStyles}; `; From 8ae9a610c47bd68b18ca0d53744986afc5639f1b Mon Sep 17 00:00:00 2001 From: Florent Mathieu Date: Mon, 20 Oct 2025 08:26:29 -1000 Subject: [PATCH 3/3] chore: clean up --- .../modals/src/elements/TooltipDialog/TooltipDialog.tsx | 2 -- packages/modals/src/styled/StyledTooltipDialogBackdrop.ts | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx b/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx index d6a5dff34be..d14bed2a9a5 100644 --- a/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx +++ b/packages/modals/src/elements/TooltipDialog/TooltipDialog.tsx @@ -175,8 +175,6 @@ const TooltipDialogComponent = React.forwardRef)} {...backdropProps} ref={transitionRef} - style={backdropProps?.style} - $isHidden={isHidden} aria-hidden={isHidden ? true : undefined} > ` +})` position: fixed; inset: 0; z-index: 400; @@ -39,7 +35,7 @@ export const StyledTooltipDialogBackdrop = styled.div.attrs({ opacity: 0; } - ${props => props.$isHidden && hideVisually()} + ${props => props['aria-hidden'] && hideVisually()} ${componentStyles}; `;