Fix mobile locale select under drawer

Select menus portal at z-3 while drawers sit at z-5, so
the popup opened behind the drawer. Portal into the drawer
panel instead. Point .env.example VITE_API_URL at the slug
subdomain with a local Vite note.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-22 12:32:01 +02:00
parent 52310dbf65
commit a9a476a343
5 changed files with 29 additions and 11 deletions

View File

@@ -1,7 +1,7 @@
# GraphQL API origin. Default matches other apps (probod on :8080). For local # GraphQL API origin (compliance-portal HTTPS origin; <slug> is the portal
# Vite development, set this to http://localhost:5174 so the browser hits the # slug). For local Vite development, set this to http://localhost:5174 so the
# Vite origin and /graphql is proxied (see vite.config.ts). # browser hits the Vite origin and /graphql is proxied (see vite.config.ts).
VITE_API_URL=http://localhost:8080 VITE_API_URL=https://<slug>.probopage.localhost
# Vite /graphql proxy target (compliance-portal HTTPS origin; <slug> is the # Vite /graphql proxy target (compliance-portal HTTPS origin; <slug> is the
# portal slug). Required for local Vite development — uncomment and set the # portal slug). Required for local Vite development — uncomment and set the

View File

@@ -23,6 +23,7 @@ import { Select } from "@probo/ui/src/v2/Select/Select";
import { SelectItem } from "@probo/ui/src/v2/Select/SelectItem"; import { SelectItem } from "@probo/ui/src/v2/Select/SelectItem";
import { SelectPopup } from "@probo/ui/src/v2/Select/SelectPopup"; import { SelectPopup } from "@probo/ui/src/v2/Select/SelectPopup";
import { SelectTrigger } from "@probo/ui/src/v2/Select/SelectTrigger"; import { SelectTrigger } from "@probo/ui/src/v2/Select/SelectTrigger";
import type { RefObject } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -38,6 +39,9 @@ interface LocaleSelectProps {
persist?: boolean; persist?: boolean;
// 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
// is not painted under the modal layer (Select defaults to body + z-3).
portalContainer?: RefObject<HTMLElement | null>;
} }
// Compact locale control for the top bar (guest and mobile). Uses the v2 Select // Compact locale control for the top bar (guest and mobile). Uses the v2 Select
@@ -45,6 +49,7 @@ interface LocaleSelectProps {
export function LocaleSelect({ export function LocaleSelect({
persist = false, persist = false,
onLocaleChange, onLocaleChange,
portalContainer,
}: LocaleSelectProps) { }: LocaleSelectProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const locale = useLocale(); const locale = useLocale();
@@ -75,7 +80,7 @@ export function LocaleSelect({
</span> </span>
)} )}
</SelectTrigger> </SelectTrigger>
<SelectPopup> <SelectPopup container={portalContainer}>
{URL_LOCALES.map(code => ( {URL_LOCALES.map(code => (
<SelectItem key={code} value={code}> <SelectItem key={code} value={code}>
{URL_LOCALE_LABELS[code]} {URL_LOCALE_LABELS[code]}

View File

@@ -38,7 +38,7 @@ import { DrawerTitle } from "@probo/ui/src/v2/Drawer/DrawerTitle";
import { DrawerTrigger } from "@probo/ui/src/v2/Drawer/DrawerTrigger"; import { DrawerTrigger } from "@probo/ui/src/v2/Drawer/DrawerTrigger";
import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton"; import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton";
import { Text } from "@probo/ui/src/v2/typography/Text"; import { Text } from "@probo/ui/src/v2/typography/Text";
import { useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
@@ -76,6 +76,9 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
const { openSubscribe, isSubscribed, unsubscribe, isUnsubscribing } = useSubscribeDialog(); const { openSubscribe, isSubscribed, unsubscribe, isUnsubscribing } = useSubscribeDialog();
const [signOut, isSigningOut] = useSignOut(); const [signOut, isSigningOut] = useSignOut();
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
// popup so they stack inside the modal layer (z-5) instead of under it.
const drawerPopupRef = useRef<HTMLDivElement>(null);
const identity = useFragment(topBarMobileNavFragment, identityKey); const identity = useFragment(topBarMobileNavFragment, identityKey);
const localizedPath = useLocalizedPath(); const localizedPath = useLocalizedPath();
@@ -103,7 +106,7 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
)} )}
/> />
</div> </div>
<DrawerPopup side="right"> <DrawerPopup side="right" ref={drawerPopupRef}>
<DrawerHeader> <DrawerHeader>
<DrawerTitle>{t("topBar.menuTitle")}</DrawerTitle> <DrawerTitle>{t("topBar.menuTitle")}</DrawerTitle>
<DrawerClose <DrawerClose
@@ -147,6 +150,7 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
<LocaleSelect <LocaleSelect
persist={identity != null} persist={identity != null}
onLocaleChange={close} onLocaleChange={close}
portalContainer={drawerPopupRef}
/> />
</div> </div>
{identity == null {identity == null

View File

@@ -33,15 +33,21 @@ 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
// into the drawer stacking context.
export function DrawerPopup(props: DrawerPopupProps) { export function DrawerPopup(props: DrawerPopupProps) {
const { className, children, side, ...popupProps } = props; const { className, children, side, ref, ...popupProps } = props;
const slots = drawer({ side }); const slots = drawer({ side });
return ( return (
<BaseDrawer.Portal> <BaseDrawer.Portal>
<BaseDrawer.Backdrop className={slots.backdrop()} /> <BaseDrawer.Backdrop className={slots.backdrop()} />
<BaseDrawer.Viewport className={slots.viewport()}> <BaseDrawer.Viewport className={slots.viewport()}>
<BaseDrawer.Popup className={slots.popup({ className })} {...popupProps}> <BaseDrawer.Popup
ref={ref}
className={slots.popup({ className })}
{...popupProps}
>
<BaseDrawer.Content className={slots.content()}> <BaseDrawer.Content className={slots.content()}>
{children} {children}
</BaseDrawer.Content> </BaseDrawer.Content>

View File

@@ -27,6 +27,9 @@ 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
// (body portal uses z-3, below drawers at z-5).
container?: ComponentProps<typeof BaseSelect.Portal>["container"];
// Positioner placement passthrough. // Positioner placement passthrough.
side?: ComponentProps<typeof BaseSelect.Positioner>["side"]; side?: ComponentProps<typeof BaseSelect.Positioner>["side"];
align?: ComponentProps<typeof BaseSelect.Positioner>["align"]; align?: ComponentProps<typeof BaseSelect.Positioner>["align"];
@@ -36,13 +39,13 @@ export type SelectPopupProps
// Portal + positioner + styled popup holding the select items. // Portal + positioner + styled popup holding the select items.
export function SelectPopup(props: SelectPopupProps) { export function SelectPopup(props: SelectPopupProps) {
const { const {
className, children, className, children, container,
side = "bottom", align = "start", sideOffset = 4, side = "bottom", align = "start", sideOffset = 4,
...popupProps ...popupProps
} = props; } = props;
return ( return (
<BaseSelect.Portal> <BaseSelect.Portal container={container}>
{/* z-3 on the Positioner so the portaled root wins over in-page z-1. */} {/* z-3 on the Positioner so the portaled root wins over in-page z-1. */}
<BaseSelect.Positioner className="z-3" side={side} align={align} sideOffset={sideOffset}> <BaseSelect.Positioner className="z-3" side={side} align={align} sideOffset={sideOffset}>
<BaseSelect.Popup className={selectPopup({ className })} {...popupProps}> <BaseSelect.Popup className={selectPopup({ className })} {...popupProps}>