From 6988ff5d7b86f8d3a792b488806768fa1118c53d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 24 Aug 2026 20:59:07 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=82=EF=B8=8F=20fix:=20Unclip=20the=20Shar?= =?UTF-8?q?e=20Dialog's=20Public=20Role=20Menu=20(#15177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #14734 replaced PublicSharingToggle's hand-rolled reveal (which set overflow: visible while open) with the shared Collapse, whose permanent overflow-hidden shears the non-portaled access-roles menu to a sliver. Adds an opt-in overflowVisibleWhenOpen prop to Collapse — clipped while closed and during the closing tween, unclipped once open — so in-tree popovers can escape; the menu stays non-portaled because portaled menus inside modal OGDialogs land aria-hidden and get focus-yanked shut. --- .../Sharing/PublicSharingToggle.tsx | 2 +- .../__tests__/PublicSharingToggle.spec.tsx | 15 ++++++++++ client/src/components/ui/Collapse.tsx | 21 ++++++++++++-- .../components/ui/__tests__/Collapse.spec.tsx | 29 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/client/src/components/Sharing/PublicSharingToggle.tsx b/client/src/components/Sharing/PublicSharingToggle.tsx index 6bcf329894..f7efcf54c1 100644 --- a/client/src/components/Sharing/PublicSharingToggle.tsx +++ b/client/src/components/Sharing/PublicSharingToggle.tsx @@ -85,7 +85,7 @@ export default function PublicSharingToggle({ - +
diff --git a/client/src/components/Sharing/__tests__/PublicSharingToggle.spec.tsx b/client/src/components/Sharing/__tests__/PublicSharingToggle.spec.tsx index 440adb62b2..0cb1f12134 100644 --- a/client/src/components/Sharing/__tests__/PublicSharingToggle.spec.tsx +++ b/client/src/components/Sharing/__tests__/PublicSharingToggle.spec.tsx @@ -26,6 +26,7 @@ describe('PublicSharingToggle', () => { const collapse = permissionLabel.closest('[aria-hidden="true"]'); expect(collapse).toHaveClass('grid-rows-[0fr]'); + expect(permissionLabel.closest('.overflow-hidden')).toBeInTheDocument(); rerender( { expect(permissionLabel.closest('.bg-transparent')).not.toHaveClass('bg-surface-secondary/50'); expect(permissionLabel.closest('.grid')).toHaveClass('grid-rows-[1fr]'); }); + + it('does not clip the open permission row, so the inline role menu can escape the box', () => { + render( + , + ); + + const permissionLabel = screen.getByText('com_ui_everyone_permission_level'); + expect(permissionLabel.closest('.overflow-hidden')).toBeNull(); + expect(permissionLabel.closest('.overflow-visible')).toBeInTheDocument(); + }); }); diff --git a/client/src/components/ui/Collapse.tsx b/client/src/components/ui/Collapse.tsx index 9ddcb69155..e5aced96dc 100644 --- a/client/src/components/ui/Collapse.tsx +++ b/client/src/components/ui/Collapse.tsx @@ -5,6 +5,7 @@ interface CollapseProps { open: boolean; children: ReactNode; className?: string; + overflowVisibleWhenOpen?: boolean; } /** @@ -14,8 +15,19 @@ interface CollapseProps { * cross-fade smoothly without a measuring wrapper fighting nested reveals. * Content fades to soften the swap; while closed it is `inert` (removed from tab * order and the a11y tree) so collapsed form fields can't be focused or read. + * + * `overflowVisibleWhenOpen` lifts the clip while open so non-portaled popovers + * anchored inside (e.g. dropdown menus, which must stay in-tree within modal + * dialogs to remain inside the focus trap) aren't sheared at the box edge; the + * closed state and the closing tween still clip, and the opacity fade masks the + * un-clipped opening tween. */ -export default function Collapse({ open, children, className }: CollapseProps) { +export default function Collapse({ + open, + children, + className, + overflowVisibleWhenOpen = false, +}: CollapseProps) { return (
-
+
{ expect(root).toHaveClass('grid-rows-[0fr]'); expect(root).toHaveAttribute('aria-hidden', 'true'); }); + + test('clips content while open by default', () => { + const { container } = render( + + + , + ); + const clipWrapper = (container.firstChild as HTMLElement).firstElementChild as HTMLElement; + expect(clipWrapper).toHaveClass('overflow-hidden'); + }); + + test('lifts the clip while open for popover-hosting content, restoring it when closed', () => { + const { container, rerender } = render( + + + , + ); + const clipWrapper = (container.firstChild as HTMLElement).firstElementChild as HTMLElement; + expect(clipWrapper).toHaveClass('overflow-visible'); + expect(clipWrapper).not.toHaveClass('overflow-hidden'); + + rerender( + + + , + ); + expect(clipWrapper).toHaveClass('overflow-hidden'); + expect(clipWrapper).not.toHaveClass('overflow-visible'); + }); });