From 179aafe7bbc95f6c3fa4ec229e523c54136cf430 Mon Sep 17 00:00:00 2001 From: hechang27-sprt Date: Thu, 26 Mar 2026 19:03:29 +0800 Subject: [PATCH] fix(list-table-v2): eliminate resize flicker with queueMicrotask pattern - Fix call site bug where renamed function handleResize wasn't called - Use queueMicrotask to clear and set estimatedRowHeight in same microtask - This minimizes the gap where el-table-v2 uses default row height - Also update shouldUseProbeRow to check estimatedRowHeight prop - Add cell-text CSS for proper overflow handling in mini table - Document queueMicrotask pattern and build commands in spec guides Co-Authored-By: Claude Opus 4.6 --- .trellis/spec/frontend/quality-guidelines.md | 47 ++++++++++++++++- .../spec/guides/code-reuse-thinking-guide.md | 16 ++++++ examples/view/base/table-v2.vue | 2 +- packages/base/data/list-table-v2.vue | 50 ++++++++++++------- 4 files changed, 94 insertions(+), 21 deletions(-) diff --git a/.trellis/spec/frontend/quality-guidelines.md b/.trellis/spec/frontend/quality-guidelines.md index 5814000..f8b9728 100644 --- a/.trellis/spec/frontend/quality-guidelines.md +++ b/.trellis/spec/frontend/quality-guidelines.md @@ -59,13 +59,26 @@ When making changes to `packages/` directory and testing with `examples/`: const DEV_MODE_TS = "2026-03-26T03:50:00.000Z"; // Update timestamp to force reload ``` +### Build Commands + +This project uses **bun** as package manager: + +| Command | Purpose | When to Use | +|---------|---------|-------------| +| `bun run dev` | Start Vite dev server | Testing example project | +| `bun run build` | Build examples project | **DO NOT USE** - examples don't need build | +| `bun run build:lib` | Build library for external repos | When done with changes, to verify build | +| `bun run lint` | Lint code | Before commit | + +**Common mistake**: Running `bun run build` which builds the examples project unnecessarily. The examples project is for development only and Vite handles hot reload automatically. + ### Quick Test Cycle ```bash # 1. Make changes to packages/ # 2. Dev server auto-reloads when accessing example pages # 3. Modify DEV_MODE_TS in packages/manage/router/index.vue if changes don't appear -# 4. Run build when done to verify +# 4. Run build:lib when done to verify bun run build:lib ``` @@ -120,6 +133,38 @@ const height = probeRowRef.value?.offsetHeight; **Verification**: All rows should have identical heights and consistent spacing. +### el-table-v2 resize with probe row (clear-then-set pattern) + +When handling window/container resize with a probe row for dynamic row height: + +**Problem**: Clearing `estimatedRowHeight` to trigger re-measure creates a visible flash (1-2 frames) where el-table-v2 uses default height. + +**Root Cause**: The gap between clearing the old value and setting the new value is visible to el-table-v2. + +**Solution**: Measure first (capturing new values), then use `queueMicrotask` to clear and set in the SAME microtask: +```typescript +// 1. Measure first while old values still set +const headerHeight = headerEl?.offsetHeight; +const rowHeight = firstRow?.offsetHeight; +const newHeader = headerHeight && headerHeight > 0 ? headerHeight : estimatedHeaderHeight.value; +const newRow = rowHeight && rowHeight > 0 ? rowHeight : estimatedRowHeight.value; + +// 2. Clear then set in same microtask (before next paint) +estimatedRowHeight.value = undefined; +estimatedHeaderHeight.value = undefined; +queueMicrotask(() => { + estimatedRowHeight.value = newRow; + estimatedHeaderHeight.value = newHeader; +}); +``` + +**Why queueMicrotask works**: It defers the set to the end of the current microtask queue, but BEFORE the next paint. So the sequence is: +- Microtask 1: `undefined` is set (triggers el-table-v2 re-measure) +- Microtask 2 (queued): New value is set +- Paint: Only one paint happens with the correct value + +**Without queueMicrotask**: The clear and set happen in separate Vue reactivity updates, causing TWO paints (one with undefined, one with new value). + --- ## Code Review Checklist diff --git a/.trellis/spec/guides/code-reuse-thinking-guide.md b/.trellis/spec/guides/code-reuse-thinking-guide.md index f9d5f99..27afc23 100644 --- a/.trellis/spec/guides/code-reuse-thinking-guide.md +++ b/.trellis/spec/guides/code-reuse-thinking-guide.md @@ -97,6 +97,22 @@ When you've made similar changes to multiple files: --- +## Gotcha: Rename Without Propagation + +**Problem**: When renaming a function, variable, or component, IDE refactoring tools update all references automatically. But if you manually rename (even with search-and-replace), you may miss some references. + +**Symptom**: Code compiles/runs but behavior is broken because some code still references the old name. + +**Example**: Renaming `debouncedMeasure` to `handleResize` but forgetting to update the call site in a ResizeObserver callback. + +**Prevention checklist**: +- [ ] **Use IDE rename** (F2 in most IDEs) - it finds ALL references +- [ ] **If manual rename**: Search for the old name with grep BEFORE renaming to find all occurrences +- [ ] **If manual rename**: Search for the new name AFTER renaming to verify all references were updated +- [ ] **Test the feature** that uses the renamed entity + +--- + ## Checklist Before Commit - [ ] Searched for existing similar code diff --git a/examples/view/base/table-v2.vue b/examples/view/base/table-v2.vue index b16fff3..c25a97f 100644 --- a/examples/view/base/table-v2.vue +++ b/examples/view/base/table-v2.vue @@ -39,7 +39,7 @@ const example = reactive({ const generateData = () => { const rows = []; const now = Math.floor(Date.now() / 1000); // Unix timestamp in seconds - const lorem = new LoremIpsum({ sentencesPerParagraph: { min: 1, max: 4 }, wordsPerSentence: { min: 5, max: 10 } }); + const lorem = new LoremIpsum({ sentencesPerParagraph: { min: 2, max: 5 }, wordsPerSentence: { min: 5, max: 20 } }); for (let i = 1; i <= 100; i++) { rows.push({ id: i, diff --git a/packages/base/data/list-table-v2.vue b/packages/base/data/list-table-v2.vue index d679677..a6909fc 100644 --- a/packages/base/data/list-table-v2.vue +++ b/packages/base/data/list-table-v2.vue @@ -6,7 +6,12 @@
-
+
{ item.name || (item.i18n ? t(item.i18n) : item.key) }
@@ -191,7 +196,7 @@ const getProbeCellText = (rowData: any, item: TableColumn): string => { // Whether to use probe row for dynamic height measurement // Only use probe row when rowHeight is NOT explicitly specified const shouldUseProbeRow = computed(() => { - return prop.rowHeight === undefined; + return prop.rowHeight === undefined && prop.estimatedRowHeight === undefined; }); // The estimated row height to pass to el-table-v2 @@ -243,36 +248,37 @@ watch( // Setup ResizeObserver on mount (not window resize - that causes flickering) onMounted(() => { - // Create debounced resize handler for mini table width changes - const debouncedMeasure = lodash.debounce(async () => { + // Resize handler - measure first, then clear and set in same microtask + const handleResize = lodash.debounce(async () => { if (!shouldUseProbeRow.value || miniTableData.value.length === 0) { return; } - // Clear previous measurements so el-table-v2 recalculates - estimatedRowHeight.value = undefined; - estimatedHeaderHeight.value = undefined; await nextTick(); - // Wait TWO frames for layout to settle await new Promise((resolve) => requestAnimationFrame(resolve)); await new Promise((resolve) => requestAnimationFrame(resolve)); - // Measure header height + + // Measure first while old values still set const headerEl = miniTableRef.value?.querySelector(".mini-header"); const headerHeight = headerEl?.offsetHeight; - if (headerHeight && headerHeight > 0) { - estimatedHeaderHeight.value = headerHeight; - } - // Measure first row height const firstRow = miniTableRef.value?.querySelector(".mini-row:not(.mini-header)"); const rowHeight = firstRow?.offsetHeight; - if (rowHeight && rowHeight > 0) { - estimatedRowHeight.value = rowHeight; - } + + const newHeader = headerHeight && headerHeight > 0 ? headerHeight : estimatedHeaderHeight.value; + const newRow = rowHeight && rowHeight > 0 ? rowHeight : estimatedRowHeight.value; + + // Clear then set in same microtask to minimize gap + estimatedRowHeight.value = undefined; + estimatedHeaderHeight.value = undefined; + queueMicrotask(() => { + estimatedRowHeight.value = newRow; + estimatedHeaderHeight.value = newHeader; + }); }, 50); // Use ResizeObserver on .my-table to detect width changes // This is better than window resize because it only fires when OUR container changes miniTableResizeObserver = new ResizeObserver(() => { - debouncedMeasure(); + handleResize(); }); if (myTableRef.value) { @@ -472,12 +478,12 @@ const renderCellContent = (item: TableColumn, value: any, row: any, slots: Retur // Handle dict display if (item.dict) { - return {formatterByDist(item.dict, value)}; + return {formatterByDist(item.dict, value)}; } // Handle formatting const formatted = formatCellValue(value, item, row); - return {formatted}; + return {formatted}; }; // Get mini cell style - mirrors the real table's column width/flex distribution @@ -609,6 +615,12 @@ const getMiniCellStyle = (item: TableColumn): Record => { background: v-bind("state.style.tableChildBg") !important; } +.my-table :deep(.cell-text) { + display: block; + overflow: hidden; + text-overflow: ellipsis; +} + .my-pagination { display: flex; justify-content: center;