mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
fix: resource drawer opening behind dialog in chat mode (#8328)
* fix: resource drawer opening behind dialog in chat mode Integrate Modal into the Disposable z-index stacking system so drawers opened from within a modal (e.g. "Add a new resource") correctly appear above the dialog instead of behind it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resource drawer opening behind dialog in chat mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: simplify minZIndex tracking by removing unnecessary refcount Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use map-based minZIndex tracking and conditional chat elevation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use plain object instead of Map for reactive minZIndex tracking $state(new Map()) is not deeply reactive in Svelte 5 — only plain objects and arrays are proxied. Replaced with Record<string, number> so that property assignments properly trigger $derived updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
<script lang="ts" module>
|
||||
export let openedDrawers: { val: string[] } = $state({ val: [] })
|
||||
|
||||
// When a disposable with minZIndex is open, all disposables use that as
|
||||
// their z-index base so that overlays opened on top (e.g. a Drawer from
|
||||
// inside a Modal) stack correctly above it.
|
||||
// We track per-id entries so concurrent modals don't clobber each other
|
||||
// (closing one must not reset the base while another is still open).
|
||||
let minZIndexEntries: Record<string, number> = $state({})
|
||||
let activeMinZIndex = $derived.by(() => {
|
||||
const values = Object.values(minZIndexEntries)
|
||||
return values.length > 0 ? Math.max(...values) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -11,6 +22,11 @@
|
||||
id?: any
|
||||
preventEscape?: boolean
|
||||
initialOffset?: number
|
||||
/** Minimum z-index base for this overlay. While any disposable with a
|
||||
* minZIndex is open, all disposables use that as their base so that
|
||||
* subsequent overlays stack above it (e.g. zIndexes.aiChat + 1 for
|
||||
* modals that need to render above the AI chat panel). */
|
||||
minZIndex?: number
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
@@ -21,13 +37,17 @@
|
||||
id = (Math.random() + 1).toString(36).substring(10),
|
||||
preventEscape = false,
|
||||
initialOffset = 0,
|
||||
minZIndex = 0,
|
||||
children,
|
||||
onOpen,
|
||||
onClose
|
||||
}: Props = $props()
|
||||
|
||||
let offset = $state(untrack(() => initialOffset))
|
||||
let zIndex = $derived(zIndexes.disposables + offset)
|
||||
// Note: when a Modal with minZIndex is open, all disposables (including
|
||||
// already-open Drawers) are elevated. This is acceptable — relative
|
||||
// stacking order is preserved by the per-instance offset.
|
||||
let zIndex = $derived(Math.max(zIndexes.disposables, activeMinZIndex) + offset)
|
||||
|
||||
export function toggleDrawer() {
|
||||
if (!open) {
|
||||
@@ -44,6 +64,9 @@
|
||||
}
|
||||
openedDrawers.val.push(id)
|
||||
offset = initialOffset + openedDrawers.val.length
|
||||
if (minZIndex > 0) {
|
||||
minZIndexEntries[id] = minZIndex
|
||||
}
|
||||
}
|
||||
|
||||
export function closeDrawer() {
|
||||
@@ -51,6 +74,9 @@
|
||||
offset = initialOffset
|
||||
if (openedDrawers.val.includes(id)) {
|
||||
openedDrawers.val = openedDrawers.val.filter((drawer) => drawer !== id)
|
||||
if (minZIndex > 0) {
|
||||
delete minZIndexEntries[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +115,9 @@
|
||||
if (open) {
|
||||
openedDrawers.val.push(untrack(() => id))
|
||||
offset = untrack(() => initialOffset) + openedDrawers.val.length
|
||||
if (minZIndex > 0) {
|
||||
minZIndexEntries[untrack(() => id)] = minZIndex
|
||||
}
|
||||
}
|
||||
|
||||
let wasEverOpen = false
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
import { createBubbler, stopPropagation } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Button from '../button/Button.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import CloseButton from '../CloseButton.svelte'
|
||||
import Disposable from '../drawer/Disposable.svelte'
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
import { chatState } from '$lib/components/copilot/chat/sharedChatState.svelte'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
@@ -28,12 +31,28 @@
|
||||
cancelText = undefined,
|
||||
kind = 'button',
|
||||
settings,
|
||||
children,
|
||||
children: children_render,
|
||||
actions
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let disposable: Disposable | undefined = $state(undefined)
|
||||
|
||||
// Only elevate above the AI chat panel when it's actually open —
|
||||
// when chat is closed there's nothing at z-index 1200 to stack above.
|
||||
const minZIndex = $derived(chatState.size > 0 ? zIndexes.aiChat + 1 : 0)
|
||||
|
||||
// Both `bind:open` and this $effect are needed: bind:open syncs the
|
||||
// boolean, while the effect calls openDrawer/closeDrawer to register
|
||||
// the disposable in the stacking system (same pattern as Drawer.svelte).
|
||||
$effect(() => {
|
||||
open
|
||||
untrack(() => {
|
||||
open ? disposable?.openDrawer() : disposable?.closeDrawer()
|
||||
})
|
||||
})
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (open) {
|
||||
switch (event.key) {
|
||||
@@ -58,70 +77,80 @@
|
||||
|
||||
<svelte:window onkeydowncapture={onKeyDown} />
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
onclick={() => (open = false)}
|
||||
transition:fadeFast|local
|
||||
class={'fixed top-0 bottom-0 left-0 right-0 z-[9999]'}
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
|
||||
open ? 'ease-out duration-300 opacity-100' : 'ease-in duration-200 opacity-0'
|
||||
)}
|
||||
></div>
|
||||
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<Disposable bind:open bind:this={disposable} preventEscape {minZIndex}>
|
||||
{#snippet children({ zIndex })}
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
onclick={() => (open = false)}
|
||||
transition:fadeFast|local
|
||||
class="fixed top-0 bottom-0 left-0 right-0"
|
||||
style="z-index: {zIndex}"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
onclick={stopPropagation(bubble('click'))}
|
||||
class={twMerge(
|
||||
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
|
||||
c,
|
||||
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
|
||||
open
|
||||
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
|
||||
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
? 'ease-out duration-300 opacity-100'
|
||||
: 'ease-in duration-200 opacity-0'
|
||||
)}
|
||||
{style}
|
||||
>
|
||||
{#if kind == 'X'}
|
||||
<div class="absolute top-4 right-4"><CloseButton on:close={() => (open = false)} /></div
|
||||
>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<div class="text-left flex-1">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
|
||||
{@render settings?.()}
|
||||
</div>
|
||||
></div>
|
||||
|
||||
<div class="mt-4 text-sm text-primary">
|
||||
{@render children?.()}
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
onclick={stopPropagation(bubble('click'))}
|
||||
class={twMerge(
|
||||
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
|
||||
c,
|
||||
open
|
||||
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
|
||||
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
)}
|
||||
{style}
|
||||
>
|
||||
{#if kind == 'X'}
|
||||
<div class="absolute top-4 right-4"
|
||||
><CloseButton on:close={() => (open = false)} /></div
|
||||
>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<div class="text-left flex-1">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
|
||||
{@render settings?.()}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-primary">
|
||||
{@render children_render?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if kind == 'button'}
|
||||
<div
|
||||
class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4"
|
||||
>
|
||||
{@render actions?.()}
|
||||
<Button
|
||||
on:click={() => {
|
||||
dispatch('canceled')
|
||||
open = false
|
||||
}}
|
||||
color="light"
|
||||
size="sm"
|
||||
>
|
||||
{cancelText ?? 'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if kind == 'button'}
|
||||
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
|
||||
{@render actions?.()}
|
||||
<Button
|
||||
on:click={() => {
|
||||
dispatch('canceled')
|
||||
open = false
|
||||
}}
|
||||
color="light"
|
||||
size="sm"
|
||||
>
|
||||
{cancelText ?? 'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Disposable>
|
||||
|
||||
Reference in New Issue
Block a user