diff --git a/package.json b/package.json index 81b0b26db0..ae560037e2 100644 --- a/package.json +++ b/package.json @@ -167,5 +167,8 @@ "vue-jest": "^5.0.0-alpha.10" }, "license": "MIT", - "repository": "git@github.com:antvis/S2.git" + "repository": { + "type": "git", + "url": "https://github.com/antvis/S2.git" + } } diff --git a/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx b/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx index 341858fe8e..ca339c07fc 100644 --- a/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx +++ b/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { waitFor } from '@testing-library/react'; import { SheetComponent, type SheetComponentsProps } from '../../src'; import * as mockDataConfig from '../data/simple-data.json'; import { getContainer, renderComponent } from '../util/helpers'; @@ -31,7 +32,7 @@ describe('Spread Sheet Tests', () => { container?.remove(); }); - test('should display scroll bar if s2Options.width more than browser window width', () => { + test('should display scroll bar if s2Options.width more than browser window width', async () => { renderComponent( { container, ); - expect(hasScrollBar(container)).toBeTruthy(); + await waitFor(() => { + expect(hasScrollBar(container)).toBeTruthy(); + }); }); test.skip('should hidden scroll bar if window width more than s2Options.width', () => { @@ -53,5 +56,30 @@ describe('Spread Sheet Tests', () => { expect(hasScrollBar(container)).toBeFalsy(); }); + + test('should only mount container once in strict mode for React 18', async () => { + // eslint-disable-next-line no-console + console.table(process.env); + const onMounted = jest.fn(); + + renderComponent( + + + , + , + container, + ); + + await waitFor(() => { + expect( + Array.from(document.querySelectorAll('.antv-s2-container canvas')), + ).toHaveLength(1); + expect(onMounted).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index ed74e8323d..cb75e07241 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -48,11 +48,12 @@ import reactPkg from '../package.json'; import type { SheetComponentOptions } from '../src'; import { SheetComponent } from '../src'; import { ConfigProvider } from '../src/components/config-provider'; -import { PlaygroundContext } from './context/playground.context'; import { CustomGrid } from './components/CustomGrid'; import { CustomTree } from './components/CustomTree'; import { EditableSheet } from './components/EditableSheet'; import { GridAnalysisSheet } from './components/GridAnalysisSheet'; +import { MobileSheetComponent } from './components/Mobile'; +import { PluginsSheet } from './components/Plugins'; import { ResizeConfig } from './components/ResizeConfig'; import { StrategySheet } from './components/StrategySheet'; import { Links } from './components/links'; @@ -67,16 +68,13 @@ import { tableSheetMultipleColumns, tableSheetSingleColumns, } from './config'; +import { PlaygroundContext } from './context/playground.context'; import { partDrillDown } from './drill-down'; -import { MobileSheetComponent } from './components/Mobile'; import { onSheetMounted } from './utils'; -import { PluginsSheet } from './components/Plugins'; import './index.less'; type TableSheetColumnType = 'single' | 'multiple'; -import './index.less'; - const CustomTooltip = () => (
自定义 Tooltip
1
@@ -247,6 +245,8 @@ function MainLayout() { }, [sheetType]); React.useEffect(() => { + console.log('env:', process.env); + if (sheetType !== 'table') { return; } @@ -373,938 +373,1001 @@ function MainLayout() { label: '基础表', children: ( <> - - - - - - 透视表 - 明细表 - - - {sheetType === 'table' && ( - - - - 单列头 - - - 多列头 - - - - )} - - - - 行列等宽 - - - 列等宽 - - 紧凑 - - - - - - - { - updateOptions({ debug: checked }); - }} - /> - { - updateOptions({ - hierarchyType: checked ? 'tree' : 'grid', - }); - }} - disabled={sheetType === 'table'} - /> - { - updateDataCfg({ - fields: { - valueInCols: checked, - }, - }); - }} - disabled={sheetType === 'table'} - /> - { - updateOptions({ - style: { - colCell: { - hideValue: checked, - }, - }, - }); - }} - disabled={sheetType === 'table'} - /> - { - updateOptions({ - totals: { - row: { - showGrandTotals: checked, - showSubTotals: checked, - reverseGrandTotalsLayout: true, - reverseSubTotalsLayout: true, - subTotalsDimensions: ['province'], - }, - }, - }); - }} - disabled={sheetType === 'table'} - /> - { - updateOptions({ - totals: { - col: { - showGrandTotals: checked, - showSubTotals: checked, - reverseGrandTotalsLayout: true, - reverseSubTotalsLayout: true, - subTotalsDimensions: ['type'], - }, - }, - }); - }} - disabled={sheetType === 'table'} - /> - - { - updateOptions({ - frozen: { - rowHeader: checked, - }, - }); - }} - disabled={sheetType === 'table'} - /> - - - { - if (checked) { - updateOptions({ - frozen: TableSheetFrozenOptions, - }); - } else { - updateOptions({ - frozen: { - rowCount: 0, - colCount: 0, - trailingColCount: 0, - trailingRowCount: 0, - }, - }); - } - }} - disabled={sheetType === 'pivot'} - /> - - { - updateOptions({ - showSeriesNumber: checked, - }); - }} - /> - { - updateOptions({ - seriesNumberText: checked - ? '自定义序号文本' - : getDefaultSeriesNumberText(), - }); - }} - disabled={!mergedOptions.showSeriesNumber} - /> - - - { - updateOptions({ - showDefaultHeaderActionIcon: checked, - }); - }} - /> - { - updateOptions({ - interaction: { - linkFields: checked - ? ['province', 'city'] - : [], - }, - }); - }} - /> - - { - updateOptions({ - style: { - colCell: { - height: checked - ? 0 - : s2Options?.style?.colCell?.height ?? - DEFAULT_STYLE.colCell?.height, - }, - }, - }); - }} - /> - - - { - setDataCfg( - customMerge(dataCfg, { - fields: { - columns: checked - ? [] - : pivotSheetDataCfg.fields.columns, - }, - }), - ); - }} - /> - - { - updateOptions({ - conditions: checked - ? s2ConditionsOptions - : null, - }); - }} - /> - - - - 主题配置 - - - - - 默认 - 简约灰 - - 多彩蓝 - - 暗黑 - - - - { - setThemeColor(color.hex); - const palette = getPalette(themeCfg.name); - const newPalette = generatePalette({ - ...palette, - brandColor: color.hex, + + + + + + 透视表 + + + 明细表 + + + + {sheetType === 'table' && ( + + + + 单列头 + + + 多列头 + + + + )} + + + + 行列等宽 + + + 列等宽 + + + 紧凑 + + + + + + + + { + updateOptions({ debug: checked }); + }} + /> + { + updateOptions({ + hierarchyType: checked ? 'tree' : 'grid', }); - - setThemeCfg({ - name: themeCfg.name, - palette: newPalette, + }} + disabled={sheetType === 'table'} + /> + { + updateDataCfg({ + fields: { + valueInCols: checked, + }, }); }} + disabled={sheetType === 'table'} /> - - } - > - - - - - - Tooltip 配置 - - - { - updateOptions({ - tooltip: { - enable: checked, - }, - }); - }} - /> - - - { - updateOptions({ - tooltip: { - operation: { - menu: { - mode: checked - ? 'horizontal' - : 'vertical', + { + updateOptions({ + style: { + colCell: { + hideValue: checked, + }, }, - }, - }, - }); - }} - /> - - - - - - - - 宽高配置 - - - - - - - - - - 滚动 - - - -
- 水平滚动速率 : - + { + updateOptions({ + totals: { + row: { + showGrandTotals: checked, + showSubTotals: checked, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province'], + }, + }, + }); + }} + disabled={sheetType === 'table'} + /> + { + updateOptions({ + totals: { + col: { + showGrandTotals: checked, + showSubTotals: checked, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['type'], + }, + }, + }); + }} + disabled={sheetType === 'table'} + /> + + { + updateOptions({ + frozen: { + rowHeader: checked, + }, + }); + }} + disabled={sheetType === 'table'} /> - 垂直滚动速率 : - + + { + if (checked) { + updateOptions({ + frozen: TableSheetFrozenOptions, + }); + } else { + updateOptions({ + frozen: { + rowCount: 0, + colCount: 0, + trailingColCount: 0, + trailingRowCount: 0, + }, + }); + } + }} + disabled={sheetType === 'pivot'} /> -
- - } - > - -
- - - - - - - + +
+ + + Tooltip 配置 + + + { + updateOptions({ + tooltip: { + enable: checked, + }, + }); + }} + /> + + + { + updateOptions({ + tooltip: { + operation: { + menu: { + mode: checked + ? 'horizontal' + : 'vertical', + }, + }, + }, + }); + }} + /> + + + + + + + + 宽高配置 + + + + + + + + + + 滚动 + + + +
+ 水平滚动速率 : + + 垂直滚动速率 : + +
+ + } + > + +
+ + + + + + + -
- - - 折叠 / 展开 - - - - { - updateOptions({ - style: { - rowCell: { - collapseAll: checked, - collapseFields: null, - expandDepth: null, - }, - }, - }); - }} - /> - - { - updateOptions({ - style: { - rowCell: { - collapseAll: null, - expandDepth: null, - collapseFields: { - 'root[&]浙江省': checked, - }, - }, - }, - }); - }} - /> - 透视表树状模式默认行头展开层级配置

} - > - -
-
-
- - - - { - updateOptions({ - interaction: { - selectedCellsSpotlight: checked, - }, - }); - }} - /> - - - - - - { - updateOptions({ - interaction: { - hoverHighlight: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - hoverFocus: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - autoResetSheetStyle: checked, - }, - }); - }} - /> - - -

默认隐藏列

-

明细表: 列头指定 field: number

-

- 透视表: 列头指定id: - root[&]家具[&]沙发[&]number -

- - } - > - -
-
-
- - - -
- {render && ( - - {reactPkg.name} playground - + {scrollTimer.current + ? '停止滚动' + : '循环滚动'} + + + + + 折叠 / 展开 + + + + { + updateOptions({ + style: { + rowCell: { + collapseAll: checked, + collapseFields: null, + expandDepth: null, + }, + }, + }); + }} + /> + + { + updateOptions({ + style: { + rowCell: { + collapseAll: null, + expandDepth: null, + collapseFields: { + 'root[&]浙江省': checked, + }, + }, + }, + }); + }} + /> + 透视表树状模式默认行头展开层级配置

+ } + > + +
+
+ ), - description: ( + }, + { + key: 'interaction', + label: '交互配置', + children: ( - - {reactPkg.name}: {reactPkg.version} - - - {corePkg.name}: {corePkg.version} - - - antd: {AntdVersion} - - - react: {React.version} - - - lang: {getLang()} - + + { + updateOptions({ + interaction: { + selectedCellsSpotlight: checked, + }, + }); + }} + /> + + + + + + { + updateOptions({ + interaction: { + hoverHighlight: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + hoverFocus: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + autoResetSheetStyle: checked, + }, + }); + }} + /> + + +

默认隐藏列

+

明细表: 列头指定 field: number

+

+ 透视表: 列头指定id: + root[&]家具[&]沙发[&]number +

+ + } + > + +
), - switcherCfg: { open: true }, - exportCfg: { open: true }, - advancedSortCfg: { - open: true, - }, - }} - onAfterRender={logHandler('onAfterRender')} - onRangeSort={logHandler('onRangeSort')} - onRangeSorted={logHandler('onRangeSorted')} - onMounted={onSheetMounted} - onDestroy={onSheetDestroy} - onColCellClick={onColCellClick} - onRowCellClick={logHandler('onRowCellClick')} - onCornerCellClick={logHandler( - 'onCornerCellClick', - (cellInfo) => { - if (!showCustomTooltip) { - return; - } + }, + { + key: 'resize', + label: '宽高调整热区配置', + children: ( + + ), + }, + ]} + /> + {render && ( + + + {reactPkg.name} playground + + ), + description: ( + + + {reactPkg.name}: {reactPkg.version} + + + {corePkg.name}: {corePkg.version} + + + antd: {AntdVersion} + + + react: {React.version} + + + lang: {getLang()} + + + ), + switcherCfg: { open: true }, + exportCfg: { open: true }, + advancedSortCfg: { + open: true, + }, + }} + onAfterRender={logHandler('onAfterRender')} + onRangeSort={logHandler('onRangeSort')} + onRangeSorted={logHandler('onRangeSorted')} + onMounted={onSheetMounted} + onDestroy={onSheetDestroy} + onColCellClick={onColCellClick} + onRowCellClick={logHandler('onRowCellClick')} + onCornerCellClick={logHandler( + 'onCornerCellClick', + (cellInfo) => { + if (!showCustomTooltip) { + return; + } - s2Ref.current?.showTooltip({ - position: { - x: cellInfo.event.clientX, - y: cellInfo.event.clientY, - }, - content: 'click', - }); - }, - )} - onDataCellClick={logHandler('onDataCellClick')} - onLayoutResize={logHandler('onLayoutResize')} - onCopied={logHandler('onCopied')} - onColCellHidden={logHandler('onColCellHidden')} - onColCellExpanded={logHandler('onColCellExpanded')} - onSelected={logHandler('onSelected')} - onScroll={logHandler('onScroll')} - onRowCellScroll={logHandler('onRowCellScroll')} - onLinkFieldJump={logHandler('onLinkFieldJump', () => { - window.open( - 'https://s2.antv.antgroup.com/zh/docs/manual/advanced/interaction/link-jump#%E6%A0%87%E8%AE%B0%E9%93%BE%E6%8E%A5%E5%AD%97%E6%AE%B5', - ); - })} - onDataCellBrushSelection={logHandler( - 'onDataCellBrushSelection', - )} - onColCellBrushSelection={logHandler( - 'onColCellBrushSelection', - )} - onRowCellBrushSelection={logHandler( - 'onRowCellBrushSelection', - )} - onRowCellCollapsed={logHandler('onRowCellCollapsed')} - onRowCellAllCollapsed={logHandler( - 'onRowCellAllCollapsed', - )} - /> + s2Ref.current?.showTooltip({ + position: { + x: cellInfo.event.clientX, + y: cellInfo.event.clientY, + }, + content: 'click', + }); + }, + )} + onDataCellClick={logHandler('onDataCellClick')} + onLayoutResize={logHandler('onLayoutResize')} + onCopied={logHandler('onCopied')} + onColCellHidden={logHandler('onColCellHidden')} + onColCellExpanded={logHandler('onColCellExpanded')} + onSelected={logHandler('onSelected')} + onScroll={logHandler('onScroll')} + onRowCellScroll={logHandler('onRowCellScroll')} + onLinkFieldJump={logHandler('onLinkFieldJump', () => { + window.open( + 'https://s2.antv.antgroup.com/zh/docs/manual/advanced/interaction/link-jump#%E6%A0%87%E8%AE%B0%E9%93%BE%E6%8E%A5%E5%AD%97%E6%AE%B5', + ); + })} + onDataCellBrushSelection={logHandler( + 'onDataCellBrushSelection', + )} + onColCellBrushSelection={logHandler( + 'onColCellBrushSelection', + )} + onRowCellBrushSelection={logHandler( + 'onRowCellBrushSelection', + )} + onRowCellCollapsed={logHandler('onRowCellCollapsed')} + onRowCellAllCollapsed={logHandler( + 'onRowCellAllCollapsed', + )} + /> + )} ), diff --git a/packages/s2-react/src/hooks/useSpreadSheet.ts b/packages/s2-react/src/hooks/useSpreadSheet.ts index d1066efd0e..6c77d38c13 100644 --- a/packages/s2-react/src/hooks/useSpreadSheet.ts +++ b/packages/s2-react/src/hooks/useSpreadSheet.ts @@ -18,6 +18,15 @@ export function useSpreadSheet(props: SheetComponentsProps) { const s2Ref = React.useRef(null); const containerRef = React.useRef(null); const wrapperRef = React.useRef(null); + const shouldInit = React.useRef(true); + + const isDevMode = React.useMemo(() => { + try { + return process.env['NODE_ENV'] !== 'production'; + } catch { + return false; + } + }, []); const { spreadsheet: customSpreadSheet, @@ -58,15 +67,15 @@ export function useSpreadSheet(props: SheetComponentsProps) { const buildSpreadSheet = React.useCallback(async () => { setLoading(true); - const spreadsheet = renderSpreadSheet(containerRef.current!); + const s2 = renderSpreadSheet(containerRef.current!); - spreadsheet.setThemeCfg(props.themeCfg); - await spreadsheet.render(); + s2.setThemeCfg(props.themeCfg); + await s2.render(); setLoading(false); - s2Ref.current = spreadsheet; + s2Ref.current = s2; - /* - * 子 hooks 内使用了 s2Ref.current 作为 dep + /** + * 子 hooks 内使用了 s2Ref.current 作为 deps * forceUpdate 一下保证子 hooks 能 rerender */ forceUpdate(); @@ -74,9 +83,14 @@ export function useSpreadSheet(props: SheetComponentsProps) { props.onMounted?.(s2Ref.current); }, [props, renderSpreadSheet, setLoading, forceUpdate]); - // init React.useEffect(() => { + // 兼容 React 18 StrictMode 开发环境下渲染两次 + if (isDevMode && !shouldInit.current) { + return; + } + buildSpreadSheet(); + shouldInit.current = false; return () => { s2Ref.current?.destroy?.();