diff --git a/package.json b/package.json index f03c70a303..5c777cfebe 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "build:umd": "pnpm -r --filter './packages/*' --stream build:umd", "build:size-limit": "pnpm -r --filter './packages/*' --stream build:size-limit", "build:size-limit-json": "pnpm -r --filter './packages/*' --stream build:size-limit-json", - "release": "pnpm -r --filter !@antv/s2-shared --filter !@antv/s2-site --workspace-concurrency=1 exec npx --no-install semantic-release", + "release": "pnpm -r --filter !@antv/s2-shared --filter !@antv/s2-site --filter !@antv/s2-react-components --workspace-concurrency=1 exec npx --no-install semantic-release", "release:preview": "pnpm release --dry-run --no-ci", "release:bump-version": "node ./scripts/bump-version.js", "test": "pnpm -r --filter './packages/*' --stream test", diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index 7b3d296644..0eb22ef77e 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -353,18 +353,21 @@ export interface S2PivotSheetOptions { cornerExtraFieldText?: string; } -export interface S2Options< - T = TooltipContentType, - P = Pagination, - Menu = BaseTooltipOperatorMenuOptions, -> extends S2BasicOptions, - S2PivotSheetOptions { +export interface S2FrozenOptions { /** * 行列冻结 */ frozen?: S2PivotSheetFrozenOptions & S2BaseFrozenOptions; } +export interface S2Options< + T = TooltipContentType, + P = Pagination, + Menu = BaseTooltipOperatorMenuOptions, +> extends S2BasicOptions, + S2PivotSheetOptions, + S2FrozenOptions {} + /** * 自定义渲染模式 */ diff --git a/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/__snapshots__/index-spec.tsx.snap b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/__snapshots__/index-spec.tsx.snap new file mode 100644 index 0000000000..1f0b362735 --- /dev/null +++ b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/__snapshots__/index-spec.tsx.snap @@ -0,0 +1,2112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Frozen Panel Component Tests should default collapse panel 1`] = ` + +
+
+ +
+
+
+`; + +exports[`Frozen Panel Component Tests should hidden frozen col 1`] = ` + +
+
+
+
+ + + +
+ + 冻结行列头 + +
+ + + + + + 重置 + + +
+
+
+
+
+ +
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+
+
+
+
+
+
+`; + +exports[`Frozen Panel Component Tests should hidden frozen row 1`] = ` + +
+
+
+
+ + + +
+ + 冻结行列头 + +
+ + + + + + 重置 + + +
+
+
+
+
+ +
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+
+
+
+
+
+
+`; + +exports[`Frozen Panel Component Tests should hidden frozen row header 1`] = ` + +
+
+
+
+ + + +
+ + 冻结行列头 + +
+ + + + + + 重置 + + +
+
+
+
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+
+
+
+
+
+
+`; + +exports[`Frozen Panel Component Tests should render correctly panel 1`] = ` + +
+
+
+
+ + + +
+ + 冻结行列头 + +
+ + + + + + 重置 + + +
+
+
+
+
+ +
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+
+
+
+
+
+
+`; + +exports[`Frozen Panel Component Tests should set custom input number props 1`] = ` + +
+
+
+
+ + + +
+ + 冻结行列头 + +
+ + + + + + 重置 + + +
+
+
+
+
+ +
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 行 +
+
+
+ + + 冻结前 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+ + 冻结后 +
+
+ + + + + + + + + + +
+
+ +
+
+ 列 +
+
+
+
+
+
+
+`; diff --git a/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/frozen-input-number/__snapshots__/index-spec.tsx.snap b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/frozen-input-number/__snapshots__/index-spec.tsx.snap new file mode 100644 index 0000000000..6cbb7c8463 --- /dev/null +++ b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/frozen-input-number/__snapshots__/index-spec.tsx.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Frozen Input Number Component Tests should render correctly panel 1`] = ` + +
+
+ + + + + + + + + + +
+
+ +
+
+
+`; + +exports[`Frozen Input Number Component Tests should set custom input number props 1`] = ` + +
+
+ + + + + + + + + + +
+
+ +
+
+
+`; diff --git a/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/frozen-input-number/index-spec.tsx b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/frozen-input-number/index-spec.tsx new file mode 100644 index 0000000000..81c3281b16 --- /dev/null +++ b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/frozen-input-number/index-spec.tsx @@ -0,0 +1,24 @@ +import { FrozenInputNumber } from '@/components'; +import { render } from '@testing-library/react'; +import React from 'react'; + +describe('Frozen Input Number Component Tests', () => { + test('should render correctly panel', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('should set custom input number props', () => { + const { asFragment } = render( + , + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/index-spec.tsx b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/index-spec.tsx new file mode 100644 index 0000000000..aae9207ba0 --- /dev/null +++ b/packages/s2-react-components/__tests__/unit/components/config/frozen-panel/index-spec.tsx @@ -0,0 +1,165 @@ +import { FrozenPanel } from '@/components'; +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +describe('Frozen Panel Component Tests', () => { + test('should render correctly panel', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + expect(screen.getByText('冻结行列头')).toBeDefined(); + expect(screen.getByText('冻结行头')).toBeDefined(); + expect(screen.getByText('冻结行')).toBeDefined(); + expect(screen.getByText('冻结列')).toBeDefined(); + }); + + test('should render custom panel title', () => { + const title = '自定义标题'; + + render(); + + expect(screen.getByText(title)).toBeDefined(); + }); + + test('should default collapse panel', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('should render custom content', () => { + const { container } = render( + + content + , + ); + + expect(container.querySelector('.custom-content')).toBeDefined(); + }); + + test('should hidden frozen row header', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('should hidden frozen row', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('should hidden frozen col', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('should set custom input number props', () => { + const { asFragment } = render( + , + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('should trigger onReset event', () => { + const onRest = jest.fn(); + + render(); + + fireEvent.click(screen.getByText('重置')); + + expect(onRest).toHaveReturnedTimes(1); + expect(onRest).toHaveBeenCalledWith( + { + frozenCol: [], + frozenRow: [], + frozenRowHeader: true, + }, + expect.anything(), + ); + }); + + test('should trigger onReset event with prev options', () => { + const onRest = jest.fn(); + + render(); + + fireEvent.click(screen.getByText('冻结行头')); + fireEvent.click(screen.getByText('冻结行')); + fireEvent.click(screen.getByText('冻结列')); + fireEvent.click(screen.getByText('重置')); + + expect(onRest).toHaveBeenCalledWith( + { + frozenCol: [], + frozenRow: [], + frozenRowHeader: true, + }, + { + frozenCol: [1, 1], + frozenRow: [1, 1], + frozenRowHeader: false, + }, + ); + }); + + test('should trigger onChange event', () => { + const onChange = jest.fn(); + + render(); + + fireEvent.click(screen.getByText('冻结行头')); + + expect(onChange).toHaveBeenLastCalledWith({ + frozenCol: [], + frozenRow: [], + frozenRowHeader: false, + }); + + fireEvent.click(screen.getByText('冻结行')); + + expect(onChange).toHaveBeenLastCalledWith({ + frozenCol: [], + frozenRow: [1, 1], + frozenRowHeader: false, + }); + + fireEvent.click(screen.getByText('冻结列')); + + expect(onChange).toHaveBeenLastCalledWith({ + frozenCol: [1, 1], + frozenRow: [1, 1], + frozenRowHeader: false, + }); + }); + + test('should disable input number', () => { + const { container } = render(); + + expect([ + ...container.querySelectorAll('.ant-input-number-disabled'), + ]).toHaveLength(4); + }); + + test('should enable input number', () => { + const { container } = render( + , + ); + + expect([ + ...container.querySelectorAll('.ant-input-number-disabled'), + ]).toHaveLength(0); + }); +}); diff --git a/packages/s2-react-components/playground/config.tsx b/packages/s2-react-components/playground/config.tsx index f5a278e142..8afe9ef91b 100644 --- a/packages/s2-react-components/playground/config.tsx +++ b/packages/s2-react-components/playground/config.tsx @@ -1,6 +1,6 @@ /* eslint-disable max-classes-per-file */ /* eslint-disable no-console */ -import { EMPTY_PLACEHOLDER, ResizeType, type S2DataConfig } from '@antv/s2'; +import { type S2DataConfig } from '@antv/s2'; import type { SheetComponentOptions } from '@antv/s2-react'; import { data, @@ -21,80 +21,21 @@ export const s2Options: SheetComponentOptions = { width: 800, height: 600, hierarchyType: 'grid', - placeholder: { - cell: EMPTY_PLACEHOLDER, - empty: { - icon: 'Empty', - description: '暂无数据', - }, - }, - seriesNumber: { - enable: false, - }, - transformCanvasConfig() { - return { - supportsCSSTransform: true, - // devicePixelRatio: 3, - // cursor: 'crosshair', - }; - }, frozen: { rowHeader: true, - // rowCount: 1, - // trailingRowCount: 1, - // colCount: 1, - // trailingColCount: 1, }, - cornerText: '测试测试测试测试测试测试测试测试测试测试', interaction: { - copy: { - enable: true, - withFormat: true, - withHeader: true, - }, - hoverAfterScroll: true, - hoverHighlight: true, - selectedCellHighlight: true, - selectedCellMove: true, - rangeSelection: true, // 防止 mac 触控板横向滚动触发浏览器返回, 和移动端下拉刷新 overscrollBehavior: 'none', - brushSelection: { - dataCell: true, - colCell: true, - rowCell: true, + }, + tooltip: {}, + style: { + colCell: { + width: 120, }, - resize: { - rowResizeType: ResizeType.ALL, - colResizeType: ResizeType.ALL, + dataCell: { + width: 200, + height: 100, }, }, - // totals: { - // col: { - // showGrandTotals: true, - // showSubTotals: false, - // reverseGrandTotalsLayout: true, - // reverseSubTotalsLayout: true, - // subTotalsDimensions: ['type'], - // }, - // row: { - // showGrandTotals: true, - // showSubTotals: true, - // reverseGrandTotalsLayout: true, - // reverseSubTotalsLayout: true, - // subTotalsDimensions: ['province'], - // }, - // }, - // mergedCellsInfo: [ - // [ - // { colIndex: 1, rowIndex: 1, showText: true }, - // { colIndex: 1, rowIndex: 2 }, - // ], - // [ - // { colIndex: 2, rowIndex: 1 }, - // { colIndex: 2, rowIndex: 2, showText: true }, - // ], - // ], - tooltip: {}, - style: {}, }; diff --git a/packages/s2-react-components/playground/index.tsx b/packages/s2-react-components/playground/index.tsx index 480d349d3f..51af4cd2d8 100644 --- a/packages/s2-react-components/playground/index.tsx +++ b/packages/s2-react-components/playground/index.tsx @@ -14,7 +14,7 @@ import { version as AntdVersion, Space, Tag } from 'antd'; import React from 'react'; import { createRoot } from 'react-dom/client'; import pkg from '../package.json'; -import { TextAlignPanel, ThemePanel } from '../src'; +import { FrozenPanel, TextAlignPanel, ThemePanel } from '../src'; import { s2DataConfig, s2Options } from './config'; import '@antv/s2-react/dist/style.min.css'; @@ -64,6 +64,36 @@ function MainLayout() { console.log('onReset:', options, prevOptions, theme); }} /> + { + const [rowCount = 0, trailingRowCount = 0] = options.frozenRow; + const [colCount = 0, trailingColCount = 0] = options.frozenCol; + + s2Ref.current?.setOptions({ + frozen: { + rowHeader: options.frozenRowHeader, + rowCount, + colCount, + trailingRowCount, + trailingColCount, + }, + }); + s2Ref.current?.render(false); + console.log('onChange:', options); + }} + onReset={(options, prevOptions) => { + console.log('onReset:', options, prevOptions); + }} + /> = React.memo( + (props) => { + const { disabled, value, onChange, ...attrs } = props; + const [inputValue, setInputValue] = React.useState( + value, + ); + + const onDebounceChange = React.useMemo(() => { + return debounce((nextValue) => { + onChange?.(nextValue); + }, 500); + }, []); + + React.useEffect(() => { + if (value !== inputValue) { + setInputValue(value); + } + + return () => { + onDebounceChange.cancel(); + }; + }, [value]); + + return ( + { + setInputValue(nextValue); + onDebounceChange(nextValue); + }} + /> + ); + }, +); + +FrozenInputNumber.displayName = 'FrozenInputNumber'; diff --git a/packages/s2-react-components/src/components/config/frozen-panel/frozen-input-number/interface.ts b/packages/s2-react-components/src/components/config/frozen-panel/frozen-input-number/interface.ts new file mode 100644 index 0000000000..95d1f937ce --- /dev/null +++ b/packages/s2-react-components/src/components/config/frozen-panel/frozen-input-number/interface.ts @@ -0,0 +1,7 @@ +import type { InputNumberProps } from 'antd'; + +export interface FrozenInputNumberProps + extends Omit { + value: number | null; + onChange?: (value: number) => void; +} diff --git a/packages/s2-react-components/src/components/config/frozen-panel/index.less b/packages/s2-react-components/src/components/config/frozen-panel/index.less new file mode 100644 index 0000000000..eddd64e553 --- /dev/null +++ b/packages/s2-react-components/src/components/config/frozen-panel/index.less @@ -0,0 +1,34 @@ +@import '@antv/s2/src/styles/variables.less'; + +.@{s2-cls-prefix}-frozen-panel { + width: 400px; + + &-container { + display: flex; + align-items: center; + justify-content: space-between; + + &:not(:last-of-type) { + margin-bottom: 6px; + } + + &-group { + color: rgba(0, 0, 0, 0.43); + + &:not(:last-of-type) { + margin-right: 12px; + } + } + + .ant-checkbox-wrapper { + margin-right: 50px; + color: #535455; + } + + .ant-input-number.@{s2-cls-prefix}-frozen-input-number { + width: 50px; + margin: 0 4px; + border-radius: 4px; + } + } +} diff --git a/packages/s2-react-components/src/components/config/frozen-panel/index.tsx b/packages/s2-react-components/src/components/config/frozen-panel/index.tsx new file mode 100644 index 0000000000..15dbf7e86b --- /dev/null +++ b/packages/s2-react-components/src/components/config/frozen-panel/index.tsx @@ -0,0 +1,141 @@ +import { S2_PREFIX_CLS, i18n } from '@antv/s2'; +import { Checkbox } from 'antd'; +import type { CheckboxChangeEvent } from 'antd/es/checkbox'; +import { isEmpty } from 'lodash'; +import React from 'react'; +import { ResetGroup } from '../../common'; +import { FrozenInputNumber } from './frozen-input-number'; +import './index.less'; +import type { FrozenPanelOptions, FrozenPanelProps } from './interface'; + +const PRE_CLASS = `${S2_PREFIX_CLS}-frozen-panel`; + +export const FrozenPanel: React.FC = React.memo((props) => { + const { + title = i18n('冻结行列头'), + defaultOptions: defaultTextAlignPanelOptions, + defaultCollapsed = false, + inputNumberProps, + showFrozenRowHeader = true, + showFrozenRow = true, + showFrozenCol = true, + children, + onChange, + onReset, + } = props; + const [options, setOptions] = React.useState({ + frozenRow: [], + frozenCol: [], + frozenRowHeader: true, + ...defaultTextAlignPanelOptions, + }); + const defaultOptions = React.useRef(options); + + const onResetClick = () => { + setOptions(defaultOptions.current); + onReset?.(defaultOptions.current, options); + }; + + const onRowHeaderChange = (event: CheckboxChangeEvent) => { + const newOptions: FrozenPanelOptions = { + ...options, + frozenRowHeader: event.target.checked, + }; + + setOptions(newOptions); + onChange?.(newOptions); + }; + + const BASE_FROZEN_CONFIG: Array<{ + field: keyof Omit; + suffix: string; + visible: boolean; + }> = [ + { + suffix: i18n('行'), + field: 'frozenRow' as const, + visible: showFrozenRow, + }, + { + suffix: i18n('列'), + field: 'frozenCol' as const, + visible: showFrozenCol, + }, + ].filter(({ visible }) => visible); + + return ( + + {showFrozenRowHeader && ( +
+ + {i18n('冻结行头')} + +
+ )} + {BASE_FROZEN_CONFIG.map((config) => { + const leadingCount = options[config.field]?.[0]; + const trailingCount = options[config.field]?.[1]; + const enable = !isEmpty(options[config.field]); + + const onGroupChange = (value: [number?, number?]) => { + const newOptions: FrozenPanelOptions = { + ...options, + [config.field]: value, + }; + + setOptions(newOptions); + onChange?.(newOptions); + }; + + return ( +
+ { + onGroupChange(event.target.checked ? [1, 1] : []); + }} + > + {i18n('冻结')} + {config.suffix} + + + {i18n('冻结前')} + { + onGroupChange([val, trailingCount]); + }} + {...inputNumberProps} + /> + {config.suffix} + + + {i18n('冻结后')} + { + onGroupChange([leadingCount, val]); + }} + {...inputNumberProps} + /> + {config.suffix} + +
+ ); + })} + {children} +
+ ); +}); + +FrozenPanel.displayName = 'FrozenPanel'; diff --git a/packages/s2-react-components/src/components/config/frozen-panel/interface.ts b/packages/s2-react-components/src/components/config/frozen-panel/interface.ts new file mode 100644 index 0000000000..1ea6b1d4a2 --- /dev/null +++ b/packages/s2-react-components/src/components/config/frozen-panel/interface.ts @@ -0,0 +1,44 @@ +import type { BaseComponentProps } from '../../../common/interface/components'; +import type { FrozenInputNumberProps } from './frozen-input-number/interface'; + +export interface FrozenPanelOptions { + frozenRowHeader?: boolean; + frozenRow: [number?, number?]; + frozenCol: [number?, number?]; +} + +export interface FrozenPanelProps + extends BaseComponentProps { + /** + * 透传参数 + */ + inputNumberProps?: Partial; + + /** + * 是否开启 [冻结行头] + */ + showFrozenRowHeader?: boolean; + + /** + * 是否开启 [冻结行] + */ + showFrozenRow?: boolean; + + /** + * 是否开启 [冻结列] + */ + showFrozenCol?: boolean; + + /** + * 选择 + */ + onChange?: (options: FrozenPanelOptions) => void; + + /** + * 重置 + */ + onReset?: ( + options: FrozenPanelOptions, + prevOptions: FrozenPanelOptions, + ) => void; +} diff --git a/packages/s2-react-components/src/components/config/index.ts b/packages/s2-react-components/src/components/config/index.ts index c9ec76c853..b9266ec293 100644 --- a/packages/s2-react-components/src/components/config/index.ts +++ b/packages/s2-react-components/src/components/config/index.ts @@ -2,6 +2,9 @@ export { ThemePanel } from './theme-panel'; export { ColorBox } from './theme-panel/color-box'; export { ColorPickerPanel } from './theme-panel/color-picker-panel'; +export { FrozenPanel } from './frozen-panel'; +export { FrozenInputNumber } from './frozen-panel/frozen-input-number'; + export { TextAlignPanel } from './text-align-panel'; export * from './theme-panel/interface'; diff --git a/packages/s2-shared/src/constant/i18n/en_US.ts b/packages/s2-shared/src/constant/i18n/en_US.ts index 4dae05aa97..d31ab091da 100644 --- a/packages/s2-shared/src/constant/i18n/en_US.ts +++ b/packages/s2-shared/src/constant/i18n/en_US.ts @@ -69,4 +69,12 @@ export const EN_US: Record = { 表头: 'Header', '表身 (维度)': 'Body(dimension)', '表身 (指标)': 'Body(measure)', + 冻结行列头: 'Frozen header', + 行: 'Row', + 列: 'Column', + 冻结行: 'Frozen row', + 冻结列: 'Frozen column', + 冻结: 'Frozen', + 冻结前: 'Freeze the first', + 冻结后: 'Freeze the last', }; diff --git a/packages/s2-shared/src/constant/i18n/zh_CN.ts b/packages/s2-shared/src/constant/i18n/zh_CN.ts index 59bc4a4086..713c7269be 100644 --- a/packages/s2-shared/src/constant/i18n/zh_CN.ts +++ b/packages/s2-shared/src/constant/i18n/zh_CN.ts @@ -69,4 +69,12 @@ export const ZH_CN: Record = { 表头: '表头', '表身 (维度)': '表身 (维度)', '表身 (指标)': '表身 (指标)', + 冻结行列头: '冻结行列头', + 行: '行', + 列: '列', + 冻结行: '冻结行', + 冻结列: '冻结列', + 冻结: '冻结', + 冻结前: '冻结前', + 冻结后: '冻结后', };