fix(dialog): clamp grid track and wrap footer so dialogs stop clipping
This commit is contained in:
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/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user