fix dialog z-indexing issue

This commit is contained in:
pa
2026-01-15 17:13:09 +09:00
committed by Natsumi
parent 5ba081a9db
commit 75980b7062
13 changed files with 200 additions and 11 deletions

View File

@@ -19,6 +19,7 @@
<VRCXUpdateDialog></VRCXUpdateDialog>
</div>
<div id="x-dialog-portal" class="x-dialog-portal"></div>
</el-config-provider>
</TooltipProvider>
</template>

View File

@@ -1,6 +1,8 @@
<script setup>
import { AlertDialogRoot, useForwardPropsEmits } from 'reka-ui';
import AlertDialogStateProvider from './AlertDialogStateProvider.vue';
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false }
@@ -12,6 +14,8 @@
<template>
<AlertDialogRoot v-slot="slotProps" data-slot="alert-dialog" v-bind="forwarded">
<slot v-bind="slotProps" />
<AlertDialogStateProvider :open="slotProps.open">
<slot v-bind="slotProps" />
</AlertDialogStateProvider>
</AlertDialogRoot>
</template>

View File

@@ -1,8 +1,12 @@
<script setup>
import { AlertDialogContent, AlertDialogOverlay, AlertDialogPortal, useForwardPropsEmits } from 'reka-ui';
import { inject, onBeforeUnmount, ref, watch } from 'vue';
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { ALERT_DIALOG_OPEN_INJECTION_KEY } from './context';
defineOptions({
inheritAttrs: false
});
@@ -26,10 +30,30 @@
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const injectedOpen = inject(ALERT_DIALOG_OPEN_INJECTION_KEY, null);
const open = injectedOpen ?? ref(true);
const portalLayer = acquireModalPortalLayer();
const portalTo = portalLayer.element;
watch(
open,
(isOpen) => {
if (isOpen) {
portalLayer.bringToFront();
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
portalLayer.release();
});
</script>
<template>
<AlertDialogPortal>
<AlertDialogPortal :to="portalTo">
<AlertDialogOverlay
data-slot="alert-dialog-overlay"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80" />

View File

@@ -0,0 +1,15 @@
<script setup>
import { provide, toRef } from 'vue';
import { ALERT_DIALOG_OPEN_INJECTION_KEY } from './context';
const props = defineProps({
open: { type: Boolean, required: true }
});
provide(ALERT_DIALOG_OPEN_INJECTION_KEY, toRef(props, 'open'));
</script>
<template>
<slot />
</template>

View File

@@ -0,0 +1 @@
export const ALERT_DIALOG_OPEN_INJECTION_KEY = Symbol('vrcx-alert-dialog-open');

View File

@@ -1,6 +1,8 @@
<script setup>
import { DialogRoot, useForwardPropsEmits } from 'reka-ui';
import DialogStateProvider from './DialogStateProvider.vue';
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
@@ -13,6 +15,8 @@
<template>
<DialogRoot v-slot="slotProps" data-slot="dialog" v-bind="forwarded">
<slot v-bind="slotProps" />
<DialogStateProvider :open="slotProps.open">
<slot v-bind="slotProps" />
</DialogStateProvider>
</DialogRoot>
</template>

View File

@@ -1,9 +1,13 @@
<script setup>
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui';
import { inject, onBeforeUnmount, ref, watch } from 'vue';
import { X } from 'lucide-vue-next';
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { DIALOG_OPEN_INJECTION_KEY } from './context';
import DialogOverlay from './DialogOverlay.vue';
defineOptions({
@@ -30,10 +34,30 @@
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const injectedOpen = inject(DIALOG_OPEN_INJECTION_KEY, null);
const open = injectedOpen ?? ref(true);
const portalLayer = acquireModalPortalLayer();
const portalTo = portalLayer.element;
watch(
open,
(isOpen) => {
if (isOpen) {
portalLayer.bringToFront();
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
portalLayer.release();
});
</script>
<template>
<DialogPortal>
<DialogPortal :to="portalTo">
<DialogOverlay />
<DialogContent
data-slot="dialog-content"

View File

@@ -1,9 +1,13 @@
<script setup>
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui';
import { inject, onBeforeUnmount, ref, watch } from 'vue';
import { X } from 'lucide-vue-next';
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { DIALOG_OPEN_INJECTION_KEY } from './context';
defineOptions({
inheritAttrs: false
});
@@ -27,10 +31,30 @@
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const injectedOpen = inject(DIALOG_OPEN_INJECTION_KEY, null);
const open = injectedOpen ?? ref(true);
const portalLayer = acquireModalPortalLayer();
const portalTo = portalLayer.element;
watch(
open,
(isOpen) => {
if (isOpen) {
portalLayer.bringToFront();
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
portalLayer.release();
});
</script>
<template>
<DialogPortal>
<DialogPortal :to="portalTo">
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0">
<DialogContent

View File

@@ -0,0 +1,15 @@
<script setup>
import { provide, toRef } from 'vue';
import { DIALOG_OPEN_INJECTION_KEY } from './context';
const props = defineProps({
open: { type: Boolean, required: true }
});
provide(DIALOG_OPEN_INJECTION_KEY, toRef(props, 'open'));
</script>
<template>
<slot />
</template>

View File

@@ -29,7 +29,7 @@
promptErrorMessage
} = storeToRefs(modalStore);
const { handleSubmit, resetForm, setValues, values } = useForm({
const { handleSubmit, resetForm, values } = useForm({
initialValues: {
value: ''
}
@@ -88,7 +88,11 @@
[promptOpen, promptInputValue],
([open, inputValue]) => {
if (open) {
setValues({ value: inputValue ?? '' });
resetForm({
values: {
value: inputValue ?? ''
}
});
return;
}
@@ -110,12 +114,19 @@
@pointerDownOutside="onPointerDownOutside"
@interactOutside="onInteractOutside">
<form @submit="onSubmit">
<DialogHeader>
<DialogHeader class="mb-5">
<DialogTitle>{{ promptTitle }}</DialogTitle>
<DialogDescription>{{ promptDescription }}</DialogDescription>
</DialogHeader>
<FormField name="value" :rules="validateValue" v-slot="{ componentField }">
<FormField
name="value"
:rules="validateValue"
:validate-on-blur="false"
:validate-on-change="false"
:validate-on-input="false"
:validate-on-model-update="false"
v-slot="{ componentField }">
<FormItem>
<FormLabel class="sr-only">Input</FormLabel>
<FormControl>
@@ -125,7 +136,7 @@
</FormItem>
</FormField>
<DialogFooter>
<DialogFooter class="mt-5">
<Button type="button" variant="outline" @click="handleCancel">
{{ promptCancelText }}
</Button>

View File

@@ -0,0 +1 @@
export const DIALOG_OPEN_INJECTION_KEY = Symbol('vrcx-dialog-open');

View File

@@ -84,7 +84,7 @@
@update:modelValue="onValueChange">
<TabsList :class="listClass" :aria-label="ariaLabel || undefined">
<TabsIndicator
class="pointer-events-none absolute left-0 bottom-0 z-20 h-0.5 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) transition-[width,translate] duration-200 ease-out">
class="pointer-events-none absolute left-0 bottom-0 h-0.5 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) transition-[width,translate] duration-200 ease-out">
<div class="h-full w-full rounded-full bg-primary" />
</TabsIndicator>

View File

@@ -0,0 +1,65 @@
const MODAL_PORTAL_ROOT_ID = 'vrcx-modal-portal-root';
const APP_PORTAL_ROOT_ID = 'x-dialog-portal';
const BASE_Z_INDEX = 50;
const Z_STEP = 10;
let nextLayerIndex = 0;
function ensureModalPortalRoot() {
if (typeof document === 'undefined') {
return null;
}
let root = document.getElementById(APP_PORTAL_ROOT_ID);
if (root) {
root.style.position ||= 'relative';
root.style.isolation ||= 'isolate';
return root;
}
root = document.getElementById(MODAL_PORTAL_ROOT_ID);
if (root) {
return root;
}
root = document.createElement('div');
root.id = MODAL_PORTAL_ROOT_ID;
root.style.position = 'relative';
root.style.isolation = 'isolate';
document.body.appendChild(root);
return root;
}
export function acquireModalPortalLayer() {
const root = ensureModalPortalRoot();
if (!root) {
return {
element: undefined,
bringToFront() {},
release() {}
};
}
const element = document.createElement('div');
element.dataset.vrcxPortalLayer = 'modal';
element.style.position = 'relative';
element.style.isolation = 'isolate';
root.appendChild(element);
const bringToFront = () => {
nextLayerIndex += 1;
element.style.zIndex = String(BASE_Z_INDEX + nextLayerIndex * Z_STEP);
root.appendChild(element);
};
const release = () => {
element.remove();
};
return {
element,
bringToFront,
release
};
}