fix(grid): keep footer actions visible with long selection aggregates

This commit is contained in:
Jeff authored and GitHub committed 2026-10-27 12:37:06 +08:00
1 parent 640c579b42
commit 54d671808c
3 files changed
+93 -10

No files matched your search

@@ -17223,7 +17223,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
<ErrorBanner v-if="saveError" variant="card" :title="t('grid.saveErrorTitle')" :message="saveError" copy-mode="label" dismissible @dismiss="saveError = ''" />
<!-- Bottom status bar -->
<div v-if="!isErrorResult" class="grid grid-cols-[max-content_minmax(0,1fr)_max-content] items-center gap-2 px-3 py-1 border-t text-xs text-muted-foreground bg-muted/30 shrink-0" :class="{ 'relative max-sm:pt-7': selectedColumnInfo }">
<div v-if="!isErrorResult" class="grid grid-cols-[max-content_minmax(0,1fr)_minmax(0,max-content)] items-center gap-2 px-3 py-1 border-t text-xs text-muted-foreground bg-muted/30 shrink-0" :class="{ 'relative max-sm:pt-7': selectedColumnInfo }">
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
<span v-if="hasData" class="shrink-0">
{{
@@ -69,6 +69,11 @@ const emit = defineEmits<{
// 拖拽选区中聚合值显示为占位符 "…",此时不给复制入口,避免复制出一个假值
const selectionSummaryValueClass = computed(() => (props.selectionSummaryPending ? "" : "cursor-pointer transition-colors hover:text-foreground"));
// Aggregates truncate when the footer runs out of room (long numbers, narrow windows), so the tooltip carries the full value.
function selectionSummaryValueTitle(text: string) {
return props.selectionSummaryPending ? undefined : `${t("grid.copyValue")}: ${text}`;
}
function copySelectionSummaryValue(text: string) {
if (props.selectionSummaryPending) return;
emit("copySelectionSummary", text);
@@ -107,18 +112,50 @@ function handlePageInputKeydown(event: KeyboardEvent) {
</script>
<template>
<div class="flex min-w-max items-center justify-end gap-1">
<div v-if="selectionSummary" class="flex shrink-0 items-center gap-3 tabular-nums">
<button type="button" class="shrink-0 text-left" :class="selectionSummaryValueClass" :title="selectionSummaryPending ? undefined : t('grid.copyValue')" :disabled="selectionSummaryPending" data-selection-summary-sum @click="copySelectionSummaryValue(selectionSummarySumText)">
<div class="flex min-w-0 items-center justify-end gap-1">
<div v-if="selectionSummary" class="flex min-w-0 items-center gap-3 overflow-hidden tabular-nums">
<button
type="button"
class="min-w-0 truncate text-left"
:class="selectionSummaryValueClass"
:title="selectionSummaryValueTitle(t('grid.selectionSum', { value: selectionSummarySumText }))"
:disabled="selectionSummaryPending"
data-selection-summary-sum
@click="copySelectionSummaryValue(selectionSummarySumText)"
>
<span v-if="!selectionSummaryPending" class="sr-only">{{ t("grid.copyValue") }}: </span>{{ t("grid.selectionSum", { value: selectionSummarySumText }) }}
</button>
<button type="button" class="shrink-0 text-left" :class="selectionSummaryValueClass" :title="selectionSummaryPending ? undefined : t('grid.copyValue')" :disabled="selectionSummaryPending" data-selection-summary-average @click="copySelectionSummaryValue(selectionSummaryAverageText)">
<button
type="button"
class="min-w-0 truncate text-left"
:class="selectionSummaryValueClass"
:title="selectionSummaryValueTitle(t('grid.selectionAverage', { value: selectionSummaryAverageText }))"
:disabled="selectionSummaryPending"
data-selection-summary-average
@click="copySelectionSummaryValue(selectionSummaryAverageText)"
>
<span v-if="!selectionSummaryPending" class="sr-only">{{ t("grid.copyValue") }}: </span>{{ t("grid.selectionAverage", { value: selectionSummaryAverageText }) }}
</button>
<button type="button" class="shrink-0 text-left" :class="selectionSummaryValueClass" :title="selectionSummaryPending ? undefined : t('grid.copyValue')" :disabled="selectionSummaryPending" data-selection-summary-min @click="copySelectionSummaryValue(selectionSummaryMinText)">
<button
type="button"
class="min-w-0 truncate text-left"
:class="selectionSummaryValueClass"
:title="selectionSummaryValueTitle(t('grid.selectionMin', { value: selectionSummaryMinText }))"
:disabled="selectionSummaryPending"
data-selection-summary-min
@click="copySelectionSummaryValue(selectionSummaryMinText)"
>
<span v-if="!selectionSummaryPending" class="sr-only">{{ t("grid.copyValue") }}: </span>{{ t("grid.selectionMin", { value: selectionSummaryMinText }) }}
</button>
<button type="button" class="shrink-0 text-left" :class="selectionSummaryValueClass" :title="selectionSummaryPending ? undefined : t('grid.copyValue')" :disabled="selectionSummaryPending" data-selection-summary-max @click="copySelectionSummaryValue(selectionSummaryMaxText)">
<button
type="button"
class="min-w-0 truncate text-left"
:class="selectionSummaryValueClass"
:title="selectionSummaryValueTitle(t('grid.selectionMax', { value: selectionSummaryMaxText }))"
:disabled="selectionSummaryPending"
data-selection-summary-max
@click="copySelectionSummaryValue(selectionSummaryMaxText)"
>
<span v-if="!selectionSummaryPending" class="sr-only">{{ t("grid.copyValue") }}: </span>{{ t("grid.selectionMax", { value: selectionSummaryMaxText }) }}
</button>
<div class="flex shrink-0 items-center gap-1">
@@ -128,7 +165,7 @@ function handlePageInputKeydown(event: KeyboardEvent) {
<span class="shrink-0">{{ t("grid.rows", { count: selectionSummary.rowCount }) }}</span>
</div>
</div>
<Loader2 class="w-3 h-3 text-muted-foreground" :class="loading ? 'animate-spin' : 'invisible'" aria-hidden="true" />
<Loader2 class="w-3 h-3 shrink-0 text-muted-foreground" :class="loading ? 'animate-spin' : 'invisible'" aria-hidden="true" />
<template v-if="paginationEnabled && (infiniteScrollEnabled || loadAllRowsActive || infiniteScrollAllLoaded)">
<span v-if="infiniteScrollAllLoaded" class="text-xs text-muted-foreground shrink-0">{{ t("grid.allLoaded") }}</span>
</template>
@@ -3,7 +3,7 @@
import { createApp, defineComponent, h, nextTick, shallowRef } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { dispatch, findAll, findOne, hostText, mountComponent } from "./vueHostHarness";
import { dispatch, findAll, findOne, hostText, mountComponent, type HostNode } from "./vueHostHarness";
import type { DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
const settingsMock = vi.hoisted(() => ({ store: null as any }));
@@ -503,7 +503,7 @@ describe("DataGridPagination", () => {
// the complete button interaction model on a clickable text span.
expect(sum.type).toBe("button");
expect(average.type).toBe("button");
expect(sum.props.title).toBe("grid.copyValue");
expect(sum.props.title).toBe("grid.copyValue: grid.selectionSum");
expect(hostText(sum)).toContain("grid.copyValue");
dispatch(sum, "click");
@@ -514,6 +514,52 @@ describe("DataGridPagination", () => {
expect(copySelectionSummary).toHaveBeenCalledTimes(2);
});
it("lets long aggregates shrink so load-all and export stay in the footer (#13468)", () => {
const longValue = "2026102710000008194";
const mounted = mountComponent(DataGridPagination, {
selectionSummary: { cellCount: 1, rowCount: 1 },
selectionSummarySumText: longValue,
selectionSummaryAverageText: longValue,
selectionSummaryMinText: longValue,
selectionSummaryMaxText: longValue,
selectionSummaryCount: 1,
selectionSummaryDistinctCount: 1,
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
canLoadAllRows: true,
pageSize: 100,
customPageSizeInput: "",
pageSizeMenuItems: [],
exportMenuItems: [],
currentPage: 1,
canGoNextPage: true,
canJumpLastPage: true,
});
const classes = (node: HostNode) => String(node.props.class).split(/\s+/);
const sum = findOne(mounted.root, (node) => node.props["data-selection-summary-sum"] === "");
const summaryGroup = sum.parent!;
const bar = summaryGroup.parent!;
// The status bar caps this column at the space left over, so the bar itself must be
// allowed to be narrower than its content instead of forcing max-content width.
expect(classes(bar)).toContain("min-w-0");
expect(classes(bar)).not.toContain("min-w-max");
const aggregates = ["sum", "average", "min", "max"].map((key) => findOne(mounted.root, (node) => node.props[`data-selection-summary-${key}`] === ""));
for (const aggregate of aggregates) {
expect(classes(aggregate)).toEqual(expect.arrayContaining(["min-w-0", "truncate"]));
expect(classes(aggregate)).not.toContain("shrink-0");
}
expect(classes(summaryGroup)).toEqual(expect.arrayContaining(["min-w-0", "overflow-hidden"]));
expect(classes(summaryGroup)).not.toContain("shrink-0");
// Truncated text stays readable on hover.
expect(sum.props.title).toBe("grid.copyValue: grid.selectionSum");
const loadAll = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow");
expect(classes(loadAll)).toContain("shrink-0");
});
it("keeps the aggregate values non-copyable while the selection drag is pending", () => {
const copySelectionSummary = vi.fn();
const mounted = mountComponent(DataGridPagination, {