diff --git a/packages/s2-core/__tests__/README.md b/packages/s2-core/__tests__/README.md new file mode 100644 index 0000000000..71ef229c35 --- /dev/null +++ b/packages/s2-core/__tests__/README.md @@ -0,0 +1,15 @@ +### 调试单测 + +如果你想查看单测的运行结果,除了常规的 `pnpm core:test` 和 `pnpm react:test` 来运行测试之外,还可以 `可视化的调试单测(基于 jest-electron)`, 可以更快的发现单测的问题。 + +1. 选择单测 + +命令行运行 `pnpm core:start` 或者 `pnpm react:start` + +preview + +2. 查看结果 + +因为本质上就是一个浏览器,如果单测结果不符合预期,可以正常打断点进行调试,快速分析原因。 + +preview diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap index 31ea0d6095..cf6dc7c50d 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap @@ -256,7 +256,7 @@ Array [ }, Object { "field": "", - "id": "", + "id": "1", "value": "1", }, Object { @@ -430,7 +430,7 @@ Array [ ] `; -exports[`Facet Layout API Tests PivotSheet CornerCell #getCornerCell()s 1`] = ` +exports[`Facet Layout API Tests PivotSheet CornerCell #getCornerCells() 1`] = ` Array [ Object { "field": "province", @@ -694,7 +694,7 @@ exports[`Facet Layout API Tests PivotSheet SeriesNumberCell #getSeriesNumberCell Array [ Object { "field": "", - "id": "", + "id": "1", "value": "1", }, ] @@ -704,7 +704,7 @@ exports[`Facet Layout API Tests PivotSheet SeriesNumberCell #getSeriesNumberNode Array [ Object { "field": "", - "id": "", + "id": "1", "value": "1", }, ] diff --git a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts index 45dac87339..0b83b0e131 100644 --- a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts @@ -251,7 +251,10 @@ describe('PivotSheet Corner Tests', () => { const getCellSpy = jest.spyOn(s2, 'getCell').mockImplementation(() => { return { - getMeta: () => node, + getMeta: () => ({ + ...node, + cornerType: CornerNodeType.Row, + }), } as unknown as S2CellType; }); const selected = jest.fn(); @@ -286,7 +289,10 @@ describe('PivotSheet Corner Tests', () => { jest.spyOn(s2, 'getCell').mockImplementationOnce(() => { return { - getMeta: () => node, + getMeta: () => ({ + ...node, + cornerType: CornerNodeType.Row, + }), } as unknown as S2CellType; }); const selected = jest.fn(); @@ -308,7 +314,10 @@ describe('PivotSheet Corner Tests', () => { jest.spyOn(s2, 'showTooltipWithInfo').mockImplementationOnce(() => {}); jest.spyOn(s2, 'getCell').mockImplementationOnce(() => { return { - getMeta: () => node, + getMeta: () => ({ + ...node, + cornerType: CornerNodeType.Row, + }), } as unknown as S2CellType; }); diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts index 86f24da64e..e3fae89f97 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts @@ -233,7 +233,7 @@ describe('PivotSheet Brush Selection Scroll Tests', () => { ); await s2.render(); - await sleep(20); // wait for anthor loop; + await sleep(500); // wait for anthor loop; const rowCell = s2.facet.getRowCells()[0]; @@ -245,7 +245,7 @@ describe('PivotSheet Brush Selection Scroll Tests', () => { } as any); await emitBrushEvent(s2, 200, 200); - await sleep(500); + await sleep(1000); expect(s2.facet.getScrollOffset().scrollY).toBeGreaterThan(0); expect(s2.interaction.getCells()).not.toBeEmpty(); diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts index 6eb3aa1eda..51af0504ab 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts @@ -3,9 +3,12 @@ import { createPivotSheet, getContainer, } from 'tests/util/helpers'; -import { customRowGridSimpleFields } from '../data/custom-grid-simple-fields'; +import { + customColGridSimpleFields, + customRowGridSimpleFields, +} from '../data/custom-grid-simple-fields'; import { CustomGridData } from '../data/data-custom-grid'; -import { S2Event } from '@/common/constant'; +import { S2Event, SERIES_NUMBER_FIELD } from '@/common/constant'; import { CornerNodeType, type S2Options } from '@/common/interface'; import type { GEvent, HierarchyType, S2DataConfig } from '@/index'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; @@ -23,12 +26,12 @@ describe('Interaction Corner Cell Click Tests', () => { }); test.each(['grid', 'tree'] as HierarchyType[])( - 'should not selected row cell when col corner cell clicked for %s mode', + 'should selected row cell when row corner cell clicked for %s mode', async (hierarchyType) => { jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce( () => - createMockCellInfo('testId', { - cornerType: CornerNodeType.Col, + createMockCellInfo('city', { + cornerType: CornerNodeType.Row, }).mockCell, ); @@ -44,22 +47,30 @@ describe('Interaction Corner Cell Click Tests', () => { s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent); - expect(reset).toHaveBeenCalledTimes(1); - expect(s2.interaction.isSelectedState()).toBeFalsy(); + expect(reset).not.toHaveBeenCalled(); + expect(s2.interaction.getActiveCells()).toHaveLength( + s2.isHierarchyTreeType() ? 3 : 2, + ); + expect(s2.interaction.isSelectedState()).toBeTruthy(); }, ); test.each(['grid', 'tree'] as HierarchyType[])( - 'should selected row cell when row corner cell clicked for %s mode', + 'should selected custom row cell when row corner cell clicked for %s mode', async (hierarchyType) => { jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce( () => - createMockCellInfo('city', { + createMockCellInfo('a-1-1', { cornerType: CornerNodeType.Row, }).mockCell, ); - s2 = createPivotSheet({ + const customRowDataCfg: S2DataConfig = { + data: CustomGridData, + fields: customRowGridSimpleFields, + }; + + s2 = new PivotSheet(getContainer(), customRowDataCfg, { ...s2Options, hierarchyType, }); @@ -71,27 +82,65 @@ describe('Interaction Corner Cell Click Tests', () => { s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent); + const gridFields = ['a-1-1', 'a-1-2']; + const treeFields = [ + 'a-1', + 'a-1-1', + 'measure-1', + 'measure-2', + 'a-1-2', + 'a-2', + ]; + expect(reset).not.toHaveBeenCalled(); - expect(s2.interaction.getActiveCells()).toHaveLength( - s2.isHierarchyTreeType() ? 3 : 2, + expect( + s2.interaction.getActiveCells().map((cell) => cell.getMeta().field), + ).toEqual(s2.isHierarchyTreeType() ? treeFields : gridFields); + expect(s2.interaction.isSelectedState()).toBeTruthy(); + }, + ); + + test.each(['grid', 'tree'] as HierarchyType[])( + 'should selected col cell when col corner cell clicked for %s mode', + async (hierarchyType) => { + jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce( + () => + createMockCellInfo('type', { + cornerType: CornerNodeType.Col, + }).mockCell, ); + + s2 = createPivotSheet({ + ...s2Options, + hierarchyType, + }); + await s2.render(); + + const reset = jest + .spyOn(s2.interaction, 'reset') + .mockImplementationOnce(() => {}); + + s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent); + + expect(reset).not.toHaveBeenCalled(); + expect(s2.interaction.getActiveCells()).toHaveLength(1); expect(s2.interaction.isSelectedState()).toBeTruthy(); }, ); test.each(['grid', 'tree'] as HierarchyType[])( - 'should selected custom row cell when row corner cell clicked for %s mode', + 'should selected custom col cell when row corner cell clicked for %s mode', async (hierarchyType) => { jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce( () => - createMockCellInfo('a-1-1', { - cornerType: CornerNodeType.Row, + createMockCellInfo('a-1', { + cornerType: CornerNodeType.Col, }).mockCell, ); const customRowDataCfg: S2DataConfig = { data: CustomGridData, - fields: customRowGridSimpleFields, + fields: customColGridSimpleFields, }; s2 = new PivotSheet(getContainer(), customRowDataCfg, { @@ -106,20 +155,39 @@ describe('Interaction Corner Cell Click Tests', () => { s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent); - const gridFields = ['a-1-1', 'a-1-2']; - const treeFields = [ - 'a-1', - 'a-1-1', - 'measure-1', - 'measure-2', - 'a-1-2', - 'a-2', - ]; - expect(reset).not.toHaveBeenCalled(); expect( s2.interaction.getActiveCells().map((cell) => cell.getMeta().field), - ).toEqual(s2.isHierarchyTreeType() ? treeFields : gridFields); + ).toEqual(['a-1', 'a-2']); + expect(s2.interaction.isSelectedState()).toBeTruthy(); + }, + ); + + test.each(['grid', 'tree'] as HierarchyType[])( + 'should selected series cell when series corner cell clicked for %s mode', + async (hierarchyType) => { + jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce( + () => + createMockCellInfo(SERIES_NUMBER_FIELD, { + cornerType: CornerNodeType.Series, + }).mockCell, + ); + + s2 = createPivotSheet({ + ...s2Options, + seriesNumber: { enable: true }, + hierarchyType, + }); + await s2.render(); + + const reset = jest + .spyOn(s2.interaction, 'reset') + .mockImplementationOnce(() => {}); + + s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent); + + expect(reset).not.toHaveBeenCalled(); + expect(s2.interaction.getActiveCells()).toHaveLength(1); expect(s2.interaction.isSelectedState()).toBeTruthy(); }, ); diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts index c7763a0714..6cfd7af2e3 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts @@ -11,8 +11,9 @@ import { getSelectedSum, getTestTooltipData, } from '../util/interaction'; +import { CellType, InteractionStateName } from '../../src'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; -import type { S2Options } from '@/common/interface'; +import type { HierarchyType, S2Options } from '@/common/interface'; const s2Options: S2Options = { width: 600, @@ -22,6 +23,16 @@ const s2Options: S2Options = { }, }; +const highlightCellConfig: Array<{ + hierarchyType: HierarchyType; + stateName: InteractionStateName; +}> = [ + { hierarchyType: 'tree', stateName: InteractionStateName.HOVER }, + { hierarchyType: 'tree', stateName: InteractionStateName.SELECTED }, + { hierarchyType: 'grid', stateName: InteractionStateName.HOVER }, + { hierarchyType: 'grid', stateName: InteractionStateName.SELECTED }, +]; + describe('Interaction Multi Selection Tests', () => { let s2: SpreadSheet; @@ -197,4 +208,86 @@ describe('Interaction Multi Selection Tests', () => { ).toBeTruthy(); }); }); + + test.each(highlightCellConfig)( + 'should highlight relevancy header cell after selected data cell by %s mode', + async ({ hierarchyType, stateName }) => { + s2.setOptions({ + hierarchyType, + interaction: { + selectedCellHighlight: true, + hoverHighlight: true, + }, + seriesNumber: { enable: true }, + }); + await s2.render(false); + + const dataCell = s2.facet.getDataCells()[0]; + + s2.interaction.updateDataCellRelevantHeaderCells( + stateName, + dataCell.getMeta(), + ); + + expect(s2.interaction.getInteractedCells()).toHaveLength( + s2.isHierarchyTreeType() ? 4 : 5, + ); + }, + ); + + test.each(highlightCellConfig)( + 'should highlight relevancy row cell after selected data cell by %s mode', + async ({ hierarchyType, stateName }) => { + s2.setOptions({ + hierarchyType, + interaction: { + selectedCellHighlight: true, + hoverHighlight: true, + }, + seriesNumber: { enable: true }, + }); + await s2.render(false); + + const dataCell = s2.facet.getDataCells()[0]; + + s2.interaction.updateDataCellRelevantRowCells( + stateName, + dataCell.getMeta(), + ); + + const interactedCells = s2.interaction + .getInteractedCells() + .filter((cell) => cell.cellType === CellType.ROW_CELL); + + expect(interactedCells).toHaveLength(s2.isHierarchyTreeType() ? 2 : 3); + }, + ); + + test.each(highlightCellConfig)( + 'should highlight relevancy row cell after selected data cell by %s mode', + async ({ hierarchyType, stateName }) => { + s2.setOptions({ + hierarchyType, + interaction: { + selectedCellHighlight: true, + hoverHighlight: true, + }, + seriesNumber: { enable: true }, + }); + await s2.render(false); + + const dataCell = s2.facet.getDataCells()[0]; + + s2.interaction.updateDataCellRelevantColCells( + stateName, + dataCell.getMeta(), + ); + + const interactedCells = s2.interaction + .getInteractedCells() + .filter((cell) => cell.cellType === CellType.COL_CELL); + + expect(interactedCells).toHaveLength(2); + }, + ); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts index 8cf314bad5..1eea671955 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts @@ -99,7 +99,7 @@ describe('Facet Layout API Tests', () => { expect(mapNodes(s2.facet.getCornerNodes())).toMatchSnapshot(); }); - test('#getCornerCell()s', () => { + test('#getCornerCells()', () => { expect(mapCells(s2.facet.getCornerCells())).toMatchSnapshot(); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts index 2861bae844..0134c6e6c9 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts @@ -1,6 +1,6 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; import { createPivotSheet, getContainer } from 'tests/util/helpers'; -import { PivotSheet } from '../../src'; +import { CornerNodeType, PivotSheet } from '../../src'; import type { S2DataConfig, S2Options } from '@/common'; const s2Options: S2Options = { @@ -17,7 +17,7 @@ describe('SpreadSheet Tree Mode Tests', () => { }); afterEach(() => { - container?.remove(); + // container?.remove(); }); describe('Facet Tests', () => { @@ -90,5 +90,25 @@ describe('SpreadSheet Tree Mode Tests', () => { expect(cornerCell.getTextShapes()).toHaveLength(1); expect(cornerCell.isMultiLineText()).toBeFalsy(); }); + + // https://github.com/antvis/S2/issues/2563 + test('should render correctly tree icon position in row cell', async () => { + const s2 = createPivotSheet({ + ...s2Options, + width: 300, + seriesNumber: { + enable: true, + }, + }); + + await s2.render(); + + const [seriesNumberCell, rowCell] = s2.facet + .getCornerCells() + .filter((cell) => cell.getMeta().cornerType !== CornerNodeType.Col); + + expect(seriesNumberCell.getTreeIcon()).toBeFalsy(); + expect(rowCell.getTreeIcon()).toBeTruthy(); + }); }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts index 08168ba37f..cb3f451460 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts @@ -3,7 +3,11 @@ import { createMockCellInfo, sleep, } from 'tests/util/helpers'; -import { CellType, InteractionStateName, type Node } from '../../../../../src'; +import { + CornerNodeType, + InteractionStateName, + type Node, +} from '../../../../../src'; import { InterceptType, S2Event } from '@/common/constant'; import type { HierarchyType, S2Options } from '@/common/interface'; import type { GEvent } from '@/index'; @@ -14,7 +18,9 @@ jest.mock('@/interaction/event-controller'); describe('Interaction Corner Cell Click Tests', () => { let s2: SpreadSheet; - const mockCellInfo = createMockCellInfo('testId'); + const mockCellInfo = createMockCellInfo('testId', { + cornerType: CornerNodeType.Row, + }); let cornerCellClick: CornerCellClick; beforeEach(() => { @@ -68,7 +74,6 @@ describe('Interaction Corner Cell Click Tests', () => { { colIndex: -1, rowIndex: -1, - type: CellType.ROW_CELL, id: mockCellInfo.mockCellMeta['id'], }, ], diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts index 53a5c36dc8..29b29fc180 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts @@ -113,8 +113,13 @@ describe('Interaction Hover Tests', () => { }); test('should trigger data cell hover depend on separate config', async () => { - s2.facet.getColCells = jest.fn(); - s2.facet.getRowCells = jest.fn(); + s2.facet.getRowCells().forEach((cell) => { + jest.spyOn(cell, 'update').mockImplementationOnce(() => {}); + }); + + s2.facet.getColCells().forEach((cell) => { + jest.spyOn(cell, 'update').mockImplementationOnce(() => {}); + }); s2.setOptions({ interaction: { @@ -126,6 +131,7 @@ describe('Interaction Hover Tests', () => { }); s2.emit(S2Event.DATA_CELL_HOVER, { target: {} } as GEvent); + expect(s2.interaction.getState()).toEqual({ cells: [mockCellMeta], stateName: InteractionStateName.HOVER, @@ -138,8 +144,13 @@ describe('Interaction Hover Tests', () => { stateName: InteractionStateName.HOVER_FOCUS, }); - expect(s2.facet.getColCells).toHaveBeenCalled(); - expect(s2.facet.getRowCells).not.toHaveBeenCalled(); + s2.facet.getColCells().forEach((cell) => { + expect(cell.update).toHaveBeenCalled(); + }); + + s2.facet.getRowCells().forEach((cell) => { + expect(cell.update).not.toHaveBeenCalled(); + }); }); test('should not trigger data cell hover when hover cell not change', () => { diff --git a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts index 589b1c90d4..d6fc55570a 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts @@ -1,6 +1,6 @@ import type { Node } from '@/facet/layout/node'; import { - getActiveHoverRowColCells, + getActiveHoverHeaderCells, updateAllColHeaderCellState, } from '@/utils/interaction/hover-event'; import type { SpreadSheet } from '@/sheet-type/spread-sheet'; @@ -32,13 +32,13 @@ describe('Hover Event Utils Tests', () => { const cells = [ new ColCell({} as unknown as Node, {} as unknown as SpreadSheet), ]; - let result = getActiveHoverRowColCells('root[&]city', cells, false); + let result = getActiveHoverHeaderCells('root[&]city', cells, false); expect(result.map((cell) => cell.getMeta()?.id)).toStrictEqual([ 'root[&]city', ]); - result = getActiveHoverRowColCells('root[&]city', cells, true); + result = getActiveHoverHeaderCells('root[&]city', cells, true); expect(result.map((cell) => cell.getMeta()?.id)).toStrictEqual([ 'root[&]city', ]); diff --git a/packages/s2-core/src/cell/corner-cell.ts b/packages/s2-core/src/cell/corner-cell.ts index 4a41ced6d3..3b45b1eacd 100644 --- a/packages/s2-core/src/cell/corner-cell.ts +++ b/packages/s2-core/src/cell/corner-cell.ts @@ -54,10 +54,10 @@ export class CornerCell extends HeaderCell { } /** - * 绘制折叠展开的icon + * 绘制折叠展开的 icon */ protected drawTreeIcon() { - if (!this.showTreeIcon() || this.meta.cornerType === CornerNodeType.Col) { + if (!this.showTreeIcon()) { return; } @@ -191,8 +191,11 @@ export class CornerCell extends HeaderCell { } protected showTreeIcon() { - // 批量折叠或者展开的icon,只存在树状结构的第一个cell前 - return this.spreadsheet.isHierarchyTreeType() && this.meta?.x === 0; + // 批量折叠或者展开的 icon,渲染在行头对应的角头中 + return ( + this.spreadsheet.isHierarchyTreeType() && + this.meta.cornerType === CornerNodeType.Row + ); } protected getTreeIconWidth() { diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index d226657365..a2b3726865 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -178,10 +178,14 @@ export class DataCell extends BaseCell { } } + protected isDisableHover(cellMeta: CellMeta) { + return cellMeta.type !== CellType.DATA_CELL; + } + protected handleHover(cells: CellMeta[]) { - const currentHoverCell = first(cells) as CellMeta; + const hoverCellMeta = first(cells) as CellMeta; - if (currentHoverCell.type !== CellType.DATA_CELL) { + if (this.isDisableHover(hoverCellMeta)) { this.hideInteractionShape(); return; @@ -197,8 +201,8 @@ export class DataCell extends BaseCell { // 当视图内的 cell 行列 index 与 hover 的 cell 一致,绘制hover的十字样式 if ( - (currentCol && currentColIndex === currentHoverCell?.colIndex) || - (currentRow && currentRowIndex === currentHoverCell?.rowIndex) + (currentCol && currentColIndex === hoverCellMeta?.colIndex) || + (currentRow && currentRowIndex === hoverCellMeta?.rowIndex) ) { this.updateByState(InteractionStateName.HOVER); } else { @@ -211,9 +215,9 @@ export class DataCell extends BaseCell { // fix issue: https://github.com/antvis/S2/issues/1781 if ( - isEqual(currentHoverCell.id, id) && - isEqual(currentHoverCell.rowIndex, rowIndex) && - isEqual(currentHoverCell.colIndex, colIndex) + isEqual(hoverCellMeta.id, id) && + isEqual(hoverCellMeta.rowIndex, rowIndex) && + isEqual(hoverCellMeta.colIndex, colIndex) ) { this.updateByState(InteractionStateName.HOVER_FOCUS); } diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index 63fb383aa6..ae1bbe9224 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -442,6 +442,7 @@ export abstract class HeaderCell< CellType.CORNER_CELL, CellType.COL_CELL, CellType.ROW_CELL, + CellType.SERIES_NUMBER_CELL, ]); if (!first(cells)) { @@ -479,7 +480,7 @@ export abstract class HeaderCell< return [EXTRA_FIELD, EXTRA_COLUMN_FIELD].includes(this.meta.field); } - mappingValue( + public mappingValue( condition: Condition, ): ConditionMappingResult { const value = this.getMeta().value; diff --git a/packages/s2-core/src/cell/series-number-cell.ts b/packages/s2-core/src/cell/series-number-cell.ts index 79b20b51a8..73636e01b3 100644 --- a/packages/s2-core/src/cell/series-number-cell.ts +++ b/packages/s2-core/src/cell/series-number-cell.ts @@ -2,24 +2,12 @@ import type { PointLike } from '@antv/g'; import { CellType } from '../common/constant/interaction'; import type { AreaRange, FormatResult } from '../common/interface'; import { CellBorderPosition, CellClipBox } from '../common/interface/basic'; -import type { BaseHeaderConfig } from '../facet/header/interface'; -import type { Node } from '../facet/layout/node'; import { getHorizontalTextIconPosition } from '../utils/cell/cell'; import { adjustTextIconPositionWhileScrolling } from '../utils/cell/text-scrolling'; import { normalizeTextAlign } from '../utils/normalize'; -import { BaseCell } from './base-cell'; - -export class SeriesNumberCell extends BaseCell { - protected declare headerConfig: BaseHeaderConfig; - - protected handleRestOptions(...[headerConfig]: [BaseHeaderConfig]) { - this.headerConfig = { ...headerConfig }; - } - - public getHeaderConfig() { - return this.headerConfig || {}; - } +import { HeaderCell } from './header-cell'; +export class SeriesNumberCell extends HeaderCell { public get cellType() { return CellType.ROW_CELL; } @@ -30,13 +18,16 @@ export class SeriesNumberCell extends BaseCell { protected initCell(): void { this.drawBackgroundShape(); + this.drawInteractiveBgShape(); + this.drawInteractiveBorderShape(); this.drawBorders(); this.drawTextShape(); + this.update(); } - protected getBackgroundColor() { + public getBackgroundColor() { const { backgroundColor, backgroundColorOpacity } = - this.getStyle()?.cell || {}; + this.getCrossBackgroundColor(this.meta.rowIndex); return { backgroundColor, @@ -45,10 +36,6 @@ export class SeriesNumberCell extends BaseCell { }; } - public update(): void { - /** 序号单元格暂时没有交互的联动 */ - } - protected getTextStyle() { const textOverflowStyle = this.getCellTextWordWrapStyle( CellType.SERIES_NUMBER_CELL, @@ -116,15 +103,19 @@ export class SeriesNumberCell extends BaseCell { return { x: textX, y: textStart }; } - protected findFieldCondition() { + protected isBolderText() { + return false; + } + + public findFieldCondition() { return undefined; } - protected mappingValue() { + public mappingValue() { return undefined; } - protected getIconPosition(): PointLike { + public getIconPosition(): PointLike { return { x: 0, y: 0 }; } } diff --git a/packages/s2-core/src/cell/table-data-cell.ts b/packages/s2-core/src/cell/table-data-cell.ts index 13fa924d50..7f92f68f88 100644 --- a/packages/s2-core/src/cell/table-data-cell.ts +++ b/packages/s2-core/src/cell/table-data-cell.ts @@ -1,6 +1,7 @@ import { Frame } from '../facet/header/frame'; import { DataCell } from '../cell/data-cell'; import { + CellType, FrozenGroupType, KEY_GROUP_FROZEN_ROW_RESIZE_AREA, KEY_GROUP_ROW_RESIZE_AREA, @@ -18,6 +19,7 @@ import { } from '../utils/interaction/resize'; import { CustomRect, type SimpleBBox } from '../engine'; import type { FrozenFacet } from '../facet/frozen-facet'; +import type { CellMeta } from '../common'; import { BaseCell } from './base-cell'; export class TableDataCell extends DataCell { @@ -160,4 +162,8 @@ export class TableDataCell extends DataCell { ), ); } + + protected isDisableHover(cellMeta: CellMeta) { + return cellMeta?.type === CellType.COL_CELL; + } } diff --git a/packages/s2-core/src/facet/header/util.ts b/packages/s2-core/src/facet/header/util.ts index 8da2016ef2..dbcc00a796 100644 --- a/packages/s2-core/src/facet/header/util.ts +++ b/packages/s2-core/src/facet/header/util.ts @@ -1,23 +1,7 @@ -import { isNil } from 'lodash'; import type { SpreadSheet } from '../../sheet-type'; import type { Hierarchy } from '../layout/hierarchy'; import { Node } from '../layout/node'; -export const getCellPadding = () => { - const padding = [12, 4, 12, 4]; - const left = isNil(padding[3]) ? 0 : padding[3]; - const right = isNil(padding[1]) ? 0 : padding[1]; - const top = isNil(padding[0]) ? 0 : padding[0]; - const bottom = isNil(padding[2]) ? 0 : padding[2]; - - return { - left, - right, - top, - bottom, - }; -}; - export const getSeriesNumberNodes = ( rowsHierarchy: Hierarchy, seriesNumberWidth: number, @@ -27,11 +11,12 @@ export const getSeriesNumberNodes = ( const rootNodes = rowsHierarchy.getNodes(0); return rootNodes.map((node: Node, idx: number) => { + const value = `${idx + 1}`; const sNode = new Node({ - id: '', + id: value, field: '', rowIndex: idx, - value: `${idx + 1}`, + value, }); sNode.x = node.x; diff --git a/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts index 316e5c1dd8..c22d43ed3b 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts @@ -24,58 +24,49 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement { private bindCornerCellClick() { this.spreadsheet.on(S2Event.CORNER_CELL_CLICK, (event) => { - const { interaction } = this.spreadsheet; const cornerCell = this.spreadsheet.getCell(event.target); if (!cornerCell) { return; } - // 获取当前角头所对应那一列的行头单元格节点 const cornerCellMeta = cornerCell.getMeta() as Node; - // TODO: 序号/列角头的交互待拓展 - if ( - [CornerNodeType.Series, CornerNodeType.Col].includes( - cornerCellMeta?.cornerType!, - ) - ) { - interaction.reset(); + switch (cornerCellMeta?.cornerType) { + case CornerNodeType.Row: + this.onRowCornerClick(cornerCellMeta?.field, event); - return; + break; + case CornerNodeType.Col: + this.onColCornerClick(cornerCellMeta?.field, event); + + break; + case CornerNodeType.Series: + this.onSeriesCornerClick(cornerCellMeta?.field, event); + + break; + default: + break; } + }); + } - const rowNodes = this.getSelectedRowNodes(cornerCellMeta?.field!); - const sample = rowNodes[0]?.belongsCell; - const cells = this.getRowCellMetas(rowNodes); + private onRowCornerClick(field: string, event: CanvasEvent) { + const rowNodes = this.getSelectedRowNodes(field); - if (sample && interaction.isSelectedCell(sample)) { - interaction.reset(); - this.spreadsheet.emit( - S2Event.GLOBAL_SELECTED, - interaction.getActiveCells(), - ); + this.selectCells(rowNodes, event); + } - return; - } + private onColCornerClick(field: string, event: CanvasEvent) { + const colNodes = this.getSelectedColNodes(field); - if (isEmpty(rowNodes) || isEmpty(cells)) { - return; - } + this.selectCells(colNodes, event); + } - interaction.addIntercepts([InterceptType.HOVER]); - interaction.changeState({ - cells, - stateName: InteractionStateName.SELECTED, - }); - interaction.highlightNodes(rowNodes); + private onSeriesCornerClick(field: string, event: CanvasEvent) { + const seriesNodes = this.spreadsheet.facet.getSeriesNumberNodes(); - this.showTooltip(event); - this.spreadsheet.emit( - S2Event.GLOBAL_SELECTED, - interaction.getActiveCells(), - ); - }); + this.selectCells(seriesNodes, event); } private getSelectedRowNodes(field: string) { @@ -96,18 +87,64 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement { return facet.getRowNodes(sampleNode?.level); } - private getRowCellMetas(nodes: Node[]): CellMeta[] { + private getSelectedColNodes(field: string) { + const { facet } = this.spreadsheet; + + if (!this.spreadsheet.isCustomColumnFields()) { + return facet.getColNodesByField(field); + } + + // 自定义列头 field 都是独立的, 需要根据 level 区查找. + const sampleNode = facet.getColNodesByField(field)[0]; + + return facet.getColNodes(sampleNode?.level); + } + + private getCellMetas(nodes: Node[], cellType: CellType): CellMeta[] { return nodes.map((node) => { return { id: node.id, // 选中角头而高亮的行头, 不需要联动数值单元格, 所以索引设置为 -1 colIndex: -1, rowIndex: -1, - type: CellType.ROW_CELL, + type: cellType, }; }); } + private selectCells(nodes: Node[], event: CanvasEvent) { + const { interaction } = this.spreadsheet; + const sample = nodes[0]?.belongsCell; + const cells = this.getCellMetas(nodes, sample?.cellType!); + + if (sample && interaction.isSelectedCell(sample)) { + interaction.reset(); + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); + + return; + } + + if (isEmpty(nodes) || isEmpty(cells)) { + return; + } + + interaction.addIntercepts([InterceptType.HOVER]); + interaction.changeState({ + cells, + stateName: InteractionStateName.SELECTED, + }); + interaction.highlightNodes(nodes); + + this.showTooltip(event); + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); + } + private showTooltip(event: CanvasEvent) { // 角头的选中是维值, 不需要计算数值总和, 显示 [`xx 项已选中`] 即可 const selectedData = this.spreadsheet.interaction.getActiveCells(); diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts index ca459b8069..afc117b292 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts @@ -1,5 +1,4 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; -import { forEach } from 'lodash'; import type { DataCell } from '../../../cell/data-cell'; import { InteractionStateName, @@ -13,16 +12,14 @@ import type { ViewMetaData, } from '../../../common/interface'; import { - getCellMeta, afterSelectDataCells, - getRowCellForSelectedCell, + getCellMeta, } from '../../../utils/interaction/select-event'; import { getTooltipOptions, getTooltipVisibleOperator, } from '../../../utils/tooltip'; import { BaseEvent, type BaseEventImplement } from '../../base-event'; -import { updateAllColHeaderCellState } from '../../../utils/interaction'; export class DataCellClick extends BaseEvent implements BaseEventImplement { public bindEvents() { @@ -33,7 +30,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event: CanvasEvent) => { event.stopPropagation(); - const { interaction, facet } = this.spreadsheet; + const { interaction } = this.spreadsheet; interaction.clearHoverTimer(); @@ -47,7 +44,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { return; } - const cell = this.spreadsheet.getCell(event.target) as DataCell; + const cell = this.spreadsheet.getCell(event.target)!; const meta = cell.getMeta(); if (!meta) { @@ -80,29 +77,10 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { this.showTooltip(event, meta); // 点击单元格,高亮对应的行头、列头 - const { rowId, colId, spreadsheet } = meta; - const { colHeader, rowHeader } = interaction.getSelectedCellHighlight(); - - if (colHeader) { - updateAllColHeaderCellState( - colId, - facet.getColCells(), - InteractionStateName.SELECTED, - ); - } - - if (rowHeader) { - if (rowId) { - const allRowHeaderCells = getRowCellForSelectedCell( - meta, - spreadsheet, - ); - - forEach(allRowHeaderCells, (rowCell) => { - rowCell.updateByState(InteractionStateName.SELECTED); - }); - } - } + interaction.updateDataCellRelevantHeaderCells( + InteractionStateName.SELECTED, + meta, + ); }); } diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts index 8874c90730..9045c75758 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts @@ -29,6 +29,7 @@ import { getTooltipVisibleOperator, mergeCellInfo, } from '../../../utils/tooltip'; +import { SeriesNumberCell } from '../../../cell'; import type { ViewMeta } from './../../../common/interface/basic'; export class RowColumnClick extends BaseEvent implements BaseEventImplement { @@ -98,10 +99,13 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { const { interaction, options } = this.spreadsheet; const cell = this.spreadsheet.getCell(event.target)!; + if (cell instanceof SeriesNumberCell) { + return; + } + const { multiSelection: enableMultiSelection } = options.interaction!; // 关闭了多选就算按下了 Ctrl/Commend, 行/列也按单选处理 const isMultiSelection = !!(enableMultiSelection && this.isMultiSelection); - const success = interaction.selectHeaderCell({ cell, isMultiSelection, diff --git a/packages/s2-core/src/interaction/base-interaction/hover.ts b/packages/s2-core/src/interaction/base-interaction/hover.ts index 92ae0bb175..7e8bb41256 100644 --- a/packages/s2-core/src/interaction/base-interaction/hover.ts +++ b/packages/s2-core/src/interaction/base-interaction/hover.ts @@ -1,5 +1,5 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; -import { forEach, isBoolean, isEmpty } from 'lodash'; +import { isBoolean, isEmpty } from 'lodash'; import { S2Event } from '../../common/constant'; import { HOVER_FOCUS_DURATION, @@ -12,56 +12,17 @@ import type { TooltipOptions, ViewMeta, } from '../../common/interface'; -import { - getActiveHoverRowColCells, - updateAllColHeaderCellState, -} from '../../utils/interaction/hover-event'; +import type { Node } from '../../facet/layout/node'; import { getCellMeta } from '../../utils/interaction/select-event'; import { BaseEvent, type BaseEventImplement } from '../base-event'; -import type { Node } from '../../facet/layout/node'; /** * @description Hover event for data cells, row cells and col cells */ export class HoverEvent extends BaseEvent implements BaseEventImplement { public bindEvents() { - this.bindCornerCellHover(); this.bindDataCellHover(); - this.bindRowCellHover(); - this.bindColCellHover(); - } - - public updateRowColCells(meta: ViewMeta | Node) { - const { rowId, colId } = meta; - const { facet, interaction } = this.spreadsheet; - - updateAllColHeaderCellState( - colId, - facet.getColCells(), - InteractionStateName.HOVER, - ); - const { rowHeader, colHeader } = interaction.getHoverHighlight(); - - if (colHeader) { - updateAllColHeaderCellState( - colId, - facet.getColCells(), - InteractionStateName.HOVER, - ); - } - - if (rowHeader && rowId) { - // update rowHeader cells - const allRowHeaderCells = getActiveHoverRowColCells( - rowId, - facet.getRowCells(), - this.spreadsheet.isHierarchyTreeType(), - ); - - forEach(allRowHeaderCells, (cell) => { - cell.updateByState(InteractionStateName.HOVER); - }); - } + this.bindHeaderCellHover(); } /** @@ -75,7 +36,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { return; } - const meta = cell.getMeta(); + const meta = cell.getMeta() as ViewMeta; const { interaction } = this.spreadsheet; const { interaction: interactionOptions } = this.spreadsheet.options; const { hoverFocus } = interactionOptions!; @@ -99,23 +60,20 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { }; if (interactionOptions?.hoverHighlight) { - const { rowHeader, colHeader } = interaction.getHoverHighlight(); - - if (rowHeader || colHeader) { - // highlight all the row and column cells which the cell belongs to - this.updateRowColCells(meta); - } + interaction.updateDataCellRelevantHeaderCells( + InteractionStateName.HOVER, + meta, + ); } const data = this.getCellData(meta, onlyShowCellText); this.spreadsheet.showTooltipWithInfo(event, data, options); }; - let hoverFocusDuration = HOVER_FOCUS_DURATION; - if (!isBoolean(hoverFocus)) { - hoverFocusDuration = hoverFocus?.duration ?? HOVER_FOCUS_DURATION; - } + const hoverFocusDuration = !isBoolean(hoverFocus) + ? hoverFocus?.duration ?? HOVER_FOCUS_DURATION + : HOVER_FOCUS_DURATION; if (hoverFocusDuration === 0) { handleHoverFocus(); @@ -230,12 +188,10 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { }); if (interactionOptions?.hoverHighlight) { - const { rowHeader, colHeader } = interaction.getHoverHighlight(); - - if (rowHeader || colHeader) { - // highlight all the row and column cells which the cell belongs to - this.updateRowColCells(meta); - } + interaction.updateDataCellRelevantHeaderCells( + InteractionStateName.HOVER, + meta, + ); } if (interactionOptions?.hoverFocus) { @@ -244,21 +200,15 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { }); } - public bindRowCellHover() { - this.spreadsheet.on(S2Event.ROW_CELL_HOVER, (event: CanvasEvent) => { - this.handleHeaderHover(event); - }); - } - - public bindColCellHover() { - this.spreadsheet.on(S2Event.COL_CELL_HOVER, (event: CanvasEvent) => { - this.handleHeaderHover(event); - }); - } - - public bindCornerCellHover() { - this.spreadsheet.on(S2Event.CORNER_CELL_HOVER, (event: CanvasEvent) => { - this.handleHeaderHover(event); + public bindHeaderCellHover() { + [ + S2Event.ROW_CELL_HOVER, + S2Event.COL_CELL_HOVER, + S2Event.CORNER_CELL_HOVER, + ].forEach((eventName) => { + this.spreadsheet.on(eventName, (event: CanvasEvent) => { + this.handleHeaderHover(event); + }); }); } } diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index 5dd1be12cc..aacf04f59d 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -1,5 +1,5 @@ import { concat, find, forEach, isBoolean, isEmpty, isNil, map } from 'lodash'; -import type { MergedCell } from '../cell'; +import { type MergedCell } from '../cell'; import { CellType, INTERACTION_STATE_INFO_KEY, @@ -9,8 +9,8 @@ import { S2Event, } from '../common/constant'; import type { - BrushSelectionOptions, BrushSelectionInfo, + BrushSelectionOptions, CellMeta, CustomInteraction, InteractionCellHighlightOptions, @@ -19,12 +19,20 @@ import type { MergedCellInfo, S2CellType, SelectHeaderCellInfo, + ViewMeta, } from '../common/interface'; import type { Node } from '../facet/layout/node'; import type { SpreadSheet } from '../sheet-type'; import { hideColumnsByThunkGroup } from '../utils/hide-columns'; +import { + getActiveHoverHeaderCells, + updateAllColHeaderCellState, +} from '../utils/interaction/hover-event'; import { mergeCell, unmergeCell } from '../utils/interaction/merge-cell'; -import { getCellMeta } from '../utils/interaction/select-event'; +import { + getCellMeta, + getRowCellForSelectedCell, +} from '../utils/interaction/select-event'; import { clearState, setState } from '../utils/interaction/state-controller'; import { isMobile } from '../utils/is-mobile'; import type { BaseEvent } from './base-event'; @@ -629,4 +637,64 @@ export class RootInteraction { colCell, }; } + + public updateDataCellRelevantHeaderCells( + stateName: InteractionStateName, + meta: ViewMeta, + ) { + this.updateDataCellRelevantColCells(stateName, meta); + this.updateDataCellRelevantRowCells(stateName, meta); + } + + public updateDataCellRelevantRowCells( + stateName: InteractionStateName, + meta: ViewMeta, + ) { + const { rowId } = meta; + const { facet, interaction } = this.spreadsheet; + const isHoverState = stateName === InteractionStateName.HOVER; + const { rowHeader } = isHoverState + ? interaction.getHoverHighlight() + : interaction.getSelectedCellHighlight(); + + if (rowHeader && rowId) { + const activeRowCells = isHoverState + ? getActiveHoverHeaderCells( + rowId, + facet.getRowCells(), + this.spreadsheet.isHierarchyTreeType(), + ) + : getRowCellForSelectedCell(meta, this.spreadsheet); + + const activeSeriesNumberCells = facet + .getSeriesNumberCells() + .filter((seriesNumberCell) => { + return activeRowCells.find( + (rowCell) => rowCell.getMeta().y === seriesNumberCell.getMeta().y, + ); + }); + + const activeHeaderCells = [...activeSeriesNumberCells, ...activeRowCells]; + + forEach(activeHeaderCells, (cell) => { + cell.updateByState(stateName); + }); + } + } + + public updateDataCellRelevantColCells( + stateName: InteractionStateName, + meta: ViewMeta, + ) { + const { colId } = meta; + const { facet, interaction } = this.spreadsheet; + const { colHeader } = + stateName === InteractionStateName.HOVER + ? interaction.getHoverHighlight() + : interaction.getSelectedCellHighlight(); + + if (colHeader && colId) { + updateAllColHeaderCellState(colId, facet.getColCells(), stateName); + } + } } diff --git a/packages/s2-core/src/utils/interaction/hover-event.ts b/packages/s2-core/src/utils/interaction/hover-event.ts index a2bdd7549b..139983aa08 100644 --- a/packages/s2-core/src/utils/interaction/hover-event.ts +++ b/packages/s2-core/src/utils/interaction/hover-event.ts @@ -1,23 +1,23 @@ import { filter, forEach } from 'lodash'; -import type { ColCell, RowCell } from '../../cell'; -import { NODE_ID_SEPARATOR, InteractionStateName } from '../../common/constant'; +import type { ColCell, HeaderCell } from '../../cell'; +import { InteractionStateName, NODE_ID_SEPARATOR } from '../../common/constant'; import { generateId } from '../layout/generate-id'; /** * @description Return all the row cells or column cells which are needed to be highlighted. * @param id rowId or colId * @param headerCells all the rowHeader cells or all the colHeader cells - * @param isRowInHierarchyTreeType The tree mode will only highlight the leaf nodes at the head of the row + * @param isHierarchyTree The tree mode will only highlight the leaf nodes at the head of the row */ -export const getActiveHoverRowColCells = ( +export const getActiveHoverHeaderCells = ( id: string, - headerCells: (ColCell | RowCell)[], - isRowInHierarchyTreeType?: boolean, + headerCells: HeaderCell[], + isHierarchyTree?: boolean, ) => { let allHeaderIds: string[]; const ids = id.split(NODE_ID_SEPARATOR); - if (isRowInHierarchyTreeType) { + if (isHierarchyTree) { allHeaderIds = [id]; } else { allHeaderIds = [generateId(ids[0], ids[1])]; @@ -26,7 +26,7 @@ export const getActiveHoverRowColCells = ( } } - const allHeaderCells = filter(headerCells, (cell: ColCell | RowCell) => + const allHeaderCells = filter(headerCells, (cell) => allHeaderIds.includes(cell.getMeta()?.id), ); @@ -39,7 +39,7 @@ export const updateAllColHeaderCellState = ( stateName: InteractionStateName, ) => { if (colId) { - const allColHeaderCells = getActiveHoverRowColCells(colId, colHeaderCells); + const allColHeaderCells = getActiveHoverHeaderCells(colId, colHeaderCells); forEach(allColHeaderCells, (cell) => { cell.updateByState(stateName); diff --git a/packages/s2-core/src/utils/interaction/select-event.ts b/packages/s2-core/src/utils/interaction/select-event.ts index b94893437d..b88ab0f515 100644 --- a/packages/s2-core/src/utils/interaction/select-event.ts +++ b/packages/s2-core/src/utils/interaction/select-event.ts @@ -1,5 +1,5 @@ -import { forEach, reduce, uniqBy } from 'lodash'; -import { ColCell, RowCell, TableSeriesNumberCell } from '../../cell'; +import { reduce, uniqBy } from 'lodash'; +import { HeaderCell, TableSeriesNumberCell } from '../../cell'; import { CellType, InteractionKeyboardKey, @@ -15,10 +15,7 @@ import type { import type { Node } from '../../facet/layout/node'; import type { SpreadSheet } from '../../sheet-type'; import { getDataCellId } from '../cell/data-cell'; -import { - getActiveHoverRowColCells, - updateAllColHeaderCellState, -} from './hover-event'; +import { getActiveHoverHeaderCells } from './hover-event'; type HeaderGetter = { getter: typeof getRowHeaderByCellId; @@ -82,7 +79,7 @@ export function getRangeIndex( export function getRowCellForSelectedCell( meta: ViewMeta, spreadsheet: SpreadSheet, -): (ColCell | RowCell | TableSeriesNumberCell)[] { +): HeaderCell[] { const { facet, options } = spreadsheet; if (spreadsheet.isTableMode()) { @@ -99,35 +96,16 @@ export function getRowCellForSelectedCell( result.push(rowCell); } - return result; + return result as unknown as HeaderCell[]; } - return getActiveHoverRowColCells( + return getActiveHoverHeaderCells( meta.rowId!, facet.getRowCells(), spreadsheet.isHierarchyTreeType(), ); } -export function updateRowColCells(meta: ViewMeta) { - const { rowId, colId, spreadsheet } = meta; - const { facet } = spreadsheet; - - updateAllColHeaderCellState( - colId!, - facet.getColCells(), - InteractionStateName.SELECTED, - ); - - if (rowId) { - const allRowHeaderCells = getRowCellForSelectedCell(meta, spreadsheet); - - forEach(allRowHeaderCells, (cell) => { - cell.updateByState(InteractionStateName.SELECTED); - }); - } -} - export const getRowHeaderByCellId = (cellId: string, s2: SpreadSheet): Node[] => s2.facet.getRowNodes().filter((node: Node) => cellId.includes(node.id)); diff --git a/packages/s2-react/__tests__/README.md b/packages/s2-react/__tests__/README.md new file mode 100644 index 0000000000..71ef229c35 --- /dev/null +++ b/packages/s2-react/__tests__/README.md @@ -0,0 +1,15 @@ +### 调试单测 + +如果你想查看单测的运行结果,除了常规的 `pnpm core:test` 和 `pnpm react:test` 来运行测试之外,还可以 `可视化的调试单测(基于 jest-electron)`, 可以更快的发现单测的问题。 + +1. 选择单测 + +命令行运行 `pnpm core:start` 或者 `pnpm react:start` + +preview + +2. 查看结果 + +因为本质上就是一个浏览器,如果单测结果不符合预期,可以正常打断点进行调试,快速分析原因。 + +preview diff --git a/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx b/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx index 2a79130223..6974a4c28a 100644 --- a/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx +++ b/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx @@ -16,6 +16,5 @@ describe('Pagination Component Tests', () => { expect(result.asFragment()).toMatchSnapshot(); expect(screen.getByText('共计20条')).toBeDefined(); - expect(screen.getByText('1 / page')).toBeDefined(); }); }); diff --git a/packages/s2-react/playground/README.md b/packages/s2-react/playground/README.md new file mode 100644 index 0000000000..e776e0dce2 --- /dev/null +++ b/packages/s2-react/playground/README.md @@ -0,0 +1,15 @@ +### 开发与调试 + +根目录运行 `pnpm react:playground` 来运行 `S2`, 可用于调试 `@antv/s2` 和 `@antv/s2-react`, 提供了一些常用的图表场景和配置。 + +![playground](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*6t8RRbg5x_kAAAAAAAAAAAAADmJ7AQ/original) + +S2 基于 `AntV/G` 渲染引擎绘制,如果想像 DOM 一样调试的话,可以安装 [G 开发者工具](https://g.antv.antgroup.com/api/devtools/g-devtools) + +1. 访问 `chrome://extensions/` 安装后 + +![extensions](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*RTYXTpb3WuIAAAAAAAAAAAAADmJ7AQ/original) + +2. 开始调试 + +![dev-tool](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sP9eQaWxDpcAAAAAAAAAAAAADmJ7AQ/original) diff --git a/packages/s2-react/playground/config.tsx b/packages/s2-react/playground/config.tsx index f1a82d3f02..def059f443 100644 --- a/packages/s2-react/playground/config.tsx +++ b/packages/s2-react/playground/config.tsx @@ -316,6 +316,9 @@ export const s2Options: SheetComponentOptions = { width: 800, height: 600, hierarchyType: 'grid', + seriesNumber: { + enable: true, + }, transformCanvasConfig() { return { supportsCSSTransform: true, @@ -338,6 +341,8 @@ export const s2Options: SheetComponentOptions = { withHeader: true, }, hoverAfterScroll: true, + hoverHighlight: true, + selectedCellHighlight: true, selectedCellMove: true, rangeSelection: true, // 防止 mac 触控板横向滚动触发浏览器返回, 和移动端下拉刷新 diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index a221cff2bf..c401a5565d 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -82,7 +82,7 @@ const onSheetMounted = (s2: SpreadSheet) => { // @ts-ignore window.s2 = s2; // @ts-ignore - window.g_instances = [s2.container]; + window.__g_instances__ = [s2.container]; }; const CustomTooltip = () => ( @@ -1289,7 +1289,7 @@ function MainLayout() { }} /> - + + + { + updateOptions({ + interaction: { + selectedCellHighlight: checked, + }, + }); + }} + /> + void` | | isSelectedCell | 是否是选中的单元格 | (cell: [S2CellType](#s2celltype)) => void | | isActiveCell | 是否是激活的单元格 | (cell: [S2CellType](#s2celltype)) => void | -| getCells | 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视范围内的单元格 | `() => Partial[]` | +| getCells | 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视范围内的单元格 | () => Partial<[ViewMeta](#viewmeta)>[] | | getActiveCells | 获取当前在可视区域的单元格实例 | `() => S2CellType[]` | | clearStyleIndependent | 清除单元格样式 | `() => void` | | getUnSelectedDataCells | 获取可视区域内选中的数值单元格 | `() => DataCell[]` | @@ -48,6 +48,9 @@ s2.interaction.reset() | hasIntercepts | 是否有指定拦截的交互 | (interceptTypes: [InterceptType](#intercepttype)[]) => boolean | | removeIntercepts | 移除指定交互拦截 | (interceptTypes: [InterceptType](#intercepttype)[]) => void | | highlightNodes | 高亮节点对应的单元格 | (nodes: [Node](/docs/api/basic-class/node)[]) => void | +| updateDataCellRelevantHeaderCells | 高亮数值单元格和所对应行列单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void | +| updateDataCellRelevantRowCells | 高亮数值单元格和所对应行头单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void | +| updateDataCellRelevantColCells | 高亮数值单元格和所对应列头单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void | diff --git a/s2-site/docs/manual/advanced/custom/custom-icon.zh.md b/s2-site/docs/manual/advanced/custom/custom-icon.zh.md index e1195580b1..a34f8ba18f 100644 --- a/s2-site/docs/manual/advanced/custom/custom-icon.zh.md +++ b/s2-site/docs/manual/advanced/custom/custom-icon.zh.md @@ -70,7 +70,7 @@ const s2Options = { } ``` -也可以覆盖内置 `icon`, 例如自定义树状表格收起展开 `icon` +也可以覆盖内置 `icon`, 例如自定义树状表格收起展开 `icon`. [查看示例](/examples/custom/custom-icon/#custom-tree-icon) ``` ts const s2Options = { diff --git a/s2-site/docs/manual/contribution.zh.md b/s2-site/docs/manual/contribution.zh.md index 3f4cb66920..7b08452d41 100644 --- a/s2-site/docs/manual/contribution.zh.md +++ b/s2-site/docs/manual/contribution.zh.md @@ -53,18 +53,39 @@ tag: Updated ## ⌨️ 本地开发 -:::info{title="提示"} + -我们使用 `pnpm` 作为包管理 +### 调试功能 -```bash -npm i -g pnpm -``` +根目录运行 `pnpm react:playground` 来运行 `S2`, 可用于调试 `@antv/s2` 和 `@antv/s2-react`, 提供了一些常用的图表场景和配置。 -推荐本地运行 `pnpm react:playground` 来调试 `@antv/s2` 和 `@antv/s2-react` -::: +preview - +S2 基于 `AntV/G` 渲染引擎绘制,如果想像 DOM 一样调试的话,可以安装 [G 开发者工具](https://g.antv.antgroup.com/api/devtools/g-devtools) + +1. 访问 `chrome://extensions/` 安装后 + +preview + +1. 开始调试 + +preview + +### 调试单测 + +如果你想查看单测的运行结果,除了常规的 `pnpm core:test` 和 `pnpm react:test` 来运行测试之外,还可以 `可视化的调试单测(基于 jest-electron)`, 可以更快的发现单测的问题。 + +1. 选择单测 + +命令行运行 `pnpm core:start` 或者 `pnpm react:start` + +preview + +2. 查看结果 + +因为本质上就是一个浏览器,如果单测结果不符合预期,可以正常打断点进行调试,快速分析原因。 + +preview ## 📦 版本 diff --git a/s2-site/examples/custom/custom-icon/demo/custom-tree-icon.ts b/s2-site/examples/custom/custom-icon/demo/custom-tree-icon.ts new file mode 100644 index 0000000000..62d8d50678 --- /dev/null +++ b/s2-site/examples/custom/custom-icon/demo/custom-tree-icon.ts @@ -0,0 +1,39 @@ +import { PivotSheet, S2Options, S2DataConfig } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/cd9814d0-6dfa-42a6-8455-5a6bd0ff93ca.json', +) + .then((res) => res.json()) + .then(async (res) => { + const container = document.getElementById('container'); + const s2DataConfig: S2DataConfig = { + fields: { + rows: ['province', 'city'], + columns: ['type', 'sub_type'], + values: ['number'], + }, + meta: res.meta, + data: res.data, + }; + + const s2Options: S2Options = { + width: 600, + height: 480, + hierarchyType: 'tree', + // 覆盖掉默认的展开 (Plus) 收起 (Minus) 图标 + customSVGIcons: [ + { + name: 'Plus', + svg: 'https://gw.alipayobjects.com/zos/antfincdn/kXgP1pnClS/plus.svg', + }, + { + name: 'Minus', + svg: 'https://gw.alipayobjects.com/zos/antfincdn/2aWYZ7%26rQF/minus-circle.svg', + }, + ], + }; + + const s2 = new PivotSheet(container, s2DataConfig, s2Options); + + await s2.render(); + }); diff --git a/s2-site/examples/custom/custom-icon/demo/meta.json b/s2-site/examples/custom/custom-icon/demo/meta.json index cedecee50f..fd13d08749 100644 --- a/s2-site/examples/custom/custom-icon/demo/meta.json +++ b/s2-site/examples/custom/custom-icon/demo/meta.json @@ -35,6 +35,15 @@ "en": "Custom display condition" }, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Eb5CSq0nszQAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "custom-tree-icon.ts", + "title": { + "zh": "自定义树状模式展开收起图标", + "en": "Custom tree icon" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EjLvQ6LrFOEAAAAAAAAAAAAADmJ7AQ/original", + "new": true } ] } diff --git a/s2-site/examples/interaction/basic/demo/corner-cell-click-selection.ts b/s2-site/examples/interaction/basic/demo/corner-cell-click-selection.ts new file mode 100644 index 0000000000..c270f79ac4 --- /dev/null +++ b/s2-site/examples/interaction/basic/demo/corner-cell-click-selection.ts @@ -0,0 +1,38 @@ +import { PivotSheet, S2Event, S2Options } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', +) + .then((res) => res.json()) + .then(async (dataCfg) => { + const container = document.getElementById('container'); + + /** + * 试一试点击角头: + * 点击列角头 (类别/子类别): 会选中对应的列头 + * 点击行角头 (省份/城市): 会选中对应的行头 + * 点击序号角头: 会选中对应的序号列 + */ + const s2Options: S2Options = { + width: 600, + height: 480, + seriesNumber: { + enable: true, + }, + interaction: { + hoverHighlight: true, + }, + }; + + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.on(S2Event.CORNER_CELL_CLICK, (event) => { + console.log('corner cell click:', event); + }); + + s2.on(S2Event.GLOBAL_SELECTED, (cells) => { + console.log('selected', cells); + }); + + await s2.render(); + }); diff --git a/s2-site/examples/interaction/basic/demo/event.ts b/s2-site/examples/interaction/basic/demo/event.ts index a3e82408c3..bf5aecf51c 100644 --- a/s2-site/examples/interaction/basic/demo/event.ts +++ b/s2-site/examples/interaction/basic/demo/event.ts @@ -8,26 +8,26 @@ import { } from '@antv/s2'; function addButtons(s2: SpreadSheet) { - const selectAllBtn = document.createElement('button'); - const selectHeaderCellBtn = document.createElement('button'); - const selectDataCellBtn = document.createElement('button'); - const hideColumnsBtn = document.createElement('button'); - const resetBtn = document.createElement('button'); - - [ + const [ selectAllBtn, selectHeaderCellBtn, selectDataCellBtn, hideColumnsBtn, + highlightHeaderBtn, resetBtn, - ].forEach((btn) => { + ] = Array.from({ length: 6 }).map(() => { + const btn = document.createElement('button'); + btn.className = 'ant-btn ant-btn-default'; + + return btn; }); selectAllBtn.innerHTML = '选中全部'; selectHeaderCellBtn.innerHTML = '选中指定行列头单元格'; selectDataCellBtn.innerHTML = '选中指定数值单元格'; hideColumnsBtn.innerHTML = '隐藏指定列头'; + highlightHeaderBtn.innerHTML = '高亮数值和对应的行列头单元格'; resetBtn.innerHTML = '重置'; // 查看更多 API: https://s2.antv.antgroup.com/api/basic-class/interaction @@ -81,6 +81,25 @@ function addButtons(s2: SpreadSheet) { ]); }); + highlightHeaderBtn.addEventListener('click', () => { + const dataCellViewMeta = s2.facet.getCellMeta(1, 1); + + s2.interaction.updateDataCellRelevantHeaderCells( + dataCellViewMeta, + InteractionStateName.HOVER, + ); + + // s2.interaction.updateDataCellRelevantRowCells( + // dataCellViewMeta, + // InteractionStateName.HOVER, + // ); + + // s2.interaction.updateDataCellRelevantColCells( + // dataCellViewMeta, + // InteractionStateName.HOVER, + // ); + }); + resetBtn.addEventListener('click', () => { console.log('当前状态:', s2.interaction.getState()); console.log('当前发生过交互的单元格:', s2.interaction.getInteractedCells()); @@ -96,10 +115,12 @@ function addButtons(s2: SpreadSheet) { if (canvas) { canvas.style.marginTop = '10px'; + canvas.before(selectAllBtn); canvas.before(selectHeaderCellBtn); canvas.before(selectDataCellBtn); canvas.before(hideColumnsBtn); + canvas.before(highlightHeaderBtn); canvas.before(resetBtn); } } @@ -150,6 +171,9 @@ fetch( [ S2Event.GLOBAL_SCROLL, S2Event.ROW_CELL_CLICK, + S2Event.COL_CELL_CLICK, + S2Event.CORNER_CELL_CLICK, + S2Event.DATA_CELL_CLICK, S2Event.GLOBAL_SELECTED, S2Event.DATA_CELL_BRUSH_SELECTION, ].forEach((eventName) => { diff --git a/s2-site/examples/interaction/basic/demo/meta.json b/s2-site/examples/interaction/basic/demo/meta.json index 42631712bb..38fe4d4b95 100644 --- a/s2-site/examples/interaction/basic/demo/meta.json +++ b/s2-site/examples/interaction/basic/demo/meta.json @@ -28,6 +28,15 @@ }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/XYZaL1w%24M/Kapture%2525202022-04-15%252520at%25252011.45.55.gif" }, + { + "filename": "corner-cell-click-selection.ts", + "title": { + "zh": "角头多选", + "en": "Corner Cell Click Selection" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0csYQK0JHaQAAAAAAAAAAAAADmJ7AQ/original", + "new": true + }, { "filename": "data-cell-range-selection.ts", "title": {