fix(dialog): clamp grid track and wrap footer so dialogs stop clipping

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-27 16:06:25 +08:00
1 parent eb0994e980
commit 40549c9573
5 files changed
+68 -18

No files matched your search

@@ -90,7 +90,7 @@ watch(
<template>
<Dialog :open="open" @update:open="handleOpenChange">
<DialogContent
class="dbx-update-dialog sm:max-w-[520px]"
class="dbx-update-dialog sm:max-w-[700px]"
:show-close-button="!isCloseBlocked"
@interact-outside="
(e: Event) => {
@@ -140,29 +140,36 @@ watch(
<span>{{ t("updates.activeTasksBlockUpdate", { count: activeTaskCount }) }}</span>
</div>
</div>
<DialogFooter>
<Button v-if="!isCloseBlocked" variant="outline" @click="handleCancel">{{ t("dangerDialog.cancel") }}</Button>
<Button v-if="canIgnoreVersion" variant="ghost" :disabled="isIgnoringUpdate" @click="emit('ignore-version')">
<Loader2 v-if="isIgnoringUpdate" class="h-4 w-4 animate-spin" />
{{ t("updates.ignoreVersion") }}
</Button>
<DialogFooter class="min-w-0">
<template v-if="updateInfo?.update_available">
<Button variant="outline" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
<div class="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
<Button v-if="!isCloseBlocked" variant="outline" class="shrink-0" @click="handleCancel">{{ t("dangerDialog.cancel") }}</Button>
<Button v-if="canIgnoreVersion" variant="ghost" class="shrink-0" :disabled="isIgnoringUpdate" @click="emit('ignore-version')">
<Loader2 v-if="isIgnoringUpdate" class="h-4 w-4 animate-spin" />
{{ t("updates.ignoreVersion") }}
</Button>
<Button variant="outline" class="shrink-0" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
</div>
<template v-if="canDownloadAndInstallUpdate(updateInfo, isDesktop)">
<Button v-if="updateReady" :disabled="activeTaskCount > 0" @click="emit('restart')">{{ t("updates.restart") }}</Button>
<Button v-else-if="isInstallingUpdate" disabled>
<Button v-if="updateReady" class="shrink-0" :disabled="activeTaskCount > 0" @click="emit('restart')">{{ t("updates.restart") }}</Button>
<Button v-else-if="isInstallingUpdate" class="shrink-0" disabled>
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("updates.installing") }}
</Button>
<Button v-else-if="isDownloadingUpdate" class="w-52 tabular-nums" disabled>
<Button v-else-if="isDownloadingUpdate" class="w-52 shrink-0 tabular-nums" disabled>
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("updates.downloading", { progress: downloadProgress }) }}
</Button>
<Button v-else-if="updateDownloaded" :disabled="activeTaskCount > 0" @click="emit('install-downloaded')">{{ t("updates.exitAndUpdate") }}</Button>
<Button v-else :disabled="activeTaskCount > 0" @click="emit('download-and-install')">{{ t("updates.downloadAndInstall") }}</Button>
<Button v-else-if="updateDownloaded" class="shrink-0" :disabled="activeTaskCount > 0" @click="emit('install-downloaded')">{{ t("updates.exitAndUpdate") }}</Button>
<Button v-else class="shrink-0" :disabled="activeTaskCount > 0" @click="emit('download-and-install')">{{ t("updates.downloadAndInstall") }}</Button>
</template>
</template>
<Button v-else-if="updateCheckMessage" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
<template v-else>
<div class="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
<Button v-if="!isCloseBlocked" variant="outline" class="shrink-0" @click="handleCancel">{{ t("dangerDialog.cancel") }}</Button>
<Button v-if="updateCheckMessage" class="shrink-0" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
</div>
</template>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -170,7 +177,7 @@ watch(
<style>
html.dbx-legacy-webview [data-slot="dialog-content"].dbx-update-dialog[class~="max-w-sm"] {
max-width: 520px !important;
max-width: 700px !important;
}
html.dbx-legacy-webview [data-slot="dialog-content"].dbx-update-dialog [data-slot="dialog-footer"] {
@@ -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(var(--dbx-viewport-height)-2rem)] w-full max-w-sm gap-4 overflow-hidden rounded-lg 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 grid-cols-[minmax(0,1fr)] max-h-[calc(var(--dbx-viewport-height)-2rem)] w-full max-w-sm gap-4 overflow-hidden rounded-lg border border-border p-4 text-sm shadow-lg duration-100 outline-none pointer-events-auto',
props.class,
)
"
@@ -16,7 +16,7 @@ const props = withDefaults(
</script>
<template>
<div data-slot="dialog-footer" :class="cn('bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)">
<div data-slot="dialog-footer" :class="cn('bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:justify-end', props.class)">
<slot />
<DialogClose v-if="showCloseButton" as-child>
<Button variant="outline"> Close </Button>
@@ -0,0 +1,43 @@
// Source-contract guard for the shared dialog layout primitives.
// Overview: `DialogContent` renders a single-column CSS grid (no explicit
// `grid-template-columns`). By default a grid's implicit track sizes to the
// *max-content* of its children, so any child with a large min-content (e.g. a
// footer that lays out many `whitespace-nowrap` buttons on one line) can push
// the track wider than the dialog's `max-w-*`. The dialog's `overflow-hidden`
// then clips the right edge without any scrollbar or ellipsis — see #7325
// (Update dialog clips release notes and buttons on wide locales).
//
// These two guarantees are what stop that from re-happening:
// 1. `grid-cols-[minmax(0,1fr)]` pins the track to the dialog's available
// width and lets grid items shrink (equivalent to `min-width: 0`).
// 2. `sm:flex-wrap` on the footer lets a long row of buttons wrap instead of
// forcing the track wide.
// If either is removed, the clipping regresses. Keep these assertions updated
// alongside the components whenever the class strings are touched.
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogContentSource = readFileSync(new URL("../DialogContent.vue", import.meta.url), "utf8");
const dialogFooterSource = readFileSync(new URL("../DialogFooter.vue", import.meta.url), "utf8");
describe("dialog layout guard", () => {
it("pins the DialogContent grid track to the dialog width so children can shrink", () => {
// The base class (before `props.class`) must lock the implicit track to the
// available width. Without this, a wide grid item re-expands the track beyond
// `max-w-*` and the dialog's `overflow-hidden` clips content (issue #7325).
expect(dialogContentSource).toContain("relative grid grid-cols-[minmax(0,1fr)]");
// Keep it on the shared base class, not a per-dialog override, so every dialog
// that reuses DialogContent is protected.
expect(dialogContentSource).toMatch(/'[^']*relative grid grid-cols-\[minmax\(0,1fr\)\]/);
});
it("lets the dialog footer wrap so a row of buttons does not widen the track", () => {
// At the sm breakpoint the footer is a no-wrap row; a long label (e.g. the
// Spanish "Descargar e instalar") plus other actions then overflows. Wrapping
// keeps the footer's min-content down to one button, which in turn keeps the
// `minmax(0,1fr)` track inside the dialog.
expect(dialogFooterSource).toMatch(/flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:justify-end/);
});
});
@@ -107,7 +107,7 @@ describe("legacy WebView CSS fallbacks", () => {
});
it("keeps the update dialog's wide layout fallback scoped to legacy WebViews without media queries", () => {
expect(updateDialogSource).toContain('class="dbx-update-dialog sm:max-w-[520px]"');
expect(updateDialogSource).toContain('class="dbx-update-dialog sm:max-w-[700px]"');
expect(updateDialogSource).toContain('html.dbx-legacy-webview [data-slot="dialog-content"].dbx-update-dialog[class~="max-w-sm"]');
expect(updateDialogSource).toContain('dbx-update-dialog [data-slot="dialog-footer"]');
expect(updateDialogSource).not.toContain("@media");