From cf54b4ec94ba9cf245b138f7a4b40ffca5138851 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 21 May 2026 15:14:47 +0800 Subject: [PATCH 1/3] fix: avoid selection gap with frozen rows --- .../options/listTable-api-with-frozen.test.ts | 26 ++++++ packages/vtable/examples/debug/issue-5027.ts | 81 +++++++++++++++++++ packages/vtable/examples/menu.ts | 4 + packages/vtable/src/core/BaseTable.ts | 17 ++-- 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 packages/vtable/examples/debug/issue-5027.ts diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index efc31f54cd..7d6d6073a0 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -116,4 +116,30 @@ describe('listTable init test', () => { expect(listTable.cellIsInVisualView(3, 39)).toBe(true); expect(listTable.cellIsInVisualView(3, 38)).toBe(true); }); + + test('listTable body visible row range should ignore frozen rows offset duplication', () => { + const optionWithFrozenRows = { + ...option, + frozenRowCount: 5, + rightFrozenColCount: 0, + bottomFrozenRowCount: 0, + container: createDiv(), + records + }; + optionWithFrozenRows.container.style.position = 'relative'; + optionWithFrozenRows.container.style.width = '1000px'; + optionWithFrozenRows.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithFrozenRows); + frozenTable.scrollTop = 400; + + expect(frozenTable.getBodyVisibleRowRange()).toEqual({ + rowStart: 10, + rowEnd: 28 + }); + expect(frozenTable.getBodyVisibleCellRange()).toMatchObject({ + rowStart: 10, + rowEnd: 28 + }); + }); }); diff --git a/packages/vtable/examples/debug/issue-5027.ts b/packages/vtable/examples/debug/issue-5027.ts new file mode 100644 index 0000000000..f33e0f83ff --- /dev/null +++ b/packages/vtable/examples/debug/issue-5027.ts @@ -0,0 +1,81 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +function createRecords(count: number) { + return Array.from({ length: count }, (_, index) => ({ + name: `John ${index + 1}`, + age: 18 + (index % 10), + gender: index % 2 === 0 ? 'male' : 'female', + hobby: index % 3 === 0 ? '🏀' : index % 3 === 1 ? '🎸' : '📚' + })); +} + +export function createTable() { + const container = document.getElementById(CONTAINER_ID); + if (!container) { + throw new Error('Cannot find VTable container'); + } + const option: VTable.ListTableConstructorOptions = { + container, + columns: [ + { + field: 'name', + title: 'name', + width: 180 + }, + { + field: 'age', + title: 'age', + width: 120 + }, + { + field: 'gender', + title: 'gender', + width: 140 + }, + { + field: 'hobby', + title: 'hobby', + width: 140 + } + ], + records: createRecords(2000), + frozenRowCount: 5, + heightMode: 'standard', + widthMode: 'standard', + select: { + headerSelectMode: 'inline', + highlightMode: 'column' + }, + theme: VTable.themes.DEFAULT.extends({ + scrollStyle: { + visible: 'always', + hoverOn: false + } + }) + }; + + const tableInstance = new VTable.ListTable(option); + + // Preselect the age column so the scrollbar drag issue can be reproduced immediately. + tableInstance.selectCol(1); + + const info = document.createElement('div'); + info.style.cssText = ['margin: 8px 0', 'font-size: 12px', 'line-height: 18px', 'color: #333'].join(';'); + info.innerText = 'issue-5027: drag the vertical scrollbar while the age column is selected and frozenRowCount is 5.'; + container.parentElement?.insertBefore(info, container); + + const w = window as unknown as { + tableInstance?: VTable.ListTable; + issue5027?: { + table: VTable.ListTable; + selectAgeColumn: () => void; + }; + }; + w.tableInstance = tableInstance; + w.issue5027 = { + table: tableInstance, + selectAgeColumn: () => tableInstance.selectCol(1) + }; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 9b1f6fa603..fa2b3ed89c 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -33,6 +33,10 @@ export const menus = [ { path: 'debug', name: 'issue-5114' + }, + { + path: 'debug', + name: 'issue-5027' } ] }, diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 90b058be22..45c880566a 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -2503,17 +2503,19 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { /** 获取表格body部分的显示单元格范围 */ getBodyVisibleCellRange() { const { scrollTop, scrollLeft } = this; - const frozenRowsHeight = this.getFrozenRowsHeight(); const frozenColsContentWidth = this.getFrozenColsContentWidth(); const frozenColsOffset = this.getFrozenColsOffset(); const bottomFrozenRowsHeight = this.getBottomFrozenRowsHeight(); const rightFrozenColsWidth = this.getRightFrozenColsWidth(); // 计算非冻结 - const { row: rowStart } = this.getRowAt(scrollTop + frozenRowsHeight + 1); + const rowStart = Math.max(this.getTargetRowAt(scrollTop + 1)?.row ?? -1, this.frozenRowCount); const { col: colStart } = this.getColAt(scrollLeft + frozenColsContentWidth + 1); const rowEnd = this.getAllRowsHeight() > this.tableNoFrameHeight - ? this.getRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight).row + ? Math.max( + this.getTargetRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight)?.row ?? -1, + rowStart + ) : this.rowCount - 1; const colEnd = this.getAllColsWidth() > this.tableNoFrameWidth @@ -2532,13 +2534,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { */ getBodyVisibleRowRange(start_deltaY: number = 0, end_deltaY: number = 0) { const { scrollTop } = this; - const frozenRowsHeight = this.getFrozenRowsHeight(); const bottomFrozenRowsHeight = this.getBottomFrozenRowsHeight(); // 计算非冻结 - const { row: rowStart } = this.getRowAt(scrollTop + frozenRowsHeight + 1 + start_deltaY); + const rowStart = Math.max(this.getTargetRowAt(scrollTop + 1 + start_deltaY)?.row ?? -1, this.frozenRowCount); const rowEnd = this.getAllRowsHeight() > this.tableNoFrameHeight - ? this.getRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight + end_deltaY).row + ? Math.max( + this.getTargetRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight + end_deltaY)?.row ?? + -1, + rowStart + ) : this.rowCount - 1; if (rowEnd < 0) { return null; From 3a2225d4f50fd12cf1f08b34f6dea33ded6a8241 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 21 May 2026 15:15:22 +0800 Subject: [PATCH 2/3] docs: update changlog of rush --- .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc | 23 ++++++++++++++ .../pnpm-10.7.0/last-install.flag | 3 ++ .../pnpm-10.7.0/package-lock.json | 31 +++++++++++++++++++ .../node-v24.11.0/pnpm-10.7.0/package.json | 9 ++++++ .../fix-issue-5027_2026-05-21-07-15.json | 11 +++++++ 5 files changed, 77 insertions(+) create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package.json create mode 100644 common/changes/@visactor/vtable/fix-issue-5027_2026-05-21-07-15.json diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc b/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc new file mode 100644 index 0000000000..7db0e7b3d6 --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc @@ -0,0 +1,23 @@ +# Rush uses this file to configure the NPM package registry during installation. It is applicable +# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", +# "rush update", and the "install-run.js" scripts. +# +# NOTE: The "rush publish" command uses .npmrc-publish instead. +# +# Before invoking the package manager, Rush will copy this file to the folder where installation +# is performed. The copied file will omit any config lines that reference environment variables +# that are undefined in that session; this avoids problems that would otherwise result due to +# a missing variable being replaced by an empty string. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# + +registry=https://registry.npmjs.org/ +always-auth=false diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag b/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag new file mode 100644 index 0000000000..0aae51e4a3 --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag @@ -0,0 +1,3 @@ +{ + "node": "24.11.0" +} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json new file mode 100644 index 0000000000..c810d8aeaf --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "pnpm-local-install", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pnpm-local-install", + "version": "0.0.0", + "dependencies": { + "pnpm": "10.7.0" + } + }, + "node_modules/pnpm": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.7.0.tgz", + "integrity": "sha512-a4Za1LYqHZhCth1nSjk5A7hx2SRJVPZSuIQsK1U8chdrJ49kxGPlLUD/+KujhcI1yMns9cx95P14uLttSWM6tg==", + "license": "MIT", + "bin": { + "pnpm": "bin/pnpm.cjs", + "pnpx": "bin/pnpx.cjs" + }, + "engines": { + "node": ">=18.12" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + } + } +} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json new file mode 100644 index 0000000000..fe8ad16943 --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "pnpm": "10.7.0" + }, + "description": "Temporary file generated by the Rush tool", + "name": "pnpm-local-install", + "private": true, + "version": "0.0.0" +} diff --git a/common/changes/@visactor/vtable/fix-issue-5027_2026-05-21-07-15.json b/common/changes/@visactor/vtable/fix-issue-5027_2026-05-21-07-15.json new file mode 100644 index 0000000000..7ee1609801 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5027_2026-05-21-07-15.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: avoid selection gap with frozen rows\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file From 0f438a23b8fba82ca9fc8cde4c1802759b590dd5 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 21 May 2026 15:19:56 +0800 Subject: [PATCH 3/3] chore: remove local rush artifacts --- .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc | 23 -------------- .../pnpm-10.7.0/last-install.flag | 3 -- .../pnpm-10.7.0/package-lock.json | 31 ------------------- .../node-v24.11.0/pnpm-10.7.0/package.json | 9 ------ 4 files changed, 66 deletions(-) delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package.json diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc b/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc deleted file mode 100644 index 7db0e7b3d6..0000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc +++ /dev/null @@ -1,23 +0,0 @@ -# Rush uses this file to configure the NPM package registry during installation. It is applicable -# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", -# "rush update", and the "install-run.js" scripts. -# -# NOTE: The "rush publish" command uses .npmrc-publish instead. -# -# Before invoking the package manager, Rush will copy this file to the folder where installation -# is performed. The copied file will omit any config lines that reference environment variables -# that are undefined in that session; this avoids problems that would otherwise result due to -# a missing variable being replaced by an empty string. -# -# * * * SECURITY WARNING * * * -# -# It is NOT recommended to store authentication tokens in a text file on a lab machine, because -# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, -# for example if the machine loses power. A safer practice is to pass the token via an -# environment variable, which can be referenced from .npmrc using ${} expansion. For example: -# -# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} -# - -registry=https://registry.npmjs.org/ -always-auth=false diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag b/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag deleted file mode 100644 index 0aae51e4a3..0000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag +++ /dev/null @@ -1,3 +0,0 @@ -{ - "node": "24.11.0" -} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json deleted file mode 100644 index c810d8aeaf..0000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "pnpm-local-install", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "pnpm-local-install", - "version": "0.0.0", - "dependencies": { - "pnpm": "10.7.0" - } - }, - "node_modules/pnpm": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.7.0.tgz", - "integrity": "sha512-a4Za1LYqHZhCth1nSjk5A7hx2SRJVPZSuIQsK1U8chdrJ49kxGPlLUD/+KujhcI1yMns9cx95P14uLttSWM6tg==", - "license": "MIT", - "bin": { - "pnpm": "bin/pnpm.cjs", - "pnpx": "bin/pnpx.cjs" - }, - "engines": { - "node": ">=18.12" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - } - } -} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json deleted file mode 100644 index fe8ad16943..0000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "pnpm": "10.7.0" - }, - "description": "Temporary file generated by the Rush tool", - "name": "pnpm-local-install", - "private": true, - "version": "0.0.0" -}