feat(grid): copy selection aggregate values on click

This commit is contained in:
Abeautifulsnow
2026-10-09 17:55:15 +08:00
committed by GitHub
parent d6777a5ce4
commit 517f939b66
3 changed files with 93 additions and 3 deletions
@@ -15475,6 +15475,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
:selection-summary="selectionSummary"
:selection-summary-sum-text="selectionSummarySumText"
:selection-summary-average-text="selectionSummaryAverageText"
:selection-summary-pending="isSelectingCells"
:loading="gridPaginationBusy || infiniteScrollLoading"
:infinite-scroll-enabled="infiniteScrollEnabled"
:infinite-scroll-all-loaded="infiniteScrollAllLoaded"
@@ -15500,6 +15501,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@last-page="lastPage"
@load-all-rows="loadAllRowsAndGoToLast"
@select-export="selectExportMenuItem"
@copy-selection-summary="copyText"
/>
</div>
@@ -17,6 +17,8 @@ const props = withDefaults(
selectionSummary: { cellCount: number; rowCount: number } | null;
selectionSummarySumText: string;
selectionSummaryAverageText: string;
// 拖拽选区期间聚合值只是占位符,父组件把该状态传下来,值不可复制
selectionSummaryPending?: boolean;
loading: boolean;
infiniteScrollEnabled: boolean;
infiniteScrollAllLoaded: boolean;
@@ -33,7 +35,7 @@ const props = withDefaults(
canGoNextPage: boolean;
canJumpLastPage: boolean;
}>(),
{ paginationEnabled: true, loadAllRowsActive: false, loadAllRowsEnabled: true, canLoadAllRows: false, canExport: true },
{ paginationEnabled: true, loadAllRowsActive: false, loadAllRowsEnabled: true, canLoadAllRows: false, canExport: true, selectionSummaryPending: false },
);
const customPageSizeInput = defineModel<string>("customPageSizeInput", { default: "" });
@@ -56,8 +58,18 @@ const emit = defineEmits<{
loadAllRows: [];
selectExport: [value: string];
applyCustomPageSizeAndSetDefault: [];
// 载荷是聚合值当前显示出来的文本,保证复制到的就是用户看到的那个数
copySelectionSummary: [text: string];
}>();
// 拖拽选区中聚合值显示为占位符 "…",此时不给复制入口,避免复制出一个假值
const selectionSummaryValueClass = computed(() => (props.selectionSummaryPending ? "" : "cursor-pointer transition-colors hover:text-foreground"));
function copySelectionSummaryValue(text: string) {
if (props.selectionSummaryPending) return;
emit("copySelectionSummary", text);
}
watch(
() => props.currentPage,
(page) => {
@@ -93,8 +105,12 @@ function handlePageInputKeydown(event: KeyboardEvent) {
<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">
<span class="shrink-0">{{ t("grid.selectionSum", { value: selectionSummarySumText }) }}</span>
<span class="shrink-0">{{ t("grid.selectionAverage", { value: selectionSummaryAverageText }) }}</span>
<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)">
<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)">
<span v-if="!selectionSummaryPending" class="sr-only">{{ t("grid.copyValue") }}: </span>{{ t("grid.selectionAverage", { value: selectionSummaryAverageText }) }}
</button>
<div class="flex shrink-0 items-center gap-1">
<span class="shrink-0">{{ t("grid.selectionCells", { count: selectionSummary.cellCount }) }}</span>
<span class="shrink-0">{{ t("grid.rows", { count: selectionSummary.rowCount }) }}</span>
@@ -461,6 +461,78 @@ describe("DataGridPagination", () => {
const exportMenus = findAll(mounted.root, (node) => node.props["data-stub"] === "LightDropdown" && node.props["aria-label"] === "grid.export");
expect(exportMenus).toHaveLength(1);
});
it("copies each aggregate value on click, and only the values", () => {
const copySelectionSummary = vi.fn();
const mounted = mountComponent(DataGridPagination, {
selectionSummary: { cellCount: 4, rowCount: 2 },
selectionSummarySumText: "10",
selectionSummaryAverageText: "2.5",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
pageSize: 100,
customPageSizeInput: "",
pageSizeMenuItems: [],
exportMenuItems: [],
currentPage: 1,
canGoNextPage: false,
canJumpLastPage: false,
onCopySelectionSummary: copySelectionSummary,
});
const sum = findOne(mounted.root, (node) => node.props["data-selection-summary-sum"] === "");
const average = findOne(mounted.root, (node) => node.props["data-selection-summary-average"] === "");
// Native buttons provide tab focus plus Enter/Space activation without recreating
// 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(hostText(sum)).toContain("grid.copyValue");
dispatch(sum, "click");
expect(copySelectionSummary).toHaveBeenCalledWith("10");
dispatch(average, "click");
expect(copySelectionSummary).toHaveBeenLastCalledWith("2.5");
expect(copySelectionSummary).toHaveBeenCalledTimes(2);
});
it("keeps the aggregate values non-copyable while the selection drag is pending", () => {
const copySelectionSummary = vi.fn();
const mounted = mountComponent(DataGridPagination, {
selectionSummary: { cellCount: 4, rowCount: 2 },
selectionSummarySumText: "…",
selectionSummaryAverageText: "…",
selectionSummaryPending: true,
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
pageSize: 100,
customPageSizeInput: "",
pageSizeMenuItems: [],
exportMenuItems: [],
currentPage: 1,
canGoNextPage: false,
canJumpLastPage: false,
onCopySelectionSummary: copySelectionSummary,
});
const sum = findOne(mounted.root, (node) => node.props["data-selection-summary-sum"] === "");
// Pending aggregate placeholders retain their layout but expose no copy action.
expect(sum.props.title).toBeUndefined();
expect(sum.props.disabled).toBe(true);
dispatch(sum, "click");
expect(copySelectionSummary).not.toHaveBeenCalled();
});
});
describe("DataGrid selection summary copy wiring", () => {
it("routes the pagination aggregate copy through the grid clipboard helper", () => {
expect(dataGridSource).toMatch(/@copy-selection-summary="copyText"/);
expect(dataGridSource).toMatch(/:selection-summary-pending="isSelectingCells"/);
});
});
describe("DataGridColumnHeader", () => {