fix(ui): support legacy WebView dialog viewports
This commit is contained in:
parent
4d2b60326d
commit
127328a4ed
|
|
@ -381,7 +381,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
|||
</div>
|
||||
|
||||
<Dialog v-model:open="scheduleDialogOpen">
|
||||
<DialogContent class="max-h-[min(760px,calc(100dvh-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
|
||||
<DialogContent class="max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ editingScheduleId ? t("databaseBackup.editSchedule") : t("databaseBackup.addSchedule") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -6377,7 +6377,7 @@ function openExternalUrl(url: string) {
|
|||
.connection-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100dvh - 2rem);
|
||||
max-height: calc(var(--dbx-viewport-height) - 2rem);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@ const isMaximized = ref(false);
|
|||
const dialogStyle = computed(() => {
|
||||
if (isMaximized.value) {
|
||||
return {
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
maxWidth: "100vw",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
borderRadius: "0",
|
||||
};
|
||||
}
|
||||
|
|
@ -133,6 +133,13 @@ function toggleMaximize() {
|
|||
isMaximized.value = !isMaximized.value;
|
||||
}
|
||||
|
||||
function handleDialogEscape(event: KeyboardEvent) {
|
||||
if (!showOptionsPanel.value) return;
|
||||
|
||||
event.preventDefault();
|
||||
showOptionsPanel.value = false;
|
||||
}
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let saveTimeout: number | null = null;
|
||||
|
||||
|
|
@ -679,7 +686,7 @@ const targetConnectionInfo = computed(() => {
|
|||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent :class="['min-w-[800px] flex flex-col overflow-hidden', isMaximized ? '' : 'resize']" :style="dialogStyle" @interact-outside.prevent>
|
||||
<DialogContent :class="['flex flex-col overflow-hidden', isMaximized ? 'min-w-0' : 'min-w-[800px] resize']" :portal-class="isMaximized ? 'p-0' : undefined" :style="dialogStyle" @interact-outside.prevent @escape-key-down="handleDialogEscape">
|
||||
<Button variant="ghost" size="icon-sm" class="absolute top-2 right-10 z-10" @click="toggleMaximize">
|
||||
<Maximize2 v-if="!isMaximized" class="w-4 h-4" />
|
||||
<Minimize2 v-else class="w-4 h-4" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const dialogSource = readFileSync(new URL("../SchemaDiffDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("SchemaDiffDialog fullscreen layout", () => {
|
||||
it("fits the dialog to its portal instead of the viewport width", () => {
|
||||
expect(dialogSource).toContain('width: "100%"');
|
||||
expect(dialogSource).toContain('height: "100%"');
|
||||
expect(dialogSource).not.toContain('width: "100vw"');
|
||||
expect(dialogSource).not.toContain('height: "100vh"');
|
||||
});
|
||||
|
||||
it("removes the normal dialog gutter and minimum width while maximized", () => {
|
||||
expect(dialogSource).toContain(":portal-class=\"isMaximized ? 'p-0' : undefined\"");
|
||||
expect(dialogSource).toContain("isMaximized ? 'min-w-0' : 'min-w-[800px] resize'");
|
||||
});
|
||||
|
||||
it("closes the options panel before allowing Escape to dismiss the dialog", () => {
|
||||
expect(dialogSource).toContain('@escape-key-down="handleDialogEscape"');
|
||||
expect(dialogSource).toContain("if (!showOptionsPanel.value) return;");
|
||||
expect(dialogSource).toContain("event.preventDefault();");
|
||||
expect(dialogSource).toContain("showOptionsPanel.value = false;");
|
||||
});
|
||||
});
|
||||
|
|
@ -172,7 +172,9 @@ const settingsRootComponent = computed(() => (isSettingsPage.value ? "div" : Dia
|
|||
const settingsRootProps = computed(() => (isSettingsPage.value ? {} : { open: props.open === true }));
|
||||
const settingsRootClass = computed(() => (isSettingsPage.value ? "h-full min-h-0 overflow-hidden bg-background" : ""));
|
||||
const settingsContentComponent = computed(() => (isSettingsPage.value ? "div" : DialogContent));
|
||||
const settingsContentClass = computed(() => (isSettingsPage.value ? "flex h-full min-h-0 flex-col gap-4 overflow-hidden bg-background p-4" : "h-[min(660px,calc(100dvh-80px))] !max-w-[min(920px,calc(100vw-32px))] grid-rows-[auto_minmax(0,1fr)] gap-3 p-4 sm:!max-w-[min(920px,calc(100vw-48px))]"));
|
||||
const settingsContentClass = computed(() =>
|
||||
isSettingsPage.value ? "flex h-full min-h-0 flex-col gap-4 overflow-hidden bg-background p-4" : "h-[min(660px,calc(var(--dbx-viewport-height)-80px))] !max-w-[min(920px,calc(100vw-32px))] grid-rows-[auto_minmax(0,1fr)] gap-3 p-4 sm:!max-w-[min(920px,calc(100vw-48px))]",
|
||||
);
|
||||
const settingsTitleComponent = computed(() => (isSettingsPage.value ? "h2" : DialogTitle));
|
||||
|
||||
function onSettingsRootOpenChange(value: boolean) {
|
||||
|
|
|
|||
|
|
@ -822,7 +822,7 @@ watch(targetMode, (mode) => {
|
|||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogScrollContent class="flex max-h-[calc(100dvh-6rem)] min-h-0 flex-col overflow-hidden sm:max-w-[980px]" :trap-focus="false" @interact-outside.prevent>
|
||||
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 flex-col overflow-hidden sm:max-w-[980px]" :trap-focus="false" @interact-outside.prevent>
|
||||
<DialogHeader class="shrink-0 pr-8">
|
||||
<DialogTitle class="flex items-center gap-2 text-base">
|
||||
<FileUp class="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ defineExpose({ show });
|
|||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="h-[min(760px,calc(100dvh-2rem))] flex flex-col overflow-hidden sm:max-w-3xl">
|
||||
<DialogContent class="h-[min(760px,calc(var(--dbx-viewport-height)-2rem))] flex flex-col overflow-hidden sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("extension.manageTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ function closeDialog() {
|
|||
|
||||
<template>
|
||||
<Dialog :open="props.open" @update:open="(value) => emit('update:open', value)">
|
||||
<DialogContent class="h-[min(760px,calc(100dvh-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-[900px]">
|
||||
<DialogContent class="h-[min(760px,calc(var(--dbx-viewport-height)-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-[900px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ function openResult(item: SearchResultItem) {
|
|||
|
||||
<template>
|
||||
<Dialog v-model:open="dialogOpen">
|
||||
<DialogScrollContent class="flex max-h-[calc(100dvh-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0">
|
||||
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0">
|
||||
<DialogHeader class="shrink-0 border-b px-5 py-4">
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Search class="h-5 w-5" />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const dialogSource = readFileSync(new URL("../DatabaseSearchDialog.vue", import.
|
|||
|
||||
describe("DatabaseSearchDialog layout", () => {
|
||||
it("keeps the header and footer visible within the dynamic viewport", () => {
|
||||
expect(dialogSource).toContain('class="flex max-h-[calc(100dvh-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0"');
|
||||
expect(dialogSource).toContain('class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0"');
|
||||
expect(dialogSource).toContain('<DialogHeader class="shrink-0 border-b px-5 py-4">');
|
||||
expect(dialogSource).toContain('<DialogFooter class="shrink-0 border-t px-5 py-3">');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ watch(
|
|||
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="handleOpenChange">
|
||||
<DialogScrollContent class="flex max-h-[calc(100dvh-6rem)] min-h-0 min-w-0 flex-col overflow-hidden sm:max-w-[860px]" :trap-focus="false" @interact-outside.prevent>
|
||||
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 min-w-0 flex-col overflow-hidden sm:max-w-[860px]" :trap-focus="false" @interact-outside.prevent>
|
||||
<DialogHeader class="shrink-0">
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<FileCode class="w-4 h-4" />
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative grid max-h-[calc(100dvh-2rem)] w-full max-w-sm gap-4 overflow-hidden rounded-xl border border-border p-4 text-sm shadow-lg duration-100 outline-none pointer-events-auto',
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative grid max-h-[calc(var(--dbx-viewport-height)-2rem)] w-full max-w-sm gap-4 overflow-hidden rounded-xl border border-border p-4 text-sm shadow-lg duration-100 outline-none pointer-events-auto',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
<div class="fixed inset-0 z-50 grid place-items-center p-4 pointer-events-none">
|
||||
<DialogContent
|
||||
data-slot="dialog-content"
|
||||
:class="cn('relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-4 shadow-lg duration-200 sm:rounded-lg md:w-full pointer-events-auto', props.class)"
|
||||
:class="cn('relative z-50 grid max-h-[calc(var(--dbx-viewport-height)-6rem)] w-full max-w-lg my-8 gap-4 border border-border bg-background p-4 shadow-lg duration-200 sm:rounded-lg md:w-full pointer-events-auto', props.class)"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
@pointer-down-outside="
|
||||
(event) => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { readFileSync } from "node:fs";
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const globalsCss = readFileSync(new URL("../globals.css", import.meta.url), "utf8");
|
||||
const dialogContentSource = readFileSync(new URL("../../components/ui/dialog/DialogContent.vue", import.meta.url), "utf8");
|
||||
const dialogScrollContentSource = readFileSync(new URL("../../components/ui/dialog/DialogScrollContent.vue", import.meta.url), "utf8");
|
||||
const connectionDialogSource = readFileSync(new URL("../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("legacy WebView CSS fallbacks", () => {
|
||||
it("scopes component overrides to WebViews without OKLCH support", () => {
|
||||
|
|
@ -24,4 +27,17 @@ describe("legacy WebView CSS fallbacks", () => {
|
|||
expect(tabsNestingDepth).toBeGreaterThan(0);
|
||||
expect(nestingDepth).toBe(0);
|
||||
});
|
||||
|
||||
it("falls back to the legacy viewport height when dynamic viewport units are unavailable", () => {
|
||||
const fallback = globalsCss.indexOf("--dbx-viewport-height: 100vh;");
|
||||
const supports = globalsCss.indexOf("@supports (height: 100dvh)");
|
||||
const enhanced = globalsCss.indexOf("--dbx-viewport-height: min(100vh, 100dvh);");
|
||||
|
||||
expect(fallback).toBeGreaterThan(-1);
|
||||
expect(supports).toBeGreaterThan(fallback);
|
||||
expect(enhanced).toBeGreaterThan(supports);
|
||||
expect(dialogContentSource).toContain("max-h-[calc(var(--dbx-viewport-height)-2rem)]");
|
||||
expect(dialogScrollContentSource).toContain("max-h-[calc(var(--dbx-viewport-height)-6rem)]");
|
||||
expect(connectionDialogSource).toContain("max-height: calc(var(--dbx-viewport-height) - 2rem);");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -120,6 +120,13 @@
|
|||
--sidebar-border: rgb(229 229 229);
|
||||
--sidebar-ring: rgb(161 161 161);
|
||||
--dbx-window-top-border: rgb(0 0 0 / 0.35);
|
||||
--dbx-viewport-height: 100vh;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
:root {
|
||||
--dbx-viewport-height: min(100vh, 100dvh);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ test("SQL file execution dialog SFC compiles", () => {
|
|||
});
|
||||
|
||||
test("SQL file execution dialog keeps actions visible within narrow viewports", () => {
|
||||
assert.match(dialogSource, /DialogScrollContent class="[^"]*max-h-\[calc\(100dvh-6rem\)\][^"]*flex-col[^"]*overflow-hidden/);
|
||||
assert.match(dialogSource, /DialogScrollContent class="[^"]*max-h-\[calc\(var\(--dbx-viewport-height\)-6rem\)\][^"]*flex-col[^"]*overflow-hidden/);
|
||||
assert.match(dialogSource, /<DialogHeader class="shrink-0">/);
|
||||
assert.match(dialogSource, /class="grid min-h-0 min-w-0 flex-1 gap-4 overflow-y-auto py-3"/);
|
||||
assert.match(dialogSource, /class="grid grid-cols-1 gap-3 sm:grid-cols-2"/);
|
||||
|
|
|
|||
Loading…
Reference in New Issue