diff --git a/.github/workflows/pr-auto-assign-reviewer.yml b/.github/workflows/pr-auto-assign-reviewer.yml index 8dca9220ef..4610e2a550 100644 --- a/.github/workflows/pr-auto-assign-reviewer.yml +++ b/.github/workflows/pr-auto-assign-reviewer.yml @@ -21,5 +21,5 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} pr-emoji: '+1, rocket' - reviewers: 'xingwanying,qubaomingg,lcx-seima,YardWill,lijinke666,wjgogogo,zxc0328,stone-lyl,' + reviewers: 'xingwanying,serializedowen,lcx-seima,YardWill,lijinke666,wjgogogo,stone-lyl,GaoFuhong' review-creator: false diff --git a/.github/workflows/sync-site-lock-changelog-with-pr.yml b/.github/workflows/release-success.yml similarity index 94% rename from .github/workflows/sync-site-lock-changelog-with-pr.yml rename to .github/workflows/release-success.yml index 05d072102b..1fa9a12c55 100644 --- a/.github/workflows/sync-site-lock-changelog-with-pr.yml +++ b/.github/workflows/release-success.yml @@ -1,4 +1,4 @@ -name: 🔁 Sync Site S2 Lock And Changelog With PR +name: 🚀 After the release is successful # 自动发布 action 执行完成后再执行 (无论成功失败都会执行) on: @@ -59,3 +59,7 @@ jobs: gh pr create --title "chore: 🤖 更新 lock 和 changelog 文件" --body "🤖 由 [[Sync Site S2 Lock And Changelog With PR](https://github.com/antvis/S2/blob/master/.github/workflows/sync-site-lock-changelog-with-pr.yml)] action 自动创建." env: GITHUB_TOKEN: ${{ secrets.JINKE_GITHUB_TOKEN }} + + # 部署官网 + - name: Delpoy Site + run: yarn site:deploy diff --git a/packages/s2-core/CHANGELOG.md b/packages/s2-core/CHANGELOG.md index cc7bd0233b..b190954242 100644 --- a/packages/s2-core/CHANGELOG.md +++ b/packages/s2-core/CHANGELOG.md @@ -1,6 +1,5 @@ # [@antv/s2-v1.24.0-alpha.4](https://github.com/antvis/S2/compare/@antv/s2-v1.24.0-alpha.3...@antv/s2-v1.24.0-alpha.4) (2022-07-21) - ### Bug Fixes * **strategysheet:** 修复子弹图颜色显示错误 & 百分比精度问题 ([#1588](https://github.com/antvis/S2/issues/1588)) ([c4bb48c](https://github.com/antvis/S2/commit/c4bb48cbe128b47e3574af903142934fd7452846)) diff --git a/packages/s2-core/__tests__/bugs/issue-1587-spec.ts b/packages/s2-core/__tests__/bugs/issue-1587-spec.ts new file mode 100644 index 0000000000..b4e0028a7d --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-1587-spec.ts @@ -0,0 +1,28 @@ +/** + * @description spec for issue #1587 + * https://github.com/antvis/S2/issues/1587 + */ + +import { getContainer } from '../util/helpers'; +import * as mockDataConfig from '../data/simple-data.json'; +import type { S2Options } from '../../src'; +import { PivotSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + width: 800, + height: 600, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + pagination: { + pageSize: 2, + }, +}; + +describe('Pagination Tests', () => { + const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); + s2.render(); + + test('should get correctly pagination scroll y if pagination.current is empty', () => { + expect(s2.facet.getPaginationScrollY()).toEqual(0); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-1624-spec.ts b/packages/s2-core/__tests__/bugs/issue-1624-spec.ts new file mode 100644 index 0000000000..6d9f9f8f77 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-1624-spec.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * @description spec for issue #1624 + * https://github.com/antvis/S2/issues/1624 + */ + +import { getContainer, sleep } from '../util/helpers'; +import * as mockDataConfig from '../data/simple-data.json'; +import { S2Event, type S2Options, DataCell } from '@/index'; +import { PivotSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + width: 800, + height: 600, +}; + +describe('Data Cell Border Tests', () => { + const borderWidth = 4; + const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); + s2.setTheme({ + dataCell: { + cell: { + verticalBorderWidth: borderWidth, + horizontalBorderWidth: borderWidth, + }, + }, + }); + s2.render(); + + test('should draw correct data cell border when hover focus', async () => { + const dataCell = s2.panelScrollGroup.getChildren()[0] as DataCell; + + s2.emit(S2Event.DATA_CELL_HOVER, { + target: dataCell, + } as any); + + await sleep(40); + + // @ts-ignore + const meta = dataCell.getCellArea(); + const borderBbox = dataCell.getChildByIndex(2).getBBox(); + + expect(meta.x).toEqual(borderBbox.x - borderWidth / 2); + expect(meta.y).toEqual(borderBbox.y - borderWidth / 2); + expect(meta.width).toEqual(borderBbox.width + borderWidth); + expect(meta.height).toEqual(borderBbox.height + borderWidth); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-1668-spec.ts b/packages/s2-core/__tests__/bugs/issue-1668-spec.ts new file mode 100644 index 0000000000..05aed9f9d0 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-1668-spec.ts @@ -0,0 +1,49 @@ +/** + * @description spec for issue #1668 + * https://github.com/antvis/S2/issues/1668 + */ + +import type { IGroup } from '@antv/g-canvas'; +import { getContainer } from '../util/helpers'; +import * as mockDataConfig from '../data/data-issue-1668.json'; +import { type S2Options, KEY_GROUP_COL_RESIZE_AREA } from '@/index'; +import { PivotSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + width: 800, + height: 400, + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseLayout: true, + reverseSubLayout: true, + subTotalsDimensions: ['province'], + }, + col: { + showGrandTotals: true, + showSubTotals: true, + reverseLayout: true, + reverseSubLayout: true, + subTotalsDimensions: ['type'], + }, + }, +}; + +describe('Totals Cell Resize Tests', () => { + const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); + s2.render(); + + test('should render extra resize id for resize area handler', () => { + const resizeArea = s2.foregroundGroup.findById( + KEY_GROUP_COL_RESIZE_AREA, + ) as IGroup; + const resizeAreaList = resizeArea.getChildren(); + + expect(resizeAreaList).not.toHaveLength(0); + + resizeAreaList.forEach((shape) => { + expect(shape.attr('appendInfo').id).toBeTruthy(); + }); + }); +}); diff --git a/packages/s2-core/__tests__/data/data-issue-1668.json b/packages/s2-core/__tests__/data/data-issue-1668.json new file mode 100644 index 0000000000..014b7a0658 --- /dev/null +++ b/packages/s2-core/__tests__/data/data-issue-1668.json @@ -0,0 +1,271 @@ +{ + "data": [ + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": "1" + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": "2" + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": "2" + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": "0.5" + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": "3" + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": "2" + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": "4" + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": "1" + }, + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": "1" + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": "2" + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": "2" + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": "0.5" + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": "3" + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": "2" + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": "4" + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": "1" + }, + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "cost": "0.5" + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "cost": "1.5" + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "cost": "1.5" + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "cost": "0.2" + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "cost": "2" + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "cost": "1" + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "cost": "3" + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "cost": "0.5" + }, + { + "price": "15.5" + }, + { + "province": "浙江", + "price": "5.5" + }, + { + "province": "浙江", + "city": "杭州", + "price": "3" + }, + { + "province": "浙江", + "city": "舟山", + "price": "2.5" + }, + { + "province": "吉林", + "price": "10" + }, + { + "province": "吉林", + "city": "长春", + "price": "5" + }, + { + "province": "吉林", + "city": "白山", + "price": "5" + }, + { + "type": "笔", + "price": "10" + }, + { + "type": "笔", + "province": "浙江", + "price": "3" + }, + { + "type": "笔", + "province": "吉林", + "price": "7" + }, + { + "type": "纸张", + "price": "5.5" + }, + { + "type": "纸张", + "province": "浙江", + "price": "2.5" + }, + { + "type": "纸张", + "province": "吉林", + "price": "3" + }, + { + "cost": "10.2" + }, + { + "province": "浙江", + "cost": "3.7" + }, + { + "province": "浙江", + "city": "杭州", + "cost": "2" + }, + { + "province": "浙江", + "city": "舟山", + "cost": "1.7" + }, + { + "province": "吉林", + "cost": "6.5" + }, + { + "province": "吉林", + "city": "长春", + "cost": "3" + }, + { + "province": "吉林", + "city": "白山", + "cost": "3.5" + }, + { + "type": "笔", + "cost": "7" + }, + { + "type": "笔", + "province": "浙江", + "cost": "2" + }, + { + "type": "笔", + "province": "吉林", + "cost": "5" + }, + { + "type": "纸张", + "cost": "3.2" + }, + { + "type": "纸张", + "province": "浙江", + "cost": "1.7" + }, + { + "type": "纸张", + "province": "吉林", + "cost": "1.5" + } + ], + "fields": { + "rows": ["province", "city"], + "columns": ["type"], + "values": ["price", "cost"] + } +} diff --git a/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts b/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts index 40001ad256..e44d42f5bd 100644 --- a/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts @@ -39,7 +39,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { ).toEqual(mockTableDataConfig.fields.columns); }); - test('should hidden column correctly', () => { + test('should hide column correctly', () => { const hiddenColumns = ['cost']; tableSheet.interaction.hideColumns(hiddenColumns); @@ -62,7 +62,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(costDetail.hideColumnNodes[0].field).toEqual('cost'); }); - test('should hidden multiple columns correctly', () => { + test('should hide multiple columns correctly', () => { const hiddenColumns = ['price', 'city']; tableSheet.interaction.hideColumns(hiddenColumns); @@ -98,7 +98,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(cityDetail.hideColumnNodes[0].field).toEqual('city'); }); - test('should hidden closer group columns correctly', () => { + test('should hide closer group columns correctly', () => { const hiddenColumns = ['cost', 'province']; tableSheet.interaction.hideColumns(hiddenColumns); @@ -125,7 +125,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(groupDetail.hideColumnNodes[1].field).toEqual('province'); }); - test('should default hidden columns by interaction hiddenColumnFields config', () => { + test('should hide columns by interaction hiddenColumnFields config by default', () => { const hiddenColumns = ['cost']; const sheet = new TableSheet(getContainer(), mockTableDataConfig, { ...s2Options, @@ -184,7 +184,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { ).toEqual([typePriceColumnId, cityPriceColumnId]); }); - test('should hidden column correctly', () => { + test('should hide column correctly', () => { const hiddenColumns = [typePriceColumnId]; pivotSheet.interaction.hideColumns(hiddenColumns); @@ -207,7 +207,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(priceDetail.hideColumnNodes[0].id).toEqual(typePriceColumnId); }); - test('should hidden multiple columns correctly', () => { + test('should hide multiple columns correctly', () => { const hiddenColumns = [typePriceColumnId, cityPriceColumnId]; pivotSheet.interaction.hideColumns(hiddenColumns); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts index a193347ade..ba8a9da1a0 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts @@ -91,6 +91,32 @@ describe('SpreadSheet Resize Active Tests', () => { expect(group.findById(KEY_GROUP_COL_RESIZE_AREA)).toBeNull(); }); + // https://github.com/antvis/S2/issues/1603 + test('should disable col cell resize area if col is hidden', () => { + const s2 = renderSheet({ + interaction: { + resize: { + colCellHorizontal: false, + colCellVertical: true, + }, + }, + } as S2Options); + + s2.setOptions({ + style: { + colCfg: { + height: 0, + }, + }, + }); + + s2.render(false); + + const group = s2.facet.foregroundGroup; + + expect(group.findById(KEY_GROUP_COL_RESIZE_AREA)).toBeNull(); + }); + test('should disable col cell vertical direction resize area', () => { const s2 = renderSheet({ interaction: { diff --git a/packages/s2-core/__tests__/spreadsheet/table-resize-spec.ts b/packages/s2-core/__tests__/spreadsheet/table-resize-spec.ts new file mode 100644 index 0000000000..4988788cbf --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/table-resize-spec.ts @@ -0,0 +1,37 @@ +import type { IGroup } from '@antv/g-canvas'; +import { getContainer } from '../util/helpers'; +import * as mockDataConfig from '../data/simple-table-data.json'; +import { TableSheet } from '@/sheet-type'; +import { KEY_GROUP_ROW_RESIZE_AREA } from '@/common/constant'; + +describe('Table Sheet Resize Test', () => { + test('should draw resize area in series cell when show series', () => { + const s2 = new TableSheet(getContainer(), mockDataConfig, { + width: 800, + height: 600, + showSeriesNumber: true, + }); + s2.render(); + + const resizeGroup = s2.foregroundGroup.findById( + KEY_GROUP_ROW_RESIZE_AREA, + ) as IGroup; + + expect(resizeGroup.getChildren().length).toBeGreaterThan(0); + }); + + test('should draw resize area in data cell when hide series', () => { + const s2 = new TableSheet(getContainer(), mockDataConfig, { + width: 800, + height: 600, + showSeriesNumber: false, + }); + s2.render(); + + const resizeGroup = s2.foregroundGroup.findById( + KEY_GROUP_ROW_RESIZE_AREA, + ) as IGroup; + + expect(resizeGroup.getChildren().length).toBeGreaterThan(0); + }); +}); diff --git a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts index 9cc3fbf499..c91a6f1800 100644 --- a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts @@ -4,6 +4,7 @@ import { type S2Options, type S2DataConfig, ResizeType, + ColCell, } from '@/index'; const data = getMockData( @@ -96,6 +97,7 @@ const options: S2Options = { }; describe('TableSheet normal spec', () => { + test('scrollWithAnimation with duration and callback', async () => { const s2 = new TableSheet(getContainer(), dataCfg, options); s2.render(); @@ -123,5 +125,68 @@ describe('TableSheet normal spec', () => { hRowScrollX: 0, }); expect(onScrollFinish).toBeCalled(); + + s2.destroy(); }); + + test('should be able to resize frozen col when there is a vertical scroll width', async () => { + const s2 = new TableSheet(getContainer(), dataCfg, options); + s2.render(); + + const onScrollFinish = jest.fn(); + s2.facet.scrollWithAnimation( + { + offsetX: { + value: 100, + animate: true, + }, + }, + 10, + onScrollFinish, + ); + await sleep(30); + + + const firstColCell = s2.getColumnNodes()[1].belongsCell as any + + expect(firstColCell.shouldAddVerticalResizeArea()).toBe(true) + expect(firstColCell.getVerticalResizeAreaOffset()).toEqual({ x: 80, y: 0 }) + + s2.destroy(); + }); + + test('should be able to resize last column', async () => { + const s2 = new TableSheet(getContainer(), dataCfg, options); + s2.render(); + + await sleep(30); + + const { x, width, top } = s2.getCanvasElement().getBoundingClientRect() + s2.getCanvasElement().dispatchEvent(new MouseEvent('mousedown', { + clientX: x + width - 1, + clientY: top + 25, + })) + + + window.dispatchEvent(new MouseEvent('mousemove', { + clientX: x+ width + 100, + clientY: top + 25, + + })) + await sleep(300); + + window.dispatchEvent(new MouseEvent('mouseup', { + clientX: x + width + 100, + clientY: top + 25 + })) + + await sleep(300); + + const columnNodes = s2.getColumnNodes() + const lastColumnCell = columnNodes[columnNodes.length - 1].belongsCell as ColCell + + expect(lastColumnCell.getMeta().width).toBe(199) + }); + + }); diff --git a/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts index c70e2f138a..ec90997f93 100644 --- a/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts @@ -2,8 +2,8 @@ import _ from 'lodash'; import type { Node } from '@/facet/layout/node'; import { PivotDataSet } from '@/data-set'; import { SpreadSheet, PivotSheet } from '@/sheet-type'; +import type { Formatter, TextAlign } from '@/common'; import { ColCell } from '@/cell'; -import type { TextAlign } from '@/common'; const MockPivotSheet = PivotSheet as unknown as jest.Mock; const MockPivotDataSet = PivotDataSet as unknown as jest.Mock; @@ -105,4 +105,35 @@ describe('Col Cell Tests', () => { }, ); }); + + describe('Col Cell Formatter Test', () => { + const node = { + id: 1, + label: 'label', + fieldValue: 'fieldValue', + value: 'value', + } as unknown as Node; + + test('should get correct col cell formatter', () => { + const formatter = jest.fn(); + jest.spyOn(s2.dataSet, 'getFieldFormatter').mockReturnValue(formatter); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const colCell = new ColCell(node, s2); + + expect(formatter).toHaveBeenCalledWith(node.label, undefined, node); + }); + + test('should return correct formatted value', () => { + const formatter: Formatter = jest.fn(() => 'test'); + + jest.spyOn(s2.dataSet, 'getFieldFormatter').mockReturnValue(formatter); + + const colCell = new ColCell(node, s2); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(colCell.textShape.attr('text')).toEqual('test'); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts index 3a51656f2b..d0cb73dc13 100644 --- a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts @@ -7,8 +7,12 @@ import { DataCell } from '@/cell'; const MockPivotSheet = PivotSheet as unknown as jest.Mock; const MockPivotDataSet = PivotDataSet as unknown as jest.Mock; + describe('data cell formatter test', () => { const meta = { + fieldValue: 'fieldValue', + label: 'label', + value: 'value', data: { city: 'chengdu', value: 12, @@ -27,18 +31,14 @@ describe('data cell formatter test', () => { s2.dataSet = dataSet; }); - test('should pass complete data into formater', () => { + test('should pass complete data into formatter', () => { const formatter = jest.fn(); jest.spyOn(s2.dataSet, 'getFieldFormatter').mockReturnValue(formatter); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const dataCell = new DataCell(meta, s2); - expect(formatter).toHaveBeenCalledWith(undefined, { - city: 'chengdu', - value: 12, - [VALUE_FIELD]: 'value', - [EXTRA_FIELD]: 12, - }); + expect(formatter).toHaveBeenCalledWith(meta.fieldValue, meta.data, meta); }); test('should return correct formatted value', () => { diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts index d55757e813..275f0ec92a 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts @@ -4,6 +4,7 @@ import { Canvas, Group } from '@antv/g-canvas'; import { assembleDataCfg, assembleOptions } from 'tests/util'; import { size, get, find } from 'lodash'; +import { DEFAULT_TREE_ROW_WIDTH } from './../../../src/common/constant/options'; import { getMockPivotMeta } from './util'; import type { PanelScrollGroup } from '@/group/panel-scroll-group'; import { SpreadSheet } from '@/sheet-type'; @@ -67,6 +68,8 @@ jest.mock('@/sheet-type', () => { interaction: { clearHoverTimer: jest.fn(), }, + measureTextWidth: + jest.fn() as unknown as SpreadSheet['measureTextWidth'], }; }), }; @@ -188,6 +191,7 @@ describe('Pivot Mode Facet Test', () => { describe('should get correct result when tree mode', () => { s2.isHierarchyTreeType = jest.fn().mockReturnValue(true); + const spy = jest.spyOn(s2, 'measureTextWidth').mockReturnValue(30); // 小于 DEFAULT_TREE_ROW_WIDTH const mockDataSet = new MockPivotDataSet(s2); const treeFacet = new PivotFacet({ spreadsheet: s2, @@ -199,6 +203,10 @@ describe('Pivot Mode Facet Test', () => { }); const { rowsHierarchy } = treeFacet.layoutResult; + afterAll(() => { + spy.mockRestore(); + }); + test('row hierarchy when tree mode', () => { const { cellCfg, spreadsheet } = facet.cfg; const rowCellStyle = spreadsheet.theme.rowCell.cell; @@ -206,10 +214,11 @@ describe('Pivot Mode Facet Test', () => { expect(rowsHierarchy.getLeaves()).toHaveLength(8); expect(rowsHierarchy.getNodes()).toHaveLength(10); - expect(rowsHierarchy.width).toBe(width); + expect(rowsHierarchy.width).toBe(DEFAULT_TREE_ROW_WIDTH); + expect(width).toBeUndefined(); rowsHierarchy.getNodes().forEach((node, index) => { - expect(node.width).toBe(width); + expect(node.width).toBe(DEFAULT_TREE_ROW_WIDTH); expect(node.height).toBe( cellCfg.height + rowCellStyle.padding?.top + @@ -314,4 +323,30 @@ describe('Pivot Mode Facet Test', () => { } }, ); + + // https://github.com/antvis/S2/issues/1622 + test('should render custom column leaf node width and use treeRowsWidth first for tree mode', () => { + const mockDataSet = new MockPivotDataSet(s2); + const customWidthFacet = new PivotFacet({ + spreadsheet: s2, + dataSet: mockDataSet, + ...assembleDataCfg().fields, + ...assembleOptions(), + hierarchyType: 'tree', + cellCfg: {}, + colCfg: {}, + rowCfg: { + // 行头宽度 + width: 200, + // 已废弃 + treeRowsWidth: 300, + }, + // 树状结构下行头宽度 (优先级最高) + treeRowsWidth: 400, + }); + + customWidthFacet.layoutResult.rowNodes.forEach((node) => { + expect(node.width).toStrictEqual(400); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts index cd1c73a68e..7ad52864c6 100644 --- a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts @@ -173,11 +173,33 @@ describe('Table Mode Facet Test With Adaptive Layout', () => { describe('Table Mode Facet Test With Compact Layout', () => { describe('should get correct col layout', () => { + const LABEL_WIDTH = [36, 36, 48, 24, 56]; // 采样的文本宽度 const ss: SpreadSheet = new MockSpreadSheet(); const dataSet: TableDataSet = new MockTableDataSet(ss); ss.getLayoutWidthType = () => { return 'compact'; }; + + const mockMeasureFunc = (text: string | number) => { + switch (text) { + case '浙江省': + return LABEL_WIDTH[0]; + case '杭州市': + return LABEL_WIDTH[1]; + case '办公用品': + return LABEL_WIDTH[2]; + case '沙发': + return LABEL_WIDTH[3]; + case 'undefined': + return LABEL_WIDTH[4]; + default: + return 0; + } + }; + ss.measureTextWidth = + mockMeasureFunc as unknown as SpreadSheet['measureTextWidth']; + ss.measureTextWidthRoughly = mockMeasureFunc; + const facet: TableFacet = new TableFacet({ spreadsheet: ss, dataSet, @@ -190,7 +212,6 @@ describe('Table Mode Facet Test With Compact Layout', () => { test('col hierarchy coordinate with compact layout', () => { const { colLeafNodes } = facet.layoutResult; - const COMPACT_WIDTH = [53, 53, 65, 41, 73]; let lastX = 0; @@ -205,11 +226,32 @@ describe('Table Mode Facet Test With Compact Layout', () => { }); describe('should get correct col layout with seriesNumber', () => { + const LABEL_WIDTH = [36, 36, 48, 24, 56]; // 采样的文本宽度 const ss: SpreadSheet = new MockSpreadSheet(); const dataSet: TableDataSet = new MockTableDataSet(ss); ss.getLayoutWidthType = () => { return 'compact'; }; + const mockMeasureFunc = (text: string | number) => { + switch (text) { + case '浙江省': + return LABEL_WIDTH[0]; + case '杭州市': + return LABEL_WIDTH[1]; + case '办公用品': + return LABEL_WIDTH[2]; + case '沙发': + return LABEL_WIDTH[3]; + case 'undefined': // seriesnumber & price + return LABEL_WIDTH[4]; + default: + return 0; + } + }; + ss.measureTextWidth = + mockMeasureFunc as unknown as SpreadSheet['measureTextWidth']; + ss.measureTextWidthRoughly = mockMeasureFunc; + const facet: TableFacet = new TableFacet({ spreadsheet: ss, dataSet, diff --git a/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts b/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts index bc14438738..9fcd62919f 100644 --- a/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts @@ -15,6 +15,7 @@ import { type S2Options, SpreadSheet, type ThemeCfg, + customMerge, } from '@/index'; import type { BaseFacet } from '@/facet/base-facet'; @@ -233,6 +234,8 @@ describe('Interaction Row Column Resize Tests', () => { isResizeArea: true, effect: ResizeAreaEffect.Cell, id: '', + resizedWidth: 5, + resizedHeight: 0, } as ResizeInfo; emitResizeEvent( @@ -343,6 +346,8 @@ describe('Interaction Row Column Resize Tests', () => { isResizeArea: true, effect: ResizeAreaEffect.Cell, id: '', + resizedWidth: 0, + resizedHeight: 2, } as ResizeInfo; emitResizeEvent( @@ -409,31 +414,81 @@ describe('Interaction Row Column Resize Tests', () => { }); test('should get horizontal tree resize style', () => { + const resize = jest.fn(); + const treeWidthResize = jest.fn(); + + s2.on(S2Event.LAYOUT_RESIZE_TREE_WIDTH, treeWidthResize); + s2.on(S2Event.LAYOUT_RESIZE, resize); + const resizeInfo = emitResize( ResizeDirectionType.Horizontal, ResizeAreaEffect.Tree, ); + const newResizeInfo = { + info: { ...resizeInfo, resizedWidth: 5, resizedHeight: 0 }, + style: { + rowCfg: { + treeRowsWidth: 5, + }, + treeRowsWidth: 5, + }, + }; + expect(resize).toHaveBeenCalledWith(newResizeInfo); + expect(treeWidthResize).toHaveBeenCalledWith(newResizeInfo); expect(s2.options.style.rowCfg.treeRowsWidth).toEqual(resizeInfo.width); + expect(s2.options.style.treeRowsWidth).toEqual(resizeInfo.width); }); test('should get horizontal filed resize style', () => { + const resize = jest.fn(); + const rowWidthResize = jest.fn(); + + s2.on(S2Event.LAYOUT_RESIZE_ROW_WIDTH, rowWidthResize); + s2.on(S2Event.LAYOUT_RESIZE, resize); + const resizeInfo = emitResize( ResizeDirectionType.Horizontal, ResizeAreaEffect.Field, ); + const newResizeInfo = { + info: { ...resizeInfo, resizedWidth: 5, resizedHeight: 0 }, + style: { + rowCfg: { + widthByField: { + [resizeInfo.id]: 5, + }, + }, + }, + }; + + expect(resize).toHaveBeenCalledWith(newResizeInfo); + expect(rowWidthResize).toHaveBeenCalledWith(newResizeInfo); expect(s2.options.style.rowCfg.widthByField).toEqual({ [resizeInfo.id]: resizeInfo.width, }); }); test('should get horizontal series resize style', () => { + const resize = jest.fn(); + const seriesWidthResize = jest.fn(); + + s2.on(S2Event.LAYOUT_RESIZE_SERIES_WIDTH, seriesWidthResize); + s2.on(S2Event.LAYOUT_RESIZE, resize); + const resizeInfo = emitResize( ResizeDirectionType.Horizontal, ResizeAreaEffect.Series, ); + const newResizeInfo = { + info: { ...resizeInfo, resizedWidth: 5, resizedHeight: 0 }, + style: undefined, + }; + + expect(resize).toHaveBeenCalledWith(newResizeInfo); + expect(seriesWidthResize).toHaveBeenCalledWith(newResizeInfo); expect(s2.theme.rowCell.seriesNumberWidth).toEqual(resizeInfo.width); }); diff --git a/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts b/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts index 615a44a2a9..6ca5e22826 100644 --- a/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts @@ -30,6 +30,12 @@ describe('Interaction Keyboard Move Tests', () => { }, }, } as S2Options; + s2.theme = { + splitLine: { + verticalBorderWidth: 1, + horizontalBorderWidth: 1, + }, + }; s2.isTableMode = jest.fn(() => true); s2.dataSet = { fields: { columns: ['0', '1'] }, @@ -43,8 +49,16 @@ describe('Interaction Keyboard Move Tests', () => { ], }, getTotalHeightForRange: (start, end) => 0, - scrollWithAnimation: (data) => {}, - getScrollOffset: () => ({ scrollX: 0, scrollY: 0 }), + scrollWithAnimation: (data) => { + s2.store.set('scrollX', data?.offsetX?.value); + s2.store.set('scrollY', data?.offsetY?.value); + }, + getScrollOffset: () => { + return { + scrollX: s2.store.get('scrollX', 0), + scrollY: s2.store.get('scrollY', 0), + }; + }, panelBBox: { viewportHeight: 200, viewportWidth: 200, @@ -309,4 +323,34 @@ describe('Interaction Keyboard Move Tests', () => { s2.interaction.eventController.isCanvasEffect = true; }); + + test('should scroll to active cell', () => { + s2.interaction.changeState = jest.fn((state) => {}); + s2.interaction.getCells = () => [mockCell01.mockCell as any]; + // select cell + keyboardMove.startCell = mockCell01.mockCell; + keyboardMove.endCell = mockCell01.mockCell; + s2.facet.scrollWithAnimation({ + offsetX: { + value: 1, + }, + offsetY: { + value: 1, + }, + }); + + expect(s2.facet.getScrollOffset()).toEqual({ + scrollX: 1, + scrollY: 1, + }); + + s2.emit(S2Event.GLOBAL_KEYBOARD_DOWN, { + key: InteractionKeyboardKey.ARROW_LEFT, + } as KeyboardEvent); + + expect(s2.facet.getScrollOffset()).toEqual({ + scrollX: 0, + scrollY: 1, + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts index 4526edff29..bc2c23e2e7 100644 --- a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts @@ -1,6 +1,7 @@ import { assembleDataCfg, assembleOptions, TOTALS_OPTIONS } from 'tests/util'; import { getContainer } from 'tests/util/helpers'; import { data as originalData, totalData } from 'tests/data/mock-dataset.json'; +import { map } from 'lodash'; import { TableSheet, PivotSheet } from '@/sheet-type'; import { @@ -10,6 +11,7 @@ import { } from '@/common/constant/interaction'; import { convertString, + CopyMIMEType, getCopyData, getSelectedData, } from '@/utils/export/copy'; @@ -102,6 +104,25 @@ describe('List Table Core Data Process', () => { expect(getSelectedData(s2).split('\n')[2].split('\t').length).toBe(5); }); + it('should copy normal data with header in table mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.render(); + + const cell = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL)[0]; + + s2.interaction.changeState({ + cells: [getCellMeta(cell)], + stateName: InteractionStateName.SELECTED, + }); + expect(getSelectedData(s2)).toEqual('province\r\n浙江省'); + }); + it('should copy format data', () => { const ss = new TableSheet( getContainer(), @@ -131,6 +152,13 @@ describe('List Table Core Data Process', () => { }); it('should copy correct data with data filtered', () => { + s2.setOptions({ + interaction: { + copyWithHeader: false, + }, + }); + s2.render(); + s2.emit(S2Event.RANGE_FILTER, { filterKey: 'province', filteredValues: ['浙江省'], @@ -221,6 +249,10 @@ describe('Pivot Table Core Data Process', () => { const ROW_COUNT = 7; // 11 = 8(维度节点) + 2(小计) + 1(总计) const COL_COUNT = 11; + // 3 = ['type', 'sub_type', 'number'].length 行头高度 + const COL_HEADER_HEIGHT = 3; + // 2 = ['province', 'city'].length 列头宽度 + const ROW_HEADER_WIDTH = 2; const s2 = new PivotSheet( getContainer(), @@ -363,11 +395,133 @@ describe('Pivot Table Core Data Process', () => { expect(getSelectedData(ss)).toEqual(`${originalData[0].number}元`); }); + it('should copy normal data with header in grid mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.render(); + + const allDataCells = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + + const hangzhouDeskCell = allDataCells[0]; + const zhejiangCityDeskSubTotalCell = allDataCells[4]; + + // 普通数据节点 + s2.interaction.changeState({ + cells: [getCellMeta(hangzhouDeskCell)], + stateName: InteractionStateName.SELECTED, + }); + + expect(getSelectedData(s2)).toEqual( + `\t\t家具\r\n\t\t桌子\r\n\t\tnumber\r\n浙江省\t杭州市\t7789`, + ); + + // 小计节点 + s2.interaction.changeState({ + cells: [getCellMeta(zhejiangCityDeskSubTotalCell)], + stateName: InteractionStateName.SELECTED, + }); + expect(getSelectedData(s2)).toEqual( + `\t\t家具\r\n\t\t桌子\r\n\t\tnumber\r\n浙江省\t小计\t18375`, + ); + }); + + // 看图更清晰 https://gw.alipayobjects.com/zos/antfincdn/zK68PhcnX/d852ffb8-603a-43e5-b841-dbf3c7577638.png + it('should copy col data with header in grid mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.render(); + + const cell = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.COL_CELL)[0]; + + s2.interaction.changeState({ + cells: [getCellMeta(cell)], + stateName: InteractionStateName.SELECTED, + }); + + // 复制的数据高度 = 列头高度 + 数据高度 + expect(getSelectedData(s2).split('\n')).toHaveLength( + COL_COUNT + COL_HEADER_HEIGHT, + ); + // 复制的数据宽度 = 行头宽度 + 数据宽度 + expect(getSelectedData(s2).split('\n')[0].split('\t')).toHaveLength(5); + }); + + // https://gw.alipayobjects.com/zos/antfincdn/q3mBlV9Ii/1d68499a-b529-4594-93ce-8b04f8b4c4bc.png + it('should copy row data with header in grid mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.render(); + + const allRowCells = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.ROW_CELL); + + const hangzhouDeskCell = allRowCells[1]; + const zhejiangCityDeskSubTotalCell = allRowCells[0]; + + // 选择某一行, city 维度下 + s2.interaction.changeState({ + cells: [getCellMeta(hangzhouDeskCell)], + stateName: InteractionStateName.SELECTED, + }); + + expect(getSelectedData(s2).split('\n')).toHaveLength(4); + expect(getSelectedData(s2).split('\n')[0].split('\t')).toHaveLength(9); + + // 选择某几行,province 维度 + s2.interaction.changeState({ + cells: [getCellMeta(zhejiangCityDeskSubTotalCell)], + stateName: InteractionStateName.SELECTED, + }); + + expect(getSelectedData(s2).split('\n')).toHaveLength(8); + expect(getSelectedData(s2).split('\n')[0].split('\t')).toHaveLength(9); + }); + + it('should copy all data with header in grid mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.render(); + + s2.interaction.changeState({ + stateName: InteractionStateName.ALL_SELECTED, + }); + + expect(getSelectedData(s2).split('\n').length).toBe( + COL_COUNT + COL_HEADER_HEIGHT, + ); + expect(getSelectedData(s2).split('\n')[1].split('\t').length).toBe( + ROW_COUNT + ROW_HEADER_WIDTH, + ); + }); + it('should copy correct data with data sorted in grid mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: false, + }, + }); const node = s2.getColumnNodes().find((node) => node.isLeaf); s2.groupSortByMethod('ASC' as SortMethodType, node); s2.setDataCfg(s2.dataCfg); s2.render(); + const cell = s2.interaction .getAllCells() .filter(({ cellType }) => cellType === CellTypes.ROW_CELL) @@ -386,8 +540,6 @@ describe('Pivot Table Core Data Process', () => { }); it('should copy correct data with \n data in grid mode', () => { - const newLineText = `1 - 2`; const sss = new PivotSheet( getContainer(), assembleDataCfg({ @@ -536,4 +688,12 @@ describe('List Table getCopyData', () => { expect(data.split('\n').length).toBe(2); expect(data.split('\t').length).toBe(5); }); + + it('should copy in multiple format', () => { + const data = getCopyData(s2, CopyType.ROW, [ + CopyMIMEType.PLAIN, + CopyMIMEType.HTML, + ]) as string[]; + expect(data.length).toBe(2); + }); }); diff --git a/packages/s2-core/__tests__/unit/utils/export/export-spec.ts b/packages/s2-core/__tests__/unit/utils/export/export-spec.ts index 0644720030..8845f7511e 100644 --- a/packages/s2-core/__tests__/unit/utils/export/export-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/export-spec.ts @@ -8,7 +8,13 @@ describe('TableSheet Export Test', () => { const s2 = new TableSheet( getContainer(), assembleDataCfg({ - meta: [], + meta: [ + { + field: 'type', + name: '产品类型', + formatter: (type) => `${type}产品`, + }, + ], fields: { columns: ['province', 'city', 'type', 'sub_type', 'number'], }, @@ -18,7 +24,9 @@ describe('TableSheet Export Test', () => { }), ); s2.render(); - const data = copyData(s2, '\t'); + const data = copyData(s2, '\t', { + isFormatHeader: true, + }); const rows = data.split('\n'); const headers = rows[0].split('\t'); // 33行数据 包括一行列头 @@ -31,7 +39,7 @@ describe('TableSheet Export Test', () => { '序号', 'province', 'city', - 'type', + '产品类型', 'sub_type', 'number', ]); diff --git a/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx b/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx index debef10ae5..5b90bdb267 100644 --- a/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx +++ b/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx @@ -35,6 +35,16 @@ describe('Sort Action Test', () => { expect(sortAction(data2, 'DESC')).toEqual(['3', '2', '11']); }); + test('sort action with zero and number arr', () => { + const data1 = [1, 6, -2, 0]; + expect(sortAction(data1, 'ASC')).toEqual([-2, 0, 1, 6]); + expect(sortAction(data1, 'DESC')).toEqual([6, 1, 0, -2]); + + const data2 = ['0', 0, 2, -2]; + expect(sortAction(data2, 'ASC')).toEqual([-2, '0', 0, 2]); + expect(sortAction(data2, 'DESC')).toEqual([2, '0', 0, -2]); + }); + test('sort action with string arr', () => { const data = ['a', 'c', 'b']; expect(sortAction(data, 'ASC')).toEqual(['a', 'b', 'c']); @@ -49,6 +59,22 @@ describe('Sort Action Test', () => { expect(sortAction(data2, 'DESC')).toEqual(['啊', '2', '11']); }); + test('object data sorted by key with zero', () => { + const data1 = [{ a: 1 }, { a: 0 }, { a: -3 }, { a: 2 }]; + expect(sortAction(data1, 'ASC', 'a')).toEqual([ + { a: -3 }, + { a: 0 }, + { a: 1 }, + { a: 2 }, + ]); + expect(sortAction(data1, 'DESC', 'a')).toEqual([ + { a: 2 }, + { a: 1 }, + { a: 0 }, + { a: -3 }, + ]); + }); + test('sort action with object arr', () => { const data1 = [{ a: 1 }, { a: 3 }, { a: 2 }]; expect(sortAction(data1, 'ASC', 'a')).toEqual([ @@ -93,6 +119,7 @@ describe('Sort Action Test', () => { 'a', ), ).toEqual([{ a: undefined }, { a: '-' }, { a: 2 }, { a: '3' }]); + expect( sortAction( [{ a: '-' }, { a: '3' }, { a: 2 }, { a: undefined }], @@ -194,9 +221,33 @@ describe('Sort By Func Tests', () => { expect(result).toEqual(originValues); }); - test('should return sortFunc result', () => { + test('should return merged result', () => { + const originValues = ['四川[&]成都', '四川[&]绵阳', '浙江[&]杭州']; + const result = sortByFunc({ + originValues, + sortParam: { + sortFieldId: 'city', + sortFunc: () => ['浙江[&]杭州'], + }, + dataSet: { + fields: { + rows: ['province', 'city'], + }, + } as unknown as PivotDataSet, + }); + + // sortFunc 返回的值在前,未返回的值在后 + expect(result).toEqual(['浙江[&]杭州', '四川[&]成都', '四川[&]绵阳']); + }); + + test('should return merged result when sorting by ASC', () => { + const originValues = ['四川[&]成都', '四川[&]绵阳', '浙江[&]杭州']; + + const result = sortByFunc({ + originValues, sortParam: { + sortMethod: 'ASC', sortFieldId: 'city', sortFunc: () => ['浙江[&]杭州'], }, @@ -207,7 +258,9 @@ describe('Sort By Func Tests', () => { } as unknown as PivotDataSet, }); - expect(result).toEqual(['浙江[&]杭州']); + // asc 升序时 + // sortFunc 没返回的值在前,返回的值在后 + expect(result).toEqual(['四川[&]成都', '四川[&]绵阳', '浙江[&]杭州']); }); test('should return fallback result', () => { diff --git a/packages/s2-core/__tests__/unit/utils/text-spec.ts b/packages/s2-core/__tests__/unit/utils/text-spec.ts index 682ea47ce9..0c0d0662f8 100644 --- a/packages/s2-core/__tests__/unit/utils/text-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/text-spec.ts @@ -1,8 +1,8 @@ +import { createPivotSheet } from 'tests/util/helpers'; import { getEllipsisText, getEllipsisTextInner, isUpDataValue, - measureTextWidth, getCellWidth, getEmptyPlaceholder, } from '@/utils/text'; @@ -16,74 +16,91 @@ describe('Text Utils Tests', () => { fontWeight: 'normal', } as unknown as CSSStyleDeclaration; - test('should get correct text', () => { - const text = getEllipsisText({ - text: '12', - maxWidth: 200, - placeholder: '--', + describe('Test Widths Tests', () => { + let measureTextWidth: (text: number | string, font: unknown) => number; + + beforeEach(() => { + measureTextWidth = createPivotSheet( + {}, + { useSimpleData: true }, + ).measureTextWidth; }); - expect(text).toEqual('12'); - }); + test('should get correct text', () => { + const text = getEllipsisText({ + measureTextWidth, + text: '12', + maxWidth: 200, + placeholder: '--', + }); - test('should get correct text ellipsis', () => { - const text = getEllipsisText({ - text: '12121212121212121212', - maxWidth: 20, - placeholder: '--', + expect(text).toEqual('12'); }); - expect(text).toEndWith('...'); - expect(text.length).toBeLessThanOrEqual(5); - }); + test('should get correct text ellipsis', () => { + const text = getEllipsisText({ + measureTextWidth, + text: '12121212121212121212', + maxWidth: 20, + placeholder: '--', + }); - test('should get correct placeholder text with ""', () => { - const text = getEllipsisText({ - text: '', - maxWidth: 20, - placeholder: '--', + expect(text).toEndWith('...'); + expect(text.length).toBeLessThanOrEqual(5); }); - expect(text).toEqual('--'); - }); - test('should get correct placeholder text with 0', () => { - const text = getEllipsisText({ - text: 0 as unknown as string, - maxWidth: 20, - placeholder: '--', + test('should get correct placeholder text with ""', () => { + const text = getEllipsisText({ + measureTextWidth, + text: '', + maxWidth: 20, + placeholder: '--', + }); + expect(text).toEqual('--'); }); - expect(text).toEqual('0'); - }); + test('should get correct placeholder text with 0', () => { + const text = getEllipsisText({ + measureTextWidth, + text: 0 as unknown as string, + maxWidth: 20, + placeholder: '--', + }); - test('should get correct placeholder text with null', () => { - const text = getEllipsisText({ - text: null, - maxWidth: 20, - placeholder: '--', + expect(text).toEqual('0'); }); - expect(text).toEqual('--'); - }); + test('should get correct placeholder text with null', () => { + const text = getEllipsisText({ + measureTextWidth, + text: null, + maxWidth: 20, + placeholder: '--', + }); - test('should get correct ellipsis text', () => { - const text = getEllipsisText({ - text: '长度测试', - maxWidth: 24, + expect(text).toEqual('--'); }); - expect(text).toEndWith('...'); - expect(text.length).toBeLessThanOrEqual(4); - }); + test('should get correct ellipsis text', () => { + const text = getEllipsisText({ + measureTextWidth, + text: '长度测试', + maxWidth: 24, + }); - test('should get correct text width', () => { - const width = measureTextWidth('test', font); - expect(Math.floor(width)).toEqual(isHD ? 21 : 16); - }); + expect(text).toEndWith('...'); + expect(text.length).toBeLessThanOrEqual(4); + }); - test('should get correct ellipsis text inner', () => { - const text = getEllipsisTextInner('test', 15, font); - expect(text).toEqual('t...'); + test('should get correct text width', () => { + const width = measureTextWidth('test', font); + expect(Math.floor(width)).toEqual(isHD ? 21 : 16); + }); + + test('should get correct ellipsis text inner', () => { + const text = getEllipsisTextInner(measureTextWidth, 'test', 15, font); + expect(text).toEqual('t...'); + }); }); test.each` diff --git a/packages/s2-core/package.json b/packages/s2-core/package.json index 73046098a1..bc6a0f9701 100644 --- a/packages/s2-core/package.json +++ b/packages/s2-core/package.json @@ -69,13 +69,14 @@ "lodash": "^4.17.21" }, "devDependencies": { - "csstype": "^3.0.11", + "@testing-library/dom": "^8.16.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-timer": "^3.0.0", + "csstype": "^3.0.11", "d3-dsv": "^1.1.1", - "tinycolor2": "^1.4.2", "inquirer": "^8.2.0", - "inquirer-autocomplete-prompt": "^2.0.0" + "inquirer-autocomplete-prompt": "^2.0.0", + "tinycolor2": "^1.4.2" }, "sideEffects": [ "*.css", diff --git a/packages/s2-core/src/cell/base-cell.ts b/packages/s2-core/src/cell/base-cell.ts index 7dec33e242..f8c7e37b0d 100644 --- a/packages/s2-core/src/cell/base-cell.ts +++ b/packages/s2-core/src/cell/base-cell.ts @@ -34,11 +34,7 @@ import { } from '../utils/cell/cell'; import { renderLine, renderText, updateShapeAttr } from '../utils/g-renders'; import { isMobile } from '../utils/is-mobile'; -import { - getEllipsisText, - getEmptyPlaceholder, - measureTextWidth, -} from '../utils/text'; +import { getEllipsisText, getEmptyPlaceholder } from '../utils/text'; export abstract class BaseCell extends Group { // cell's data meta info @@ -201,9 +197,13 @@ export abstract class BaseCell extends Group { const { formattedValue } = this.getFormattedFieldValue(); const maxTextWidth = this.getMaxTextWidth(); const textStyle = this.getTextStyle(); - const { placeholder } = this.spreadsheet.options; + const { + options: { placeholder }, + measureTextWidth, + } = this.spreadsheet; const emptyPlaceholder = getEmptyPlaceholder(this, placeholder); const ellipsisText = getEllipsisText({ + measureTextWidth, text: formattedValue, maxWidth: maxTextWidth, fontParam: textStyle, @@ -233,13 +233,13 @@ export abstract class BaseCell extends Group { const device = this.spreadsheet.options.style.device; // 配置了链接跳转 if (!isMobile(device)) { - const { minX, maxX, maxY }: BBox = this.textShape.getBBox(); + const { minX, maxY }: BBox = this.textShape.getBBox(); this.linkFieldShape = renderLine( this, { x1: minX, y1: maxY + 1, - x2: maxX, + x2: minX + this.actualTextWidth, // 不用 bbox 的 maxX,因为 g-base 文字宽度预估偏差较大 y2: maxY + 1, }, { stroke: linkFillColor, lineWidth: 1 }, @@ -287,11 +287,14 @@ export abstract class BaseCell extends Group { styleKey === 'borderWidth' ) { if (isNumber(style)) { + const { horizontalBorderWidth, verticalBorderWidth } = + this.theme.dataCell.cell; + const marginStyle = { - x: x + style / 2, - y: y + style / 2, - width: width - style - 1, - height: height - style - 1, + x: x + verticalBorderWidth / 2 + style / 2, + y: y + horizontalBorderWidth / 2 + style / 2, + width: width - verticalBorderWidth - style, + height: height - horizontalBorderWidth - style, }; each(marginStyle, (currentStyle, currentStyleKey) => { updateShapeAttr(shape, currentStyleKey, currentStyle); diff --git a/packages/s2-core/src/cell/col-cell.ts b/packages/s2-core/src/cell/col-cell.ts index 1a331126e5..ca48b9e3e8 100644 --- a/packages/s2-core/src/cell/col-cell.ts +++ b/packages/s2-core/src/cell/col-cell.ts @@ -256,7 +256,13 @@ export class ColCell extends HeaderCell { } protected drawHorizontalResizeArea() { - if (!this.shouldDrawResizeAreaByType('colCellVertical', this)) { + // 隐藏列头时不绘制水平热区 https://github.com/antvis/S2/issues/1603 + const isHiddenCol = this.spreadsheet.options.style?.colCfg?.height === 0; + + if ( + isHiddenCol || + !this.shouldDrawResizeAreaByType('colCellVertical', this) + ) { return; } @@ -349,7 +355,7 @@ export class ColCell extends HeaderCell { return; } - const { label, width, height, parent } = this.meta; + const { label, width, height } = this.meta; const resizeStyle = this.getResizeAreaStyle(); const resizeArea = this.getColResizeArea(); @@ -368,7 +374,7 @@ export class ColCell extends HeaderCell { theme: resizeStyle, type: ResizeDirectionType.Horizontal, effect: ResizeAreaEffect.Cell, - id: parent.isTotals ? '' : label, + id: label, offsetX, offsetY, width, diff --git a/packages/s2-core/src/cell/corner-cell.ts b/packages/s2-core/src/cell/corner-cell.ts index 7bdaff60d8..d86f1e37b1 100644 --- a/packages/s2-core/src/cell/corner-cell.ts +++ b/packages/s2-core/src/cell/corner-cell.ts @@ -38,11 +38,7 @@ import { getResizeAreaAttrs, } from '../utils/interaction/resize'; import { isIPhoneX } from '../utils/is-mobile'; -import { - getEllipsisText, - getEmptyPlaceholder, - measureTextWidth, -} from '../utils/text'; +import { getEllipsisText, getEmptyPlaceholder } from '../utils/text'; import { i18n } from './../common/i18n'; import { shouldAddResizeArea } from './../utils/interaction/resize'; import { HeaderCell } from './header-cell'; @@ -86,7 +82,9 @@ export class CornerCell extends HeaderCell { this.meta, this.spreadsheet.options.placeholder, ); + const { measureTextWidth } = this.spreadsheet; const text = getEllipsisText({ + measureTextWidth, text: cornerText, maxWidth, fontParam: textStyle, @@ -106,6 +104,7 @@ export class CornerCell extends HeaderCell { secondLine = cornerText.slice(lastIndex); // 第二行重新计算...逻辑 secondLine = getEllipsisText({ + measureTextWidth, text: secondLine, maxWidth, fontParam: textStyle, diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 2ae56e35e7..93737ef30e 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -13,7 +13,6 @@ import type { Condition, Conditions, FormatResult, - Formatter, IconCfg, IconCondition, MappingResult, @@ -219,15 +218,11 @@ export class DataCell extends BaseCell { protected getFormattedFieldValue(): FormatResult { const { rowId, valueField, fieldValue, data } = this.meta; const rowMeta = this.spreadsheet.dataSet.getFieldMeta(rowId); - let formatter: Formatter; - if (rowMeta) { - // format by row field - formatter = this.spreadsheet.dataSet.getFieldFormatter(rowId); - } else { - // format by value field - formatter = this.spreadsheet.dataSet.getFieldFormatter(valueField); - } - const formattedValue = formatter(fieldValue, data); + const fieldId = rowMeta ? rowId : valueField; + const formatter = this.spreadsheet.dataSet.getFieldFormatter(fieldId); + // TODO: 这里只用 formatter(fieldValue, this.meta) 即可, 为了保持兼容, 暂时在第三个参入传入 meta 信息 + const formattedValue = formatter(fieldValue, data, this.meta); + return { value: fieldValue, formattedValue, diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index c7f7f1cb59..53de6358bd 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -64,10 +64,8 @@ export abstract class HeaderCell extends BaseCell { this.actionIcons = []; } - // 这个的 getFormattedFieldValue 主要是 row 和 col header 的格式化,这里不需要传递 data info protected getFormattedFieldValue(): FormatResult { const { label } = this.meta; - let content = label; const formatter = this.spreadsheet.dataSet.getFieldFormatter( this.meta.field, @@ -75,12 +73,13 @@ export abstract class HeaderCell extends BaseCell { const isTableMode = this.spreadsheet.isTableMode(); // 如果是 table mode,列头不需要被格式化 - if (formatter && !isTableMode) { - content = formatter(label); - } + const formattedValue = + formatter && !isTableMode + ? formatter(label, undefined, this.meta) + : label; return { - formattedValue: content, + formattedValue, value: label, }; } diff --git a/packages/s2-core/src/cell/table-col-cell.ts b/packages/s2-core/src/cell/table-col-cell.ts index 9690b58f06..090895ba3f 100644 --- a/packages/s2-core/src/cell/table-col-cell.ts +++ b/packages/s2-core/src/cell/table-col-cell.ts @@ -50,12 +50,32 @@ export class TableColCell extends ColCell { ); } - protected getColResizeArea() { - const isFrozenCell = this.isFrozenCell(); + protected shouldAddVerticalResizeArea() { + if (this.isFrozenCell()) { + return true; + } + return super.shouldAddVerticalResizeArea(); + } + + protected getVerticalResizeAreaOffset() { + const { x, y } = this.meta; + const { scrollX, position } = this.headerConfig; - if (!isFrozenCell) { - return super.getColResizeArea(); + if (this.isFrozenCell()) { + return { + x, + y, + }; } + return { + x: position.x + x - scrollX, + y: position.y + y, + }; + } + + protected getColResizeArea() { + const isFrozenCell = this.isFrozenCell(); + if (!isFrozenCell) return super.getColResizeArea(); return getOrCreateResizeAreaGroupById( this.spreadsheet, KEY_GROUP_FROZEN_COL_RESIZE_AREA, diff --git a/packages/s2-core/src/cell/table-data-cell.ts b/packages/s2-core/src/cell/table-data-cell.ts index 1eba82faef..bb2de2c3c9 100644 --- a/packages/s2-core/src/cell/table-data-cell.ts +++ b/packages/s2-core/src/cell/table-data-cell.ts @@ -1,5 +1,18 @@ import { DataCell } from '../cell/data-cell'; - +import { + KEY_GROUP_FROZEN_ROW_RESIZE_AREA, + KEY_GROUP_ROW_RESIZE_AREA, + ResizeAreaEffect, + ResizeDirectionType, +} from '../common/constant'; +import { + isFrozenRow as isFrozenRowUtil, + isFrozenTrailingRow as isFrozenTrailingRowUtil, +} from '../facet/utils'; +import { + getOrCreateResizeAreaGroupById, + getResizeAreaAttrs, +} from '../utils/interaction/resize'; export class TableDataCell extends DataCell { protected drawTextShape() { super.drawTextShape(); @@ -22,4 +35,67 @@ export class TableDataCell extends DataCell { this.drawLeftBorder(); } } + + public shouldDrawResizeArea() { + // 只有最左侧的单元格需要绘制resize区域 + return this.meta.colIndex === 0; + } + + public drawResizeArea() { + if (!this.shouldDrawResizeArea()) { + return; + } + const { x, y, width, height } = this.getCellArea(); + const rowIndex = this.meta.rowIndex; + const resizeStyle = this.getResizeAreaStyle(); + const { frozenRowCount, frozenTrailingRowCount } = this.spreadsheet.options; + const cellRange = this.spreadsheet.facet.getCellRange(); + const isFrozenRow = isFrozenRowUtil( + rowIndex, + cellRange.start, + frozenRowCount, + ); + const isFrozenTrailingRow = isFrozenTrailingRowUtil( + rowIndex, + cellRange.end, + frozenTrailingRowCount, + ); + const isFrozen = isFrozenRow || isFrozenTrailingRow; + const resizeAreaId = isFrozen + ? KEY_GROUP_FROZEN_ROW_RESIZE_AREA + : KEY_GROUP_ROW_RESIZE_AREA; + const resizeArea = getOrCreateResizeAreaGroupById( + this.spreadsheet, + resizeAreaId, + ); + const colHeight = this.spreadsheet.facet.layoutResult.colsHierarchy.height; + const { scrollY: sy } = this.spreadsheet.facet.getScrollOffset(); + const paginationSy = this.spreadsheet.facet.getPaginationScrollY(); + const scrollY = sy + paginationSy; + + let yOffset = y + (isFrozenTrailingRow ? 0 : colHeight); + + if (!isFrozenTrailingRow) { + yOffset -= isFrozenRow ? paginationSy : scrollY; + } + + resizeArea.addShape('rect', { + attrs: { + ...getResizeAreaAttrs({ + id: String(this.meta.rowIndex), + theme: resizeStyle, + type: ResizeDirectionType.Vertical, + effect: ResizeAreaEffect.Cell, + offsetX: x, + offsetY: yOffset, + width, + height, + meta: this.meta, + }), + x, + y: yOffset + height - resizeStyle.size / 2, + width, + }, + }); + } } diff --git a/packages/s2-core/src/cell/table-series-cell.ts b/packages/s2-core/src/cell/table-series-cell.ts index 9b600531a4..506c3818d5 100644 --- a/packages/s2-core/src/cell/table-series-cell.ts +++ b/packages/s2-core/src/cell/table-series-cell.ts @@ -1,22 +1,8 @@ -import { DataCell } from '../cell/data-cell'; -import { - CellTypes, - KEY_GROUP_FROZEN_ROW_RESIZE_AREA, - KEY_GROUP_ROW_RESIZE_AREA, - ResizeAreaEffect, - ResizeDirectionType, -} from '../common/constant'; +import { CellTypes } from '../common/constant'; import type { TextTheme } from '../common/interface'; -import { - isFrozenRow as isFrozenRowUtil, - isFrozenTrailingRow as isFrozenTrailingRowUtil, -} from '../facet/utils'; -import { - getOrCreateResizeAreaGroupById, - getResizeAreaAttrs, -} from '../utils/interaction/resize'; +import { TableDataCell } from './table-data-cell'; -export class TableSeriesCell extends DataCell { +export class TableSeriesCell extends TableDataCell { public get cellType() { return CellTypes.ROW_CELL; } @@ -24,66 +10,4 @@ export class TableSeriesCell extends DataCell { protected getTextStyle(): TextTheme { return this.theme.rowCell.seriesText; } - - protected drawBorderShape() { - super.drawBorderShape(); - if (this.meta.colIndex === 0) { - this.drawLeftBorder(); - } - } - - public drawResizeArea() { - const { x, y, width, height } = this.getCellArea(); - const rowIndex = this.meta.rowIndex; - const resizeStyle = this.getResizeAreaStyle(); - const { frozenRowCount, frozenTrailingRowCount } = this.spreadsheet.options; - const cellRange = this.spreadsheet.facet.getCellRange(); - const isFrozenRow = isFrozenRowUtil( - rowIndex, - cellRange.start, - frozenRowCount, - ); - const isFrozenTrailingRow = isFrozenTrailingRowUtil( - rowIndex, - cellRange.end, - frozenTrailingRowCount, - ); - const isFrozen = isFrozenRow || isFrozenTrailingRow; - const resizeAreaId = isFrozen - ? KEY_GROUP_FROZEN_ROW_RESIZE_AREA - : KEY_GROUP_ROW_RESIZE_AREA; - const resizeArea = getOrCreateResizeAreaGroupById( - this.spreadsheet, - resizeAreaId, - ); - const colHeight = this.spreadsheet.facet.layoutResult.colsHierarchy.height; - const { scrollY: sy } = this.spreadsheet.facet.getScrollOffset(); - const paginationSy = this.spreadsheet.facet.getPaginationScrollY(); - const scrollY = sy + paginationSy; - - let yOffset = y + (isFrozenTrailingRow ? 0 : colHeight); - - if (!isFrozenTrailingRow) { - yOffset -= isFrozenRow ? paginationSy : scrollY; - } - - resizeArea.addShape('rect', { - attrs: { - ...getResizeAreaAttrs({ - id: String(this.meta.rowIndex), - theme: resizeStyle, - type: ResizeDirectionType.Vertical, - effect: ResizeAreaEffect.Cell, - offsetX: x, - offsetY: yOffset, - width, - height, - meta: this.meta, - }), - x, - y: yOffset + height - resizeStyle.size / 2, - width, - }, - }); - } } diff --git a/packages/s2-core/src/common/constant/index.ts b/packages/s2-core/src/common/constant/index.ts index c8c2af6880..bde765ca76 100644 --- a/packages/s2-core/src/common/constant/index.ts +++ b/packages/s2-core/src/common/constant/index.ts @@ -11,3 +11,4 @@ export * from './theme'; export * from './tooltip'; export * from './resize'; export * from './copy'; +export * from './pagination'; diff --git a/packages/s2-core/src/common/constant/options.ts b/packages/s2-core/src/common/constant/options.ts index 8eb533bf34..9b8097534d 100644 --- a/packages/s2-core/src/common/constant/options.ts +++ b/packages/s2-core/src/common/constant/options.ts @@ -15,10 +15,11 @@ export enum LayoutWidthTypes { Compact = 'compact', } +export const DEFAULT_TREE_ROW_WIDTH = 120; + export const DEFAULT_STYLE: Readonly