From 591b59e5314d10096263b352ccacf0c6a2b22600 Mon Sep 17 00:00:00 2001 From: RyanGreenup <47832594+RyanGreenup@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:01:35 +1000 Subject: [PATCH] fix(solid): floating panel presence reactivity (#3936) fix(solid): react to presence changes in floating panel Content and Positioner Content and Positioner only read presence state during render, so lazyMount never mounted them and unmountOnExit never removed them. Wrap both in Show to track presence reactively. Forwarding the ref on Content tracks exit animations before unmounting. --- .changeset/solid-floating-panel-lazy-mount.md | 7 +++ .../floating-panel/floating-panel-content.tsx | 12 +++-- .../floating-panel-positioner.tsx | 11 ++-- .../components/floating-panel/tests/basic.tsx | 19 +++++++ .../tests/floating-panel.test.tsx | 52 +++++++++++++++++++ 5 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 .changeset/solid-floating-panel-lazy-mount.md create mode 100644 packages/solid/src/components/floating-panel/tests/basic.tsx create mode 100644 packages/solid/src/components/floating-panel/tests/floating-panel.test.tsx diff --git a/.changeset/solid-floating-panel-lazy-mount.md b/.changeset/solid-floating-panel-lazy-mount.md new file mode 100644 index 0000000000..58a3dd8efe --- /dev/null +++ b/.changeset/solid-floating-panel-lazy-mount.md @@ -0,0 +1,7 @@ +--- +'@ark-ui/solid': patch +--- + +Fixed FloatingPanel `Content` and `Positioner` not reacting to presence changes. The panel never appeared when +`lazyMount` was used, and was never removed from the DOM when `unmountOnExit` was used. `Content` now also forwards the +presence ref so exit animations are tracked before unmounting. diff --git a/packages/solid/src/components/floating-panel/floating-panel-content.tsx b/packages/solid/src/components/floating-panel/floating-panel-content.tsx index 730397e0a8..c698f0696b 100644 --- a/packages/solid/src/components/floating-panel/floating-panel-content.tsx +++ b/packages/solid/src/components/floating-panel/floating-panel-content.tsx @@ -1,4 +1,6 @@ import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' +import { composeRefs } from '../../utils/compose-refs.ts' import { type HTMLProps, type PolymorphicProps, ark } from '../factory.tsx' import { usePresenceContext } from '../presence/index.tsx' import { useFloatingPanelContext } from './use-floating-panel-context.ts' @@ -15,9 +17,9 @@ export const FloatingPanelContent = (props: FloatingPanelContentProps) => { props, ) - if (presence().unmounted) { - return null - } - - return + return ( + + + + ) } diff --git a/packages/solid/src/components/floating-panel/floating-panel-positioner.tsx b/packages/solid/src/components/floating-panel/floating-panel-positioner.tsx index 8aa98ef66f..9be83c73a7 100644 --- a/packages/solid/src/components/floating-panel/floating-panel-positioner.tsx +++ b/packages/solid/src/components/floating-panel/floating-panel-positioner.tsx @@ -1,4 +1,5 @@ import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' import { type HTMLProps, type PolymorphicProps, ark } from '../factory.tsx' import { usePresenceContext } from '../presence/index.tsx' import { useFloatingPanelContext } from './use-floating-panel-context.ts' @@ -11,9 +12,9 @@ export const FloatingPanelPositioner = (props: FloatingPanelPositionerProps) => const mergedProps = mergeProps(() => floatingPanel().getPositionerProps(), props) const presence = usePresenceContext() - if (presence().unmounted) { - return null - } - - return + return ( + + + + ) } diff --git a/packages/solid/src/components/floating-panel/tests/basic.tsx b/packages/solid/src/components/floating-panel/tests/basic.tsx new file mode 100644 index 0000000000..f40f7098a3 --- /dev/null +++ b/packages/solid/src/components/floating-panel/tests/basic.tsx @@ -0,0 +1,19 @@ +import { FloatingPanel } from '@ark-ui/solid/floating-panel' +import { Portal } from 'solid-js/web' + +export const ComponentUnderTest = (props: FloatingPanel.RootProps) => ( + + Open Panel + + + + + Floating Panel + Close + + Panel Body + + + + +) diff --git a/packages/solid/src/components/floating-panel/tests/floating-panel.test.tsx b/packages/solid/src/components/floating-panel/tests/floating-panel.test.tsx new file mode 100644 index 0000000000..4851744bc4 --- /dev/null +++ b/packages/solid/src/components/floating-panel/tests/floating-panel.test.tsx @@ -0,0 +1,52 @@ +import { render, screen, waitFor } from '@solidjs/testing-library' +import user from '@testing-library/user-event' +import { ComponentUnderTest } from './basic.tsx' + +describe('FloatingPanel', () => { + it('should show panel content when opened', async () => { + render(() => ) + + await user.click(screen.getByText('Open Panel')) + expect(await screen.findByText('Panel Body')).toBeVisible() + + await user.click(screen.getByText('Close')) + await waitFor(() => expect(screen.queryByText('Panel Body')).not.toBeVisible()) + }) + + it('should invoke onOpenChange if panel is closed', async () => { + const onOpenChange = vi.fn() + render(() => ) + await user.click(screen.getByText('Close')) + + expect(onOpenChange).toHaveBeenCalledTimes(1) + }) + + it('should be able to lazy mount', async () => { + render(() => ) + + expect(screen.queryByTestId('positioner')).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'Open Panel' })) + expect(screen.getByTestId('positioner')).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'Close Window' })) + expect(screen.getByTestId('positioner')).toBeInTheDocument() + }) + + it('should not have aria-controls if lazy mounted', async () => { + render(() => ) + expect(screen.getByRole('button', { name: 'Open Panel' })).not.toHaveAttribute('aria-controls') + }) + + it('should lazy mount and unmount on exit', async () => { + render(() => ) + + expect(screen.queryByTestId('positioner')).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'Open Panel' })) + expect(screen.getByTestId('positioner')).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'Close Window' })) + await waitFor(() => expect(screen.queryByTestId('positioner')).not.toBeInTheDocument()) + }) +})