diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json b/common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json new file mode 100644 index 0000000000..3096575108 --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-plugins", + "comment": "fix: keep wheel events cancelable after restoring a rotated table (GitHub #5235)", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "biukam.w@gmail.com" +} diff --git a/packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts b/packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts new file mode 100644 index 0000000000..3fd7f61bda --- /dev/null +++ b/packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts @@ -0,0 +1,88 @@ +// @ts-nocheck +jest.mock('@visactor/vtable', () => { + const vrender = jest.requireActual('../../vtable/src/vrender'); + + return { + matrixAllocate: vrender.matrixAllocate, + transformPointForCanvas: vrender.transformPointForCanvas, + mapToCanvasPointForCanvas: vrender.mapToCanvasPointForCanvas, + registerGlobalEventTransformer: vrender.registerGlobalEventTransformer, + registerWindowEventTransformer: vrender.registerWindowEventTransformer, + vglobal: { + mapToCanvasPoint: jest.fn(), + setEventListenerTransformer: jest.fn() + }, + TABLE_EVENT_TYPE: { + INITIALIZED: 'initialized' + } + }; +}); + +import { vglobal } from '@visactor/vtable'; +import { cancelTransform } from '../src/rotate-table'; + +global.__VERSION__ = 'none'; + +describe('RotateTablePlugin cancel transform - issue #5235', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('keeps wheel preventDefault connected to the native event after canceling rotation', () => { + const tableElement = document.createElement('div'); + jest.spyOn(tableElement, 'getBoundingClientRect').mockReturnValue({ + x: 40, + y: 80, + left: 40, + top: 80, + right: 640, + bottom: 480, + width: 600, + height: 400, + toJSON: () => ({}) + }); + + const originalMapToCanvasPoint = vglobal.mapToCanvasPoint; + const windowSetEventListenerTransformer = jest.fn(); + let globalTransformer; + + jest.spyOn(vglobal, 'setEventListenerTransformer').mockImplementation(transformer => { + globalTransformer = transformer; + }); + + const table = { + rotateDegree: 90, + getElement: () => tableElement, + scenegraph: { + stage: { + window: { + setEventListenerTransformer: windowSetEventListenerTransformer + } + } + }, + pluginManager: { + getPluginByName: () => ({ + vglobal_mapToCanvasPoint: originalMapToCanvasPoint + }) + } + }; + + cancelTransform.call(table, document.createElement('div')); + + const nativeWheelEvent = new WheelEvent('wheel', { + cancelable: true, + clientX: 120, + clientY: 160, + deltaY: 40 + }); + const transformedWheelEvent = globalTransformer(nativeWheelEvent); + + transformedWheelEvent.preventDefault(); + + expect(transformedWheelEvent).toBe(nativeWheelEvent); + expect(nativeWheelEvent.defaultPrevented).toBe(true); + expect(windowSetEventListenerTransformer).toHaveBeenCalledTimes(1); + expect(table.rotateDegree).toBe(0); + expect(vglobal.mapToCanvasPoint).toBe(originalMapToCanvasPoint); + }); +}); diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 8c16ad6b42..0d522dd098 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -87,6 +87,10 @@ export const menus = [ path: 'rotate-table', name: 'rotate-table' }, + { + path: 'rotate-table', + name: 'issue-5235-rotate-wheel' + }, { path: 'table-series-number', name: 'table-series-number' diff --git a/packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts b/packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts new file mode 100644 index 0000000000..1858232ed1 --- /dev/null +++ b/packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts @@ -0,0 +1,132 @@ +import * as VTable from '@visactor/vtable'; +import { RotateTablePlugin } from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const records = Array.from({ length: 80 }, (_, index) => ({ + id: index + 1, + name: `Name ${index + 1}`, + email: `${index + 1}@example.com`, + city: `City ${index + 1}` +})); + +const columns: VTable.ColumnsDefine = [ + { field: 'id', title: 'ID', width: 80, sort: true }, + { field: 'name', title: 'Name', width: 180 }, + { field: 'email', title: 'Email', width: 220 }, + { field: 'city', title: 'City', width: 180 } +]; + +const removeToolbar = () => { + document.getElementById('issue5235Toolbar')?.remove(); + document.getElementById('issue5235Spacer')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const status = document.getElementById('issue5235Status'); + if (!status) { + return; + } + status.textContent = message; + status.style.color = pass ? '#237804' : '#cf1322'; +}; + +const getRotateDom = () => document.getElementById(CONTAINER_ID) as HTMLElement; + +const dispatchWheelAndCheckPrevented = () => { + const canvas = document.querySelector(`#${CONTAINER_ID} canvas`); + if (!canvas) { + return false; + } + + const rect = canvas.getBoundingClientRect(); + const wheelEvent = new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + clientX: rect.left + rect.width / 2, + clientY: rect.top + rect.height / 2, + deltaY: 80 + }); + + canvas.dispatchEvent(wheelEvent); + return wheelEvent.defaultPrevented; +}; + +export function createTable() { + removeToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '720px'; + container.style.height = '360px'; + container.style.margin = '24px auto'; + container.style.border = '1px solid #d9d9d9'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5235Toolbar'; + toolbar.style.cssText = 'display:flex;gap:8px;align-items:center;height:48px;font-size:12px;'; + toolbar.innerHTML = ` + + + + 预期:旋转再还原后,表格内 wheel 的原始事件仍会被 preventDefault。 + + `; + container.before(toolbar); + + const spacer = document.createElement('div'); + spacer.id = 'issue5235Spacer'; + spacer.style.cssText = 'height:900px;background:linear-gradient(#fff,#f5f5f5);'; + spacer.textContent = '页面滚动占位:用于观察表格内滚轮是否带动外层页面滚动。'; + container.after(spacer); + + const rotatePlugin = new RotateTablePlugin(); + const tableInstance = new VTable.ListTable(container, { + records, + columns, + widthMode: 'standard', + rowSeriesNumber: {}, + overscrollBehavior: 'none', + plugins: [rotatePlugin] + }); + + const rotateTable = () => { + const rotateDom = getRotateDom(); + const { width, height } = rotateDom.getBoundingClientRect(); + rotateDom.style.width = `${height}px`; + rotateDom.style.height = `${width}px`; + tableInstance.rotate90WithTransform?.(rotateDom); + setStatus('READY | 已旋转,请点击还原后检查滚轮。', true); + }; + + const restoreTable = () => { + const rotateDom = getRotateDom(); + const { width, height } = rotateDom.getBoundingClientRect(); + rotateDom.style.width = `${height}px`; + rotateDom.style.height = `${width}px`; + tableInstance.cancelTransform?.(rotateDom); + setStatus('READY | 已还原,请点击滚轮检查。', true); + }; + + const checkWheel = () => { + const prevented = dispatchWheelAndCheckPrevented(); + setStatus(`${prevented ? 'PASS' : 'FAIL'} | wheel.defaultPrevented=${prevented}`, prevented); + }; + + document.getElementById('issue5235Rotate')?.addEventListener('click', rotateTable); + document.getElementById('issue5235Restore')?.addEventListener('click', restoreTable); + document.getElementById('issue5235Check')?.addEventListener('click', checkWheel); + + (window as any).tableInstance = tableInstance; + (window as any).issue5235Run = () => { + rotateTable(); + restoreTable(); + checkWheel(); + return document.getElementById('issue5235Status')?.textContent; + }; + + const release = tableInstance.release.bind(tableInstance); + tableInstance.release = () => { + removeToolbar(); + release(); + }; +} diff --git a/packages/vtable-plugins/src/rotate-table.ts b/packages/vtable-plugins/src/rotate-table.ts index 95955bc00d..582c154a2d 100644 --- a/packages/vtable-plugins/src/rotate-table.ts +++ b/packages/vtable-plugins/src/rotate-table.ts @@ -154,7 +154,6 @@ export function cancelTransform(this: ListTable, rotateDom: HTMLElement) { }; const getMatrix = () => { const matrix = matrixAllocate.allocate(1, 0, 0, 1, 0, 0); - matrix.translate(x1, y1); return matrix; }; registerGlobalEventTransformer(vglobal, this.getElement(), getMatrix, getRect as any, transformPointForCanvas);