fix(grid): keep footer actions visible with long selection aggregates
This commit is contained in:
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, {
|
||||
|
||||
Reference in new issue
Block a user