chore(ui): raise overlay primitives z-index for legacy coexistence (#33185)

This commit is contained in:
yyh 2026-03-10 13:46:38 +08:00 committed by GitHub
parent 3835cfe87e
commit 45a8967b8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 59 additions and 21 deletions

View File

@ -6,13 +6,6 @@ import * as React from 'react'
import Button from '@/app/components/base/button'
import { cn } from '@/utils/classnames'
// z-index strategy (relies on root `isolation: isolate` in layout.tsx):
// All overlay primitives (Tooltip / Popover / Dropdown / Select / Dialog / AlertDialog) — z-50
// Overlays share the same z-index; DOM order handles stacking when multiple are open.
// This ensures overlays inside an AlertDialog (e.g. a Tooltip on a dialog button) render
// above the dialog backdrop instead of being clipped by it.
// Toast — z-[99], always on top (defined in toast component)
export const AlertDialog = BaseAlertDialog.Root
export const AlertDialogTrigger = BaseAlertDialog.Trigger
export const AlertDialogTitle = BaseAlertDialog.Title
@ -39,7 +32,7 @@ export function AlertDialogContent({
<BaseAlertDialog.Backdrop
{...backdropProps}
className={cn(
'fixed inset-0 z-50 bg-background-overlay',
'fixed inset-0 z-[1002] bg-background-overlay',
'transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
overlayClassName,
)}
@ -47,7 +40,7 @@ export function AlertDialogContent({
<BaseAlertDialog.Popup
{...popupProps}
className={cn(
'fixed left-1/2 top-1/2 z-50 max-h-[calc(100vh-2rem)] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
'fixed left-1/2 top-1/2 z-[1002] max-h-[calc(100vh-2rem)] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
'transition-[transform,scale,opacity] duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
className,
)}

View File

@ -74,7 +74,7 @@ function renderContextMenuPopup({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-none', className)}
className={cn('z-[1002] outline-none', className)}
{...positionerProps}
>
<BaseContextMenu.Popup

View File

@ -1,11 +1,14 @@
'use client'
// z-index strategy (relies on root `isolation: isolate` in layout.tsx):
// All overlay primitives (Tooltip / Popover / Dropdown / Select / Dialog) — z-50
// z-index strategy (relies on root `isolation: isolate` in layout.tsx):
// All base/ui/* overlay primitives — z-[1002]
// Overlays share the same z-index; DOM order handles stacking when multiple are open.
// This ensures overlays inside a Dialog (e.g. a Tooltip on a dialog button) render
// above the dialog backdrop instead of being clipped by it.
// Toast — z-[99], always on top (defined in toast component)
// During migration, z-[1002] is chosen to sit above all legacy overlays
// (Modal z-[60], PortalToFollowElem callers up to z-[1001]).
// Once all legacy overlays are migrated, this can be reduced back to z-50.
// Toast — z-[9999], always on top (defined in toast component)
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import * as React from 'react'
@ -54,14 +57,14 @@ export function DialogContent({
<DialogPortal>
<BaseDialog.Backdrop
className={cn(
'fixed inset-0 z-50 bg-background-overlay',
'fixed inset-0 z-[1002] bg-background-overlay',
'transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
overlayClassName,
)}
/>
<BaseDialog.Popup
className={cn(
'fixed left-1/2 top-1/2 z-50 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'fixed left-1/2 top-1/2 z-[1002] max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
className,
)}

View File

@ -131,7 +131,7 @@ function renderDropdownMenuPopup({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-none', className)}
className={cn('z-[1002] outline-none', className)}
{...positionerProps}
>
<Menu.Popup

View File

@ -4,4 +4,4 @@ export const menuGroupLabelClassName = 'px-3 pb-0.5 pt-1 text-text-tertiary syst
export const menuSeparatorClassName = 'my-1 h-px bg-divider-subtle'
export const menuPopupBaseClassName = 'max-h-[var(--available-height)] overflow-y-auto overflow-x-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur py-1 text-sm text-text-secondary shadow-lg outline-none focus:outline-none focus-visible:outline-none backdrop-blur-[5px]'
export const menuPopupAnimationClassName = 'origin-[var(--transform-origin)] transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none'
export const menuBackdropClassName = 'fixed inset-0 z-50 bg-transparent transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none'
export const menuBackdropClassName = 'fixed inset-0 z-[1002] bg-transparent transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none'

View File

@ -48,7 +48,7 @@ export function PopoverContent({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-none', className)}
className={cn('z-[1002] outline-none', className)}
{...positionerProps}
>
<BasePopover.Popup

View File

@ -115,7 +115,7 @@ export function SelectContent({
sideOffset={sideOffset}
alignOffset={alignOffset}
alignItemWithTrigger={false}
className={cn('z-50 outline-none', className)}
className={cn('z-[1002] outline-none', className)}
{...positionerProps}
>
<BaseSelect.Popup

View File

@ -37,7 +37,7 @@ export function TooltipContent({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-none', className)}
className={cn('z-[1002] outline-none', className)}
>
<BaseTooltip.Popup
className={cn(

View File

@ -45,8 +45,9 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
</div>
</DialogContent>
{/* TODO: reduce z-[1002] to match base/ui primitives after legacy overlay migration completes */}
<DialogPortal>
<div className="pointer-events-none fixed left-1/2 top-1/2 z-50 flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary body-xs-regular">
<div className="pointer-events-none fixed left-1/2 top-1/2 z-[1002] flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary body-xs-regular">
<span>{t('onboarding.escTip.press', { ns: 'workflow' })}</span>
<ShortcutsName keys={[t('onboarding.escTip.key', { ns: 'workflow' })]} textColor="secondary" />
<span>{t('onboarding.escTip.toDismiss', { ns: 'workflow' })}</span>

View File

@ -53,6 +53,47 @@ pnpm -C web lint:fix --prune-suppressions <changed-files>
- If a migrated file was in the allowlist, remove it from `web/eslint.constants.mjs` in the same PR.
- Never increase allowlist scope to bypass new code.
## z-index strategy
All new overlay primitives in `base/ui/` share a single z-index value: **`z-[1002]`**.
### Why z-[1002]?
During the migration period, legacy and new overlays coexist. Legacy overlays
portal to `document.body` with explicit z-index values:
| Layer | z-index | Components |
|-------|---------|------------|
| Legacy Drawer | `z-[30]` | `base/drawer` |
| Legacy Modal | `z-[60]` | `base/modal` (default) |
| Legacy PortalToFollowElem callers | up to `z-[1001]` | various business components |
| **New UI primitives** | **`z-[1002]`** | `base/ui/*` (Popover, Dialog, Tooltip, etc.) |
| Legacy Modal (highPriority) | `z-[1100]` | `base/modal` (`highPriority={true}`) |
| Toast | `z-[9999]` | `base/toast` |
`z-[1002]` sits above all common legacy overlays, so new primitives always
render on top without needing per-call-site z-index hacks. Among themselves,
new primitives share the same z-index and rely on **DOM order** for stacking
(later portal = on top).
### Rules
- **Do NOT add z-index overrides** (e.g. `className="z-[1003]"`) on new
`base/ui/*` components. If you find yourself needing one, the parent legacy
overlay should be migrated instead.
- When migrating a legacy overlay that has a high z-index, remove the z-index
entirely — the new primitive's default `z-[1002]` handles it.
- `portalToFollowElemContentClassName` with z-index values (e.g. `z-[1000]`)
should be deleted when the surrounding legacy container is migrated.
### Post-migration cleanup
Once all legacy overlays are removed:
1. Reduce `z-[1002]` back to `z-50` across all `base/ui/` primitives.
1. Reduce Toast from `z-[9999]` to `z-[99]`.
1. Remove this section from the migration guide.
## React Refresh policy for base UI primitives
- We keep primitive aliases (for example `DropdownMenu = Menu.Root`) in the same module.