mirror of
https://github.com/langgenius/dify.git
synced 2026-03-06 15:45:14 +00:00
fix(web): unify overlay z-index, decouple Placement type, and improve animation a11y
- Add z-50 to all overlay Positioners so overlays inside a Dialog (e.g. Tooltip on a dialog button) are not clipped by its backdrop - Replace @floating-ui/react Placement import with self-owned type definition to remove the transitive type dependency - Change Dialog popup transition-all to explicit transition-[transform,scale,opacity] to avoid animating unintended CSS properties - Add motion-reduce:transition-none to all animated overlay elements for prefers-reduced-motion compliance
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
// z-index strategy (relies on root `isolation: isolate` in layout.tsx):
|
||||
// Tooltip / Popover / Dropdown — no z-index, DOM order is sufficient
|
||||
// Dialog backdrop + popup — z-50, ensures modal covers non-modal portals
|
||||
// All overlay primitives (Tooltip / Popover / Dropdown / Select / Dialog) — z-50
|
||||
// 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)
|
||||
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
|
||||
@@ -31,14 +33,14 @@ export function DialogContent({
|
||||
<BaseDialog.Backdrop
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background-overlay',
|
||||
'transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0',
|
||||
'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 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
|
||||
'transition-all duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0',
|
||||
'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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Menu } from '@base-ui/react/menu'
|
||||
import * as React from 'react'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
@@ -64,13 +64,13 @@ function renderDropdownMenuPopup({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={cn('outline-none', className)}
|
||||
className={cn('z-50 outline-none', className)}
|
||||
{...positionerProps}
|
||||
>
|
||||
<Menu.Popup
|
||||
className={cn(
|
||||
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg py-1 text-sm text-text-secondary shadow-lg',
|
||||
'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',
|
||||
'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',
|
||||
popupClassName,
|
||||
)}
|
||||
{...popupProps}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
// Placement type for overlay positioning.
|
||||
// Mirrors the Floating UI Placement spec — a stable set of 12 CSS-based position values.
|
||||
// Reference: https://floating-ui.com/docs/useFloating#placement
|
||||
|
||||
type ParsedPlacement = {
|
||||
side: 'top' | 'bottom' | 'left' | 'right'
|
||||
align: 'start' | 'center' | 'end'
|
||||
}
|
||||
type Side = 'top' | 'bottom' | 'left' | 'right'
|
||||
type Align = 'start' | 'center' | 'end'
|
||||
|
||||
export function parsePlacement(placement: Placement): ParsedPlacement {
|
||||
const [side, align] = placement.split('-') as [
|
||||
ParsedPlacement['side'],
|
||||
ParsedPlacement['align'] | undefined,
|
||||
]
|
||||
export type Placement
|
||||
= | 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
|
||||
export function parsePlacement(placement: Placement): { side: Side, align: Align } {
|
||||
const [side, align] = placement.split('-') as [Side, Align | undefined]
|
||||
|
||||
return {
|
||||
side,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Popover as BasePopover } from '@base-ui/react/popover'
|
||||
import * as React from 'react'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
@@ -48,13 +48,13 @@ export function PopoverContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={cn('outline-none', className)}
|
||||
className={cn('z-50 outline-none', className)}
|
||||
{...positionerProps}
|
||||
>
|
||||
<BasePopover.Popup
|
||||
className={cn(
|
||||
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
|
||||
'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',
|
||||
'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',
|
||||
popupClassName,
|
||||
)}
|
||||
{...popupProps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Select as BaseSelect } from '@base-ui/react/select'
|
||||
import * as React from 'react'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
@@ -77,13 +77,13 @@ export function SelectContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={cn('outline-none', className)}
|
||||
className={cn('z-50 outline-none', className)}
|
||||
{...positionerProps}
|
||||
>
|
||||
<BaseSelect.Popup
|
||||
className={cn(
|
||||
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
|
||||
'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',
|
||||
'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',
|
||||
popupClassName,
|
||||
)}
|
||||
{...popupProps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
|
||||
import * as React from 'react'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
@@ -37,7 +37,7 @@ export function TooltipContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={cn('outline-none', className)}
|
||||
className={cn('z-50 outline-none', className)}
|
||||
>
|
||||
<BaseTooltip.Popup
|
||||
className={cn(
|
||||
|
||||
Reference in New Issue
Block a user