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())
+ })
+})