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'); + }); });