Skip to content

Commit 0d580fd

Browse files
authored
fix(dify-ui): let portal order stack overlays so nested modals block clicks (#44214)
1 parent 734a9ea commit 0d580fd

22 files changed

Lines changed: 31 additions & 74 deletions

File tree

‎.agents/skills/frontend-code-review/references/code-quality.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Flag:
3838
- `!` important modifiers or important CSS overrides without a narrow, documented reason.
3939
- Manual class-list assembly through string concatenation, template strings, array `.join(' ')`, or a custom conditional combiner instead of `cn(...)`. Conditional values passed to `cn(...)` remain valid.
4040
- Incoming `className` placed before default classes in `cn(...)`, preventing call-site overrides.
41-
- Arbitrary z-index or one-off layering fixes on overlays.
41+
- Z-index, forced backdrops, or other one-off layering fixes on overlays and body-level portals.
4242

4343
Use:
4444

‎packages/dify-ui/docs/overlays.md‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,17 @@ clipping.
4444

4545
## Layering
4646

47-
| Layer | z-index |
48-
| ----------------------------------------------------------------------- | ------- |
49-
| Dialogs, pickers, drawers, menus, popovers, preview cards, and tooltips | `z-50` |
50-
| Toast viewport | `z-60` |
51-
52-
Overlays at `z-50` rely on portal DOM order; the portal mounted later appears above earlier ones.
53-
Toast remains one layer above ordinary overlays.
47+
Overlays set no z-index; portal DOM order stacks them. A Base UI modal blocks outside clicks with
48+
an unstyled sibling of its popup, so any overlay z-index can lift a lower layer above that guard.
49+
Only a body-level layer that must stay above overlays mounted after it sets one. A portal into an
50+
application container sits inside the isolated root and layers against its siblings there.
5451

5552
Positioners hide with their anchor: Base UI marks them `data-anchor-hidden` when the anchor is
5653
clipped, `display: none` or unmounted, and the shared positioner class turns that into
5754
`visibility: hidden` plus `pointer-events: none` so a popup never lingers at the viewport origin.
5855

59-
- Do not add call-site `z-*` overrides. Fix the owning overlay structure when content is clipped
60-
or hidden.
56+
- Do not add call-site `z-*` overrides or force a backdrop to block clicks. Fix the owning overlay
57+
structure when content is clipped, hidden, or clickable behind a modal.
6158
- Do not wrap a Dify UI overlay in another manual portal.
6259
- Add shared backdrop or presentation chrome inside the owning exported component, not at call
6360
sites.

‎packages/dify-ui/src/alert-dialog/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function AlertDialogContent({
6464
<BaseAlertDialog.Popup
6565
className={(state) =>
6666
cn(
67-
'fixed top-1/2 left-1/2 z-50 max-h-[calc(100vh-2rem)] w-120 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',
67+
'fixed top-1/2 left-1/2 max-h-[calc(100vh-2rem)] w-120 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',
6868
modalPopupAnimationClassName,
6969
resolveClassName(className, state),
7070
)

‎packages/dify-ui/src/dialog/index.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type DialogViewportProps = BaseDialog.Viewport.Props
5151
function DialogViewport({ className, ...props }: DialogViewportProps) {
5252
return (
5353
<BaseDialog.Viewport
54-
className={(state) => cn('fixed inset-0 z-50', resolveClassName(className, state))}
54+
className={(state) => cn('fixed inset-0', resolveClassName(className, state))}
5555
{...props}
5656
/>
5757
)
@@ -64,7 +64,7 @@ function DialogPopup({ className, ...props }: DialogPopupProps) {
6464
<BaseDialog.Popup
6565
className={(state) =>
6666
cn(
67-
'z-50 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl',
67+
'rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl',
6868
modalPopupAnimationClassName,
6969
resolveClassName(className, state),
7070
)

‎packages/dify-ui/src/drawer/index.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function DrawerBackdrop({ className, ...props }: DrawerBackdropProps) {
5151
<BaseDrawer.Backdrop
5252
className={(state) =>
5353
cn(
54-
'absolute inset-0 z-50 bg-background-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))]',
54+
'absolute inset-0 bg-background-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))]',
5555
'transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none',
5656
resolveClassName(className, state),
5757
)
@@ -68,7 +68,7 @@ function DrawerViewport({ className, ...props }: DrawerViewportProps) {
6868
<BaseDrawer.Viewport
6969
className={(state) =>
7070
cn(
71-
'fixed inset-0 z-50 touch-none overflow-hidden overscroll-contain outline-hidden',
71+
'fixed inset-0 touch-none overflow-hidden overscroll-contain outline-hidden',
7272
resolveClassName(className, state),
7373
)
7474
}
@@ -84,7 +84,7 @@ function DrawerPopup({ className, ...props }: DrawerPopupProps) {
8484
<BaseDrawer.Popup
8585
className={(state) =>
8686
cn(
87-
'fixed z-50 flex min-h-0 touch-none flex-col overflow-hidden border-[0.5px] border-components-panel-border bg-components-panel-bg text-text-primary shadow-xl outline-hidden',
87+
'fixed flex min-h-0 touch-none flex-col overflow-hidden border-[0.5px] border-components-panel-border bg-components-panel-bg text-text-primary shadow-xl outline-hidden',
8888
'transition-[transform,opacity,box-shadow] duration-200 data-swiping:duration-0 data-swiping:select-none motion-reduce:transition-none',
8989
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:h-dvh data-[swipe-direction=right]:w-120 data-[swipe-direction=right]:max-w-[calc(100vw-2rem)] data-[swipe-direction=right]:transform-[translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=right]:rounded-l-2xl data-[swipe-direction=right]:border-r-0',
9090
'data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))] data-starting-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))]',

‎packages/dify-ui/src/internal/overlay.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export const floatingPopupAnimationClassName =
1010
// Positioners hide with their anchor: Base UI only emits `data-anchor-hidden` when the anchor is
1111
// clipped, display:none or unmounted, and would otherwise keep the popup at the viewport origin.
1212
export const positionerClassName =
13-
'z-50 outline-hidden data-anchor-hidden:invisible data-anchor-hidden:pointer-events-none'
13+
'outline-hidden data-anchor-hidden:invisible data-anchor-hidden:pointer-events-none'
1414
export const hintPopupClassName =
1515
'max-w-75 rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 text-start system-xs-medium wrap-break-word text-text-secondary shadow-[0px_12px_16px_-4px_var(--color-shadow-shadow-5),0px_4px_6px_-2px_var(--color-shadow-shadow-1)] backdrop-blur-[5px] origin-(--transform-origin) transition-opacity data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0 motion-reduce:transition-none'
1616
export const modalBackdropClassName =
17-
'absolute inset-0 z-50 bg-background-overlay transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none'
17+
'absolute inset-0 bg-background-overlay transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none'
1818
export const modalPopupAnimationClassName =
1919
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none'

‎packages/dify-ui/src/popover/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type PopoverBackdropProps = BasePopover.Backdrop.Props
4747
function PopoverBackdrop({ className, ...props }: PopoverBackdropProps) {
4848
return (
4949
<BasePopover.Backdrop
50-
className={(state) => cn('fixed inset-0 z-50', resolveClassName(className, state))}
50+
className={(state) => cn('fixed inset-0', resolveClassName(className, state))}
5151
{...props}
5252
/>
5353
)

‎packages/dify-ui/src/toast/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ function Toaster({
240240
<BaseToast.Provider toastManager={toastManager} timeout={timeout} limit={limit}>
241241
<BaseToast.Portal>
242242
<BaseToast.Viewport
243-
className="pointer-events-none fixed top-4 right-4 z-60 w-90 max-w-[calc(100vw-2rem)] overflow-visible sm:right-8"
243+
className="pointer-events-none fixed top-4 right-4 z-50 w-90 max-w-[calc(100vw-2rem)] overflow-visible sm:right-8"
244244
style={offset}
245245
>
246246
{children}

‎web/app/components/base/agent-log-modal/__tests__/index.spec.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('AgentLogModal', () => {
156156
const modal = screen.getByRole('dialog')
157157
expect(container).not.toContainElement(modal)
158158
expect(document.body).toContainElement(modal)
159-
expect(modal).toHaveClass('fixed', 'z-50', 'w-120!', 'left-[max(8px,calc(100vw-1136px))]!')
159+
expect(modal).toHaveClass('fixed', 'w-120!', 'left-[max(8px,calc(100vw-1136px))]!')
160160
})
161161

162162
it('should call onCancel when close button is clicked', () => {

‎web/app/components/base/chat/chat-with-history/inputs-form/__tests__/content.spec.tsx‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -333,27 +333,6 @@ describe('InputsFormContent', () => {
333333
)
334334
})
335335

336-
it('renders select dropdown on the shared dify-ui overlay layer', async () => {
337-
const user = userEvent.setup()
338-
const context = createMockContext({
339-
inputsForms: [
340-
{
341-
variable: 'sel',
342-
type: InputVarType.select,
343-
label: 'Sel',
344-
options: ['A', 'B'],
345-
default: 'B',
346-
},
347-
],
348-
currentConversationInputs: {},
349-
})
350-
351-
renderWithContext(<InputsFormContent />, context)
352-
await user.click(screen.getByText('B'))
353-
354-
expect(screen.getByText('A').closest('.z-50')).not.toBeNull()
355-
})
356-
357336
it('handles select input with existing value (value not in options -> shows placeholder)', () => {
358337
const context = createMockContext({
359338
inputsForms: [

0 commit comments

Comments
 (0)