From 6e9e8d181889142dccb72f5202752004bc1052c6 Mon Sep 17 00:00:00 2001 From: zipg Date: Fri, 4 Sep 2026 17:10:50 +0800 Subject: [PATCH] feat(ui): progressively shrink data grid toolbar --- apps/desktop/src/components/grid/DataGrid.vue | 64 ++++++---- .../grid/DataGridColumnLayoutPopover.vue | 4 +- .../src/components/grid/DataGridToolbar.vue | 111 +++++++++++++----- apps/desktop/src/i18n/locales/en.ts | 2 +- apps/desktop/src/i18n/locales/es.ts | 2 +- apps/desktop/src/i18n/locales/it.ts | 2 +- apps/desktop/src/i18n/locales/ja.ts | 2 +- apps/desktop/src/i18n/locales/ko.ts | 2 +- apps/desktop/src/i18n/locales/pt-BR.ts | 2 +- apps/desktop/src/i18n/locales/tr.ts | 2 +- apps/desktop/src/i18n/locales/zh-CN.ts | 2 +- apps/desktop/src/i18n/locales/zh-TW.ts | 2 +- .../dataGrid/dataGridToolbar.spec.ts | 26 ++++ .../src/lib/dataGrid/dataGridToolbar.ts | 19 +++ packages/app-tests/queryResultToolbar.test.ts | 13 +- 15 files changed, 188 insertions(+), 67 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index a1e77a3f1b..8e7071f63e 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -354,7 +354,9 @@ import { resolveGridFocusRestoreTarget } from "@/lib/dataGrid/dataGridFocusResto import { buildOrderedGridRows, type GridInsertRowPosition, type GridNewRowPlacement } from "@/lib/dataGrid/gridNewRowPlacement"; import { DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH, + DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER, dataGridDeleteRowToolbarState, + dataGridToolbarActionCollapseCount, isDataGridToolbarCompact, type DataGridReloadIntent, type DataGridToolbarActionCapability, @@ -712,6 +714,7 @@ const columnCommentMap = computed(() => { const dataGridTopbarWidth = ref(0); const dataGridViewportWidth = ref(0); const dataGridTopbarOverflowCompact = ref(false); +const dataGridTopbarOverflowActionCount = ref(0); const dataGridTopbarExpandedRequiredWidth = ref(0); const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader); const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader); @@ -733,6 +736,8 @@ const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.c const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode); const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode); const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); +const responsiveDataGridToolbarActionCount = computed(() => dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); +const compactDataGridToolbarActionCount = computed(() => Math.max(responsiveDataGridToolbarActionCount.value, dataGridTopbarOverflowActionCount.value)); const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll); const queryResultMaxRows = computed(() => effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows)); const paginationMaxRows = computed(() => (isResultsContext.value ? queryResultMaxRows.value : undefined)); @@ -2561,6 +2566,8 @@ let gridScrollbarsRuntime: DataGridScrollbarsRuntime; let dataGridTopbarResizeObserver: ResizeObserver | null = null; let dataGridTopbarMutationObserver: MutationObserver | null = null; let dataGridTopbarRecheckTimer: ReturnType | null = null; +const DATA_GRID_TOPBAR_RECHECK_DELAY_MS = 360; +const DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX = 16; let cellEditResizeObserver: ResizeObserver | null = null; // vue-virtual-scroller's @resize only fires in pageMode (it observes window), // so in container-scroll mode (transpose uses RecycleScroller without pageMode) @@ -3210,43 +3217,57 @@ function observeGridHorizontalScrollbarScroller() { function updateDataGridTopbarWidth() { const topbar = dataGridTopbarRef.value; + const previousActionCount = compactDataGridToolbarActionCount.value; dataGridTopbarWidth.value = topbar?.clientWidth ?? 0; dataGridViewportWidth.value = typeof window === "undefined" ? 0 : window.innerWidth; if (!topbar) return; - const fixedBreakpointCompact = isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH); - if (fixedBreakpointCompact) return; - - if (dataGridTopbarOverflowCompact.value) { - if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) { - dataGridTopbarOverflowCompact.value = false; - dataGridTopbarExpandedRequiredWidth.value = 0; - } + if (compactDataGridToolbarActionCount.value !== previousActionCount) { + scheduleDataGridTopbarRecheck(); return; } + if (dataGridTopbarOverflowCompact.value || dataGridTopbarOverflowActionCount.value > 0) { + if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) { + dataGridTopbarOverflowCompact.value = false; + dataGridTopbarOverflowActionCount.value = 0; + dataGridTopbarExpandedRequiredWidth.value = 0; + scheduleDataGridTopbarRecheck(); + return; + } + } + // The action list gains preview/save/rollback controls in editable grids. The - // fixed breakpoint cannot see that extra width, so capture the full layout - // width before the overflow-clip wrapper can hide the trailing action. + // responsive breakpoint cannot see that extra width, so progressively + // compact the leftmost controls before overflow-clip hides a trailing action. if (topbar.scrollWidth > topbar.clientWidth + 1) { - dataGridTopbarExpandedRequiredWidth.value = topbar.scrollWidth; - dataGridTopbarOverflowCompact.value = true; + dataGridTopbarExpandedRequiredWidth.value = Math.max(dataGridTopbarExpandedRequiredWidth.value, topbar.scrollWidth + DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX); + if (!compactDataGridToolbar.value) { + dataGridTopbarOverflowCompact.value = true; + } else if (compactDataGridToolbarActionCount.value < DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.length) { + dataGridTopbarOverflowActionCount.value = compactDataGridToolbarActionCount.value + 1; + } else { + return; + } + scheduleDataGridTopbarRecheck(); } } function resetDataGridTopbarOverflowCompact() { - if (!dataGridTopbarOverflowCompact.value) return; + if (!dataGridTopbarOverflowCompact.value && dataGridTopbarOverflowActionCount.value === 0) return; dataGridTopbarOverflowCompact.value = false; + dataGridTopbarOverflowActionCount.value = 0; dataGridTopbarExpandedRequiredWidth.value = 0; + scheduleDataGridTopbarRecheck(); +} + +function scheduleDataGridTopbarRecheck() { if (dataGridTopbarRecheckTimer) clearTimeout(dataGridTopbarRecheckTimer); - // Wait for the label-width transition to finish before measuring the newly - // shortened action list. If it still overflows, updateDataGridTopbarWidth - // captures its new, smaller expanded width. dataGridTopbarRecheckTimer = setTimeout(() => { dataGridTopbarRecheckTimer = null; updateDataGridTopbarWidth(); - }, 360); + }, DATA_GRID_TOPBAR_RECHECK_DELAY_MS); } function clearDataGridTopbarRecheckTimer() { @@ -12676,7 +12697,8 @@ function openGridSnapshot() { -