Portal drawer selects onto the viewport
The swipe Popup keeps a CSS transform that re-roots position:fixed, so Base UI's modal inert cutout missed the locale trigger and blocked hover and clicks. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -40,7 +40,8 @@ interface LocaleSelectProps {
|
|||||||
// Called after a locale change is requested (e.g. close the mobile drawer).
|
// Called after a locale change is requested (e.g. close the mobile drawer).
|
||||||
onLocaleChange?: () => void;
|
onLocaleChange?: () => void;
|
||||||
// Portal target for the menu. Required inside a Drawer/Dialog so the popup
|
// Portal target for the menu. Required inside a Drawer/Dialog so the popup
|
||||||
// is not painted under the modal layer (Select defaults to body + z-3).
|
// stacks in the modal layer (Select defaults to body + z-3). Prefer a
|
||||||
|
// non-transformed container (e.g. Drawer Viewport, not the swipe Popup).
|
||||||
portalContainer?: RefObject<HTMLElement | null>;
|
portalContainer?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,10 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
|
|||||||
const [signOut, isSigningOut] = useSignOut();
|
const [signOut, isSigningOut] = useSignOut();
|
||||||
const { displayMode, toggleDisplayMode } = useDisplayMode();
|
const { displayMode, toggleDisplayMode } = useDisplayMode();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
// Select menus portal to body at z-3 by default; mount them on the drawer
|
// Select menus portal to body at z-3 by default (under the drawer at z-5).
|
||||||
// popup so they stack inside the modal layer (z-5) instead of under it.
|
// Mount them on the drawer Viewport — not the swipe Popup, which keeps a CSS
|
||||||
const drawerPopupRef = useRef<HTMLDivElement>(null);
|
// transform and would break Base UI's modal inert cutout over the trigger.
|
||||||
|
const drawerViewportRef = useRef<HTMLDivElement>(null);
|
||||||
const identity = useFragment(topBarMobileNavFragment, identityKey);
|
const identity = useFragment(topBarMobileNavFragment, identityKey);
|
||||||
const localizedPath = useLocalizedPath();
|
const localizedPath = useLocalizedPath();
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DrawerPopup side="right" ref={drawerPopupRef}>
|
<DrawerPopup side="right" ref={drawerViewportRef}>
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
<DrawerTitle>{t("topBar.menuTitle")}</DrawerTitle>
|
<DrawerTitle>{t("topBar.menuTitle")}</DrawerTitle>
|
||||||
<DrawerClose
|
<DrawerClose
|
||||||
@@ -154,7 +155,7 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
|
|||||||
<LocaleSelect
|
<LocaleSelect
|
||||||
persist={identity != null}
|
persist={identity != null}
|
||||||
onLocaleChange={close}
|
onLocaleChange={close}
|
||||||
portalContainer={drawerPopupRef}
|
portalContainer={drawerViewportRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{identity == null
|
{identity == null
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ export type DrawerPopupProps
|
|||||||
|
|
||||||
// Portal + dimmed backdrop + edge-aligned popup. Children compose the header /
|
// Portal + dimmed backdrop + edge-aligned popup. Children compose the header /
|
||||||
// body / footer regions inside Base UI's Content (swipe-safe text selection).
|
// body / footer regions inside Base UI's Content (swipe-safe text selection).
|
||||||
// `ref` lands on the Popup panel (not the viewport) so nested menus can portal
|
// `ref` lands on the Viewport (not the swipe Popup): nested Select/Menu portals
|
||||||
// into the drawer stacking context.
|
// must mount on a non-transformed ancestor. The Popup keeps a CSS transform for
|
||||||
|
// swipe/enter animation, which would re-root `position: fixed` and misalign
|
||||||
|
// Base UI's modal inert cutout so the trigger looks unclickable.
|
||||||
export function DrawerPopup(props: DrawerPopupProps) {
|
export function DrawerPopup(props: DrawerPopupProps) {
|
||||||
const { className, children, side, ref, ...popupProps } = props;
|
const { className, children, side, ref, ...popupProps } = props;
|
||||||
const slots = drawer({ side });
|
const slots = drawer({ side });
|
||||||
@@ -42,12 +44,8 @@ export function DrawerPopup(props: DrawerPopupProps) {
|
|||||||
return (
|
return (
|
||||||
<BaseDrawer.Portal>
|
<BaseDrawer.Portal>
|
||||||
<BaseDrawer.Backdrop className={slots.backdrop()} />
|
<BaseDrawer.Backdrop className={slots.backdrop()} />
|
||||||
<BaseDrawer.Viewport className={slots.viewport()}>
|
<BaseDrawer.Viewport ref={ref} className={slots.viewport()}>
|
||||||
<BaseDrawer.Popup
|
<BaseDrawer.Popup className={slots.popup({ className })} {...popupProps}>
|
||||||
ref={ref}
|
|
||||||
className={slots.popup({ className })}
|
|
||||||
{...popupProps}
|
|
||||||
>
|
|
||||||
<BaseDrawer.Content className={slots.content()}>
|
<BaseDrawer.Content className={slots.content()}>
|
||||||
{children}
|
{children}
|
||||||
</BaseDrawer.Content>
|
</BaseDrawer.Content>
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ export type SelectPopupProps
|
|||||||
= & Omit<ComponentProps<typeof BaseSelect.Popup>, "className">
|
= & Omit<ComponentProps<typeof BaseSelect.Popup>, "className">
|
||||||
& {
|
& {
|
||||||
className?: string;
|
className?: string;
|
||||||
// Mount inside a modal/drawer so the menu stacks above that layer
|
// Mount inside a modal/drawer stacking context (body portal uses z-3,
|
||||||
// (body portal uses z-3, below drawers at z-5).
|
// below drawers at z-5). Use a non-transformed ancestor — a CSS
|
||||||
|
// transform on the container re-roots `position: fixed` and breaks
|
||||||
|
// Base UI's modal inert cutout over the trigger.
|
||||||
container?: ComponentProps<typeof BaseSelect.Portal>["container"];
|
container?: ComponentProps<typeof BaseSelect.Portal>["container"];
|
||||||
// Positioner placement passthrough.
|
// Positioner placement passthrough.
|
||||||
side?: ComponentProps<typeof BaseSelect.Positioner>["side"];
|
side?: ComponentProps<typeof BaseSelect.Positioner>["side"];
|
||||||
|
|||||||
Reference in New Issue
Block a user