diff --git a/.stylelintrc b/.stylelintrc index 8b67eb4f29..1630080685 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,6 +1,6 @@ { "extends": ["@alifd/stylelint-config-next"], - "ignoreFiles": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/__docs__/**"], + "ignoreFiles": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/__docs__/**", "lib/**", "es/**"], "rules": { "max-nesting-depth": 4, "max-empty-lines": 3, diff --git a/components/config-provider/types.ts b/components/config-provider/types.ts index 33883593b2..39a70a3dca 100644 --- a/components/config-provider/types.ts +++ b/components/config-provider/types.ts @@ -35,7 +35,15 @@ export interface FallbackUIProps { export type FallbackUI = JSXElementConstructor; export interface ErrorBoundaryConfig { + /** + * 捕获错误后的行为,比如埋点上传 + * @en the behavior after catching the error, for example, upload error log + */ afterCatch?: AfterCatch; + /** + * 捕获错误后的展示 + * @en the display after catching the error + */ fallbackUI?: FallbackUI; } @@ -68,34 +76,37 @@ export interface ContextState { export interface ComponentCommonProps { /** * 样式类名的品牌前缀 + * @en Prefix of component className */ prefix?: string; /** * 组件的国际化文案对象 + * @en Locale object for components */ locale?: ComponentLocaleObject; /** * 是否开启 Pure Render 模式,会提高性能,但是也会带来副作用 + * @en Enable the Pure Render mode, it will improve performance, but it will also have side effects */ pure?: boolean; /** * 设备类型,针对不同的设备类型组件做出对应的响应式变化 + * @en device type, different device types components will make corresponding responsive changes */ device?: DeviceType; /** * 是否开启 rtl 模式 + * @en Enable right to left mode */ rtl?: boolean; /** * 是否开启错误捕捉 errorBoundary - * 如需自定义参数,请传入对象 对象接受参数列表如下: - * - * fallbackUI `Function(error?: {}, errorInfo?: {}) => Element` 捕获错误后的展示 - * afterCatch `Function(error?: {}, errorInfo?: {})` 捕获错误后的行为,比如埋点上传 + * @en Turn errorBoundary on or not */ errorBoundary?: ErrorBoundaryType; /** * 是否在开发模式下显示组件属性被废弃的 warning 提示 + * @en Whether to display the warning prompt for deprecated properties in development mode */ warning?: boolean; } diff --git a/components/locale/types.ts b/components/locale/types.ts index f4cb30870a..92c54e919b 100644 --- a/components/locale/types.ts +++ b/components/locale/types.ts @@ -116,14 +116,41 @@ export interface BaseLocale extends LocaleConfig { shortMaxTagPlaceholder: string; }; Table: { + /** + * 没有数据情况下 table 内的文案 + */ empty: string; + /** + * 过滤器中确认按钮文案 + */ ok: string; + /** + * 过滤器中重置按钮文案 + */ reset: string; + /** + * 排序升序状态下的文案 + */ asc: string; + /** + * 排序将序状态下的文案 + */ desc: string; + /** + * 可折叠行,展开状态下的文案 + */ expanded: string; + /** + * 可折叠行,折叠状态下的文案 + */ folded: string; + /** + * 过滤器文案 + */ filter: string; + /** + * header 里全选的按钮文案 + */ selectAll: string; }; TimePicker: { diff --git a/components/table/__docs__/adaptor/index.jsx b/components/table/__docs__/adaptor/index.jsx deleted file mode 100644 index e2a9300ce1..0000000000 --- a/components/table/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { Types, getChildren } from '@alifd/adaptor-helper'; -import { Table } from '@alifd/next'; - -export default { - name: 'Table', - editor: () => ({ - props: [{ - name: 'select', - label: 'Selection Mode', - type: Types.enum, - options: ['checkbox', 'none'], - default: 'none' - }, { - name: 'align', - type: Types.enum, - options: ['left', 'right', 'center'], - default: 'left' - }, { - name: 'head', - label: 'Header', - type: Types.enum, - options: ['show', 'hide'], - default: 'show' - }, { - name: 'border', - type: Types.bool, - default: false - }, { - name: 'stripe', - type: Types.bool, - default: false - }, { - name: 'width', - type: Types.number, - default: 800 - }], - data: { - default: 'Product | Price | Status\n2014 New Fashion Novelty Tank Slim Women\'s Fashion Dresses With Lace | US $2.5 | No Priced\nFree shipping women Casual dresses lady dress plus size 201 | US $2.5 | Already Priced' - } - }), - adaptor: ({ border, stripe, select, align, head, width, data, style, ...others }) => { - - const list = data.split('\n').filter(v => !!v); - let ths = []; - const bodys = []; - list.forEach((template, index) => { - if (index === 0) { - ths = template.split('|'); - } else { - const column = {}; - template.split('|').forEach((value, index) => { - column[`column_${index}`] = value; - }); - bodys.push(column); - } - }); - - return ( - {}} : null} style={{ width, ...style }} hasBorder={border} hasHeader={head === 'show'} isZebra={stripe}> - { - ths.map((label, index) => ( - value} align={align} title={label} key={`column_${index}`} dataIndex={`column_${index}`} /> - )) - } -
- ); - } -}; diff --git a/components/table/__docs__/adaptor/index.tsx b/components/table/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..fc24723973 --- /dev/null +++ b/components/table/__docs__/adaptor/index.tsx @@ -0,0 +1,108 @@ +import React, { type CSSProperties } from 'react'; +import { Types } from '@alifd/adaptor-helper'; +import { Table } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; + +export default { + name: 'Table', + editor: () => ({ + props: [ + { + name: 'select', + label: 'Selection Mode', + type: Types.enum, + options: ['checkbox', 'none'], + default: 'none', + }, + { + name: 'align', + type: Types.enum, + options: ['left', 'right', 'center'], + default: 'left', + }, + { + name: 'head', + label: 'Header', + type: Types.enum, + options: ['show', 'hide'], + default: 'show', + }, + { + name: 'border', + type: Types.bool, + default: false, + }, + { + name: 'stripe', + type: Types.bool, + default: false, + }, + { + name: 'width', + type: Types.number, + default: 800, + }, + ], + data: { + default: + "Product | Price | Status\n2014 New Fashion Novelty Tank Slim Women's Fashion Dresses With Lace | US $2.5 | No Priced\nFree shipping women Casual dresses lady dress plus size 201 | US $2.5 | Already Priced", + }, + }), + adaptor: ({ + border, + stripe, + select, + align, + head, + width, + data, + style, + ...others + }: { + border: TableProps['hasBorder']; + stripe: TableProps['isZebra']; + select: 'checkbox' | 'none'; + align: ColumnProps['align']; + head: 'show' | 'hide'; + width: number; + data: string; + style: CSSProperties; + }) => { + const list = data.split('\n').filter(v => !!v); + let ths: string[] = []; + const bodys: { [key: string]: string }[] = []; + list.forEach((template, index) => { + if (index === 0) { + ths = template.split('|'); + } else { + const column: { [key: string]: string } = {}; + template.split('|').forEach((value, index) => { + column[`column_${index}`] = value; + }); + bodys.push(column); + } + }); + + return ( + {} } : null} + style={{ width, ...style }} + hasBorder={border} + hasHeader={head === 'show'} + isZebra={stripe} + > + {ths.map((label, index) => ( + value} + align={align} + title={label} + key={`column_${index}`} + dataIndex={`column_${index}`} + /> + ))} +
+ ); + }, +}; diff --git a/components/table/__docs__/demo/accessibility/index.tsx b/components/table/__docs__/demo/accessibility/index.tsx index 02c66f5039..6f1010e68c 100644 --- a/components/table/__docs__/demo/accessibility/index.tsx +++ b/components/table/__docs__/demo/accessibility/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; +import type { ColumnProps } from '@alifd/next/types/table'; const result = [ { @@ -14,21 +15,17 @@ const result = [ title: { name: 'the great gatsby' }, }, { - id: '003', + id: '004', time: 1719, title: { name: 'The adventures of Robinson Crusoe' }, }, ]; class Demo extends React.Component { - constructor(props) { - super(props); - - this.state = { - dataSource: result, - }; - } + state = { + dataSource: result, + }; - onRemove = id => { + onRemove = (id: string) => { const { dataSource } = this.state; let index = -1; dataSource.forEach((item, i) => { @@ -44,8 +41,8 @@ class Demo extends React.Component { } }; - renderOper = (value, index, record) => { - return Remove({record.id}); + renderOper: ColumnProps['cell'] = (value, index, record) => { + return Remove({record!.id}); }; render() { return ( diff --git a/components/table/__docs__/demo/advanced/index.tsx b/components/table/__docs__/demo/advanced/index.tsx index e2094e281c..cb499f6adb 100644 --- a/components/table/__docs__/demo/advanced/index.tsx +++ b/components/table/__docs__/demo/advanced/index.tsx @@ -1,8 +1,10 @@ +/* eslint-disable react/prop-types */ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; import PropTypes from 'prop-types'; -/* eslint-disable react/no-multi-comp,react/prop-types */ +import type { TableProps } from '@alifd/next/types/table'; + const { Header, Cell } = Table; const dataSource = () => { const result = []; @@ -16,10 +18,10 @@ const dataSource = () => { return result; }; -const AppHeader = (props, context) => { +const AppHeader: NonNullable['Header'] = (props, context) => { const { columns } = props; const { onChange } = context; - const length = columns[columns.length - 1].length; + const length = columns![columns!.length - 1].length; return (
@@ -48,20 +50,22 @@ class App extends React.Component { state = { selectedKeys: [], }; + dataSource = dataSource(); + getChildContext() { return { onChange: this.onChange, }; } - dataSource = dataSource(); - onChange = checked => { - let selectedKeys = []; + + onChange: NonNullable['onChange'] = checked => { + let selectedKeys: number[] = []; if (checked) { selectedKeys = this.dataSource.map(item => item.id); } this.onRowChange(selectedKeys); }; - onRowChange = selectedKeys => { + onRowChange = (selectedKeys: number[]) => { this.setState({ selectedKeys, }); diff --git a/components/table/__docs__/demo/basic-columns/index.tsx b/components/table/__docs__/demo/basic-columns/index.tsx index 6332c00ae1..0e3adec669 100644 --- a/components/table/__docs__/demo/basic-columns/index.tsx +++ b/components/table/__docs__/demo/basic-columns/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; +import type { ColumnProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -13,8 +14,8 @@ const dataSource = () => { } return result; }; -const render = (value, index, record) => { - return Remove({record.id}); +const render: ColumnProps['cell'] = (value, index, record) => { + return Remove({record!.id}); }; const columns = [ diff --git a/components/table/__docs__/demo/basic/index.tsx b/components/table/__docs__/demo/basic/index.tsx index 080c0c6552..de68de99d7 100644 --- a/components/table/__docs__/demo/basic/index.tsx +++ b/components/table/__docs__/demo/basic/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; +import type { ColumnProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -13,8 +14,8 @@ const dataSource = () => { } return result; }; -const render = (value, index, record) => { - return Remove({record.id}); +const render: ColumnProps['cell'] = (value, index, record) => { + return Remove({record!.id}); }; ReactDOM.render( diff --git a/components/table/__docs__/demo/clear-selection/index.tsx b/components/table/__docs__/demo/clear-selection/index.tsx index daf2e2ee55..58fca2f953 100644 --- a/components/table/__docs__/demo/clear-selection/index.tsx +++ b/components/table/__docs__/demo/clear-selection/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Button, Box } from '@alifd/next'; +import type { ColumnProps, RecordItem, TableProps } from '@alifd/next/types/table'; -const dataSource = (i, j) => { +const dataSource = (i: number, j: number) => { const result = []; for (let a = i; a < j; a++) { result.push({ @@ -13,33 +14,34 @@ const dataSource = (i, j) => { } return result; }, - render = (value, index, record) => { + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - rowSelection: { - onChange: this.onChange.bind(this), - onSelect: function (selected, record, records) { - console.log('onSelect', selected, record, records); - }, - onSelectAll: function (selected, records) { - console.log('onSelectAll', selected, records); - }, - selectedRowKeys: [100306660940, 100306660941], - getProps: record => { - return { - disabled: record.id === 100306660941, - }; - }, + state: { + rowSelection: NonNullable; + dataSource?: TableProps['dataSource']; + loading?: boolean; + } = { + rowSelection: { + onChange: this.onChange.bind(this), + onSelect: function (selected, record, records) { + console.log('onSelect', selected, record, records); }, - dataSource: dataSource(0, 5), - }; - } - onChange(ids, records) { + onSelectAll: function (selected, records) { + console.log('onSelectAll', selected, records); + }, + selectedRowKeys: [100306660940, 100306660941], + getProps: record => { + return { + disabled: record.id === 100306660941, + }; + }, + }, + dataSource: dataSource(0, 5), + }; + onChange(ids: number[], records: RecordItem[]) { const { rowSelection } = this.state; rowSelection.selectedRowKeys = ids; console.log('onChange', ids, records); @@ -58,7 +60,7 @@ class App extends React.Component { const mode = rowSelection.mode; const selectedRowKeys = rowSelection.selectedRowKeys; rowSelection.mode = mode === 'single' ? 'multiple' : 'single'; - rowSelection.selectedRowKeys = selectedRowKeys.length === 1 ? selectedRowKeys : []; + rowSelection.selectedRowKeys = selectedRowKeys!.length === 1 ? selectedRowKeys : []; this.setState({ rowSelection }); } modifyDataSource() { diff --git a/components/table/__docs__/demo/colspan-lock-columns/index.tsx b/components/table/__docs__/demo/colspan-lock-columns/index.tsx index cec05fc558..94372a5311 100644 --- a/components/table/__docs__/demo/colspan-lock-columns/index.tsx +++ b/components/table/__docs__/demo/colspan-lock-columns/index.tsx @@ -46,7 +46,6 @@ const dataSource = [ ReactDOM.render( { if (colIndex === 0) { diff --git a/components/table/__docs__/demo/colspan/index.tsx b/components/table/__docs__/demo/colspan/index.tsx index 1858cff2ab..40a496bd6a 100644 --- a/components/table/__docs__/demo/colspan/index.tsx +++ b/components/table/__docs__/demo/colspan/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; +import type { TableProps } from '@alifd/next/types/table'; -const onRowClick = function (record, index, e) { +const onRowClick: TableProps['onRowClick'] = function (record, index, e) { console.log(record, index, e); }, dataSource = () => { @@ -17,10 +18,7 @@ const onRowClick = function (record, index, e) { } return result; }, - render = (value, index, record) => { - return Remove({record.id}); - }, - cellProps = (rowIndex, colIndex) => { + cellProps: TableProps['cellProps'] = (rowIndex, colIndex) => { if (rowIndex === 2 && colIndex === 1) { return { // take 3 rows's space diff --git a/components/table/__docs__/demo/column/index.tsx b/components/table/__docs__/demo/column/index.tsx index 17a59503c6..7a54cd1aa3 100644 --- a/components/table/__docs__/demo/column/index.tsx +++ b/components/table/__docs__/demo/column/index.tsx @@ -30,13 +30,13 @@ const dataSource = () => { ]; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - cols: cols, - }; - } + state = { + dataSource: dataSource(), + cols: cols, + }; + + changedCols: { title: string; dataIndex: string }[]; + openDialog = () => { Dialog.alert({ content: this.renderControlContent(), @@ -63,7 +63,7 @@ class App extends React.Component { ); } - onChange = value => { + onChange = (value: string[]) => { this.changedCols = cols.filter(col => value.indexOf(col.dataIndex) > -1); }; diff --git a/components/table/__docs__/demo/crossline/index.tsx b/components/table/__docs__/demo/crossline/index.tsx index 122b20f098..7dc796dd95 100644 --- a/components/table/__docs__/demo/crossline/index.tsx +++ b/components/table/__docs__/demo/crossline/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; +import type { ColumnProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -13,7 +14,7 @@ const dataSource = () => { } return result; }; -const render = current => { +const render: ColumnProps['cell'] = current => { return remove {current}; }; diff --git a/components/table/__docs__/demo/crud/index.tsx b/components/table/__docs__/demo/crud/index.tsx index da3d3fa358..43d744ec7c 100644 --- a/components/table/__docs__/demo/crud/index.tsx +++ b/components/table/__docs__/demo/crud/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Button } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; -const onRowClick = function (record, index, e) { +const onRowClick: TableProps['onRowClick'] = function (record, index, e) { console.log(record, index, e); }, dataSource = () => { @@ -35,7 +36,7 @@ class App extends React.Component { }); }; - onRemove = id => { + onRemove = (id: number) => { const { dataSource } = this.state; let index = -1; dataSource.forEach((item, i) => { @@ -52,7 +53,7 @@ class App extends React.Component { }; render() { - const renderOper = (value, index, record) => { + const renderOper: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; return ( diff --git a/components/table/__docs__/demo/custom-loading/index.tsx b/components/table/__docs__/demo/custom-loading/index.tsx index 0eaf27535b..ecd624cd38 100644 --- a/components/table/__docs__/demo/custom-loading/index.tsx +++ b/components/table/__docs__/demo/custom-loading/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Loading, Icon } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -14,7 +15,7 @@ const dataSource = () => { return result; }; -const render = (value, index, record) => { +const render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; @@ -24,7 +25,9 @@ const indicator = ( ); -const CustomLoading = props => ; +const CustomLoading: TableProps['loadingComponent'] = props => ( + +); ReactDOM.render(
diff --git a/components/table/__docs__/demo/dragable/index.tsx b/components/table/__docs__/demo/dragable/index.tsx index 4e33a4b8b6..434979339a 100644 --- a/components/table/__docs__/demo/dragable/index.tsx +++ b/components/table/__docs__/demo/dragable/index.tsx @@ -5,6 +5,7 @@ import { Table } from '@alifd/next'; import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import classnames from 'classnames'; +import type { ColumnProps, SelectionRowProps, TableProps } from '@alifd/next/types/table'; const { SelectionRow } = Table; @@ -14,7 +15,16 @@ const MyDndProvider = DragDropContext(HTML5Backend)(({ children }) => { return children; }); -function MyRow(props) { +type MyRowProps = SelectionRowProps & { + index?: number; + moveRow?: (dragIndex: number, hoverIndex: number) => void; + isDragging?: boolean; + isOver?: boolean; + connectDragSource?: (node: React.ReactNode) => React.ReactNode; + connectDropTarget?: (node: React.ReactNode) => React.ReactNode; +}; + +function MyRow(props: MyRowProps) { const { isDragging, isOver, @@ -29,9 +39,9 @@ function MyRow(props) { const style = { ...others.style, cursor: 'move' }; const cls = classnames({ - [className]: className, - 'drop-over-upward': isOver && others.index < dragingIndex, - 'drop-over-downward': isOver && others.index > dragingIndex, + [className!]: className, + 'drop-over-upward': isOver && others.index! < dragingIndex, + 'drop-over-downward': isOver && others.index! > dragingIndex, }); return ( @@ -39,7 +49,7 @@ function MyRow(props) { {...others} style={{ ...style, ...{ opacity } }} className={cls} - wrapper={row => connectDragSource(connectDropTarget(row))} + wrapper={row => connectDragSource!(connectDropTarget!(row))} /> ); } @@ -47,7 +57,7 @@ function MyRow(props) { const NewRow = DropTarget( 'row', { - drop(props, monitor) { + drop(props: MyRowProps, monitor) { const dragIndex = monitor.getItem().index; const hoverIndex = props.index; @@ -55,7 +65,7 @@ const NewRow = DropTarget( return; } - props.moveRow(dragIndex, hoverIndex); + props.moveRow!(dragIndex, hoverIndex!); monitor.getItem().index = hoverIndex; }, }, @@ -67,10 +77,10 @@ const NewRow = DropTarget( DragSource( 'row', { - beginDrag: props => { - dragingIndex = props.index; + beginDrag: (props: MyRowProps) => { + dragingIndex = props.index!; return { - id: props.record[props.primaryKey], + id: props.record[props.primaryKey!], index: props.rowIndex, }; }, @@ -82,15 +92,21 @@ const NewRow = DropTarget( )(MyRow) ); -class InnerTable extends React.Component { - constructor(props) { +type InnerTableProps = TableProps; + +type InnerTableState = { + dataSource: NonNullable; +}; + +class InnerTable extends React.Component { + constructor(props: InnerTableProps) { super(props); this.state = { - dataSource: [...props.dataSource], + dataSource: [...props.dataSource!], }; } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: InnerTableProps) { if ( nextProps.dataSource && JSON.stringify(nextProps.dataSource) !== JSON.stringify(this.state.dataSource) @@ -99,8 +115,7 @@ class InnerTable extends React.Component { } } - moveRow = (dragIndex, hoverIndex) => { - const { onSort } = this.props; + moveRow = (dragIndex: number, hoverIndex: number) => { const dragRow = this.state.dataSource[dragIndex]; const dataSource = [...this.state.dataSource]; dataSource.splice(dragIndex, 1); @@ -108,16 +123,13 @@ class InnerTable extends React.Component { this.setState({ dataSource, }); - - onSort && onSort(this.state.dataSource); }; render() { - const { excludeProvider, ...restProps } = this.props; - const tableProps = { - ...restProps, + const tableProps: TableProps = { + ...this.props, dataSource: this.state.dataSource, - rowProps: (props, index) => ({ + rowProps: (record, index) => ({ index, moveRow: this.moveRow, }), @@ -125,6 +137,7 @@ class InnerTable extends React.Component { Row: NewRow, }, }; + ``; return
; } @@ -149,15 +162,11 @@ const result = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); - - this.state = { - dataSource: result, - }; - } + state = { + dataSource: result, + }; - onRemove = id => { + onRemove = (id: string) => { const { dataSource } = this.state; let index = -1; dataSource.forEach((item, i) => { @@ -173,7 +182,7 @@ class Demo extends React.Component { } }; - renderOper = (value, index, record) => { + renderOper: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; render() { diff --git a/components/table/__docs__/demo/editable/index.tsx b/components/table/__docs__/demo/editable/index.tsx index 7b9f1e98d7..9d6eb470b1 100644 --- a/components/table/__docs__/demo/editable/index.tsx +++ b/components/table/__docs__/demo/editable/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Input } from '@alifd/next'; +import type { InputProps } from '@alifd/next/types/input'; +import type { ColumnProps } from '@alifd/next/types/table'; const result = [ { @@ -19,9 +21,17 @@ const result = [ title: { name: 'The adventures of Robinson Crusoe' }, }, ]; - -class EditablePane extends React.Component { - constructor(props) { +type EditablePaneProps = { + defaultTitle: string; +}; +class EditablePane extends React.Component< + EditablePaneProps, + { + cellTitle: string; + editable: boolean; + } +> { + constructor(props: EditablePaneProps) { super(props); this.state = { cellTitle: props.defaultTitle, @@ -29,7 +39,7 @@ class EditablePane extends React.Component { }; } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: EditablePaneProps) { if (nextProps.defaultTitle !== this.state.cellTitle) { this.setState({ cellTitle: nextProps.defaultTitle, @@ -37,7 +47,7 @@ class EditablePane extends React.Component { } } - onKeyDown = e => { + onKeyDown: InputProps['onKeyDown'] = e => { const { keyCode } = e; // Stop bubble up the events of keyUp, keyDown, keyLeft, and keyRight if (keyCode > 36 && keyCode < 41) { @@ -45,7 +55,7 @@ class EditablePane extends React.Component { } }; - onBlur = e => { + onBlur: InputProps['onBlur'] = e => { this.setState({ editable: false, cellTitle: e.target.value, @@ -75,15 +85,11 @@ class EditablePane extends React.Component { } class Demo extends React.Component { - constructor(props) { - super(props); - - this.state = { - dataSource: result, - id: '', - }; - } - renderCell = (value, index, record) => { + state = { + dataSource: result, + id: '', + }; + renderCell: ColumnProps['cell'] = (value: string) => { return ; }; diff --git a/components/table/__docs__/demo/expanded-complex/index.tsx b/components/table/__docs__/demo/expanded-complex/index.tsx index d3a9feca8b..efdc21ad38 100644 --- a/components/table/__docs__/demo/expanded-complex/index.tsx +++ b/components/table/__docs__/demo/expanded-complex/index.tsx @@ -1,9 +1,19 @@ -import React from 'react'; +import React, { type CSSProperties } from 'react'; import ReactDOM from 'react-dom'; import { Table, Button } from '@alifd/next'; -/*eslint-disable react/prop-types, react/no-multi-comp*/ -class ExpandedApp extends React.Component { - constructor(props) { +import type { ColumnProps, RecordItem, TableProps } from '@alifd/next/types/table'; + +type ExpandedAppProps = { + dataSource: NonNullable; + index: number; +}; +class ExpandedApp extends React.Component< + ExpandedAppProps, + { + dataSource: NonNullable; + } +> { + constructor(props: ExpandedAppProps) { super(props); this.state = { dataSource: this.props.dataSource, @@ -15,7 +25,7 @@ class ExpandedApp extends React.Component { this.setState({ dataSource }); } render() { - const style = { + const style: CSSProperties = { borderTop: '1px solid #eee', textAlign: 'center', background: '#f8f8f8', @@ -56,24 +66,28 @@ const dataSource = () => { } return result; }, - render = (value, index, record) => { + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }, - expandedRowRender = (record, index) => { + expandedRowRender: TableProps['expandedRowRender'] = (record, index) => { const children = record.children; - return ; + return ; }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - hasBorder: false, - openRowKeys: [], - }; - } - onSort(dataIndex, order) { + state: { + dataSource: ReturnType; + hasBorder: boolean; + openRowKeys: number[]; + getExpandedColProps?: TableProps['getExpandedColProps']; + hasExpandedRowCtrl?: TableProps['hasExpandedRowCtrl']; + } = { + dataSource: dataSource(), + hasBorder: false, + openRowKeys: [], + getExpandedColProps: undefined, + }; + onSort: TableProps['onSort'] = (dataIndex: 'id', order) => { const dataSource = this.state.dataSource.sort(function (a, b) { const result = a[dataIndex] - b[dataIndex]; return order === 'asc' ? (result > 0 ? 1 : -1) : result > 0 ? -1 : 1; @@ -81,10 +95,10 @@ class App extends React.Component { this.setState({ dataSource, }); - } + }; disabledExpandedCol() { this.setState({ - getExpandedColProps: (record, index) => { + getExpandedColProps: (record: RecordItem, index: number) => { console.log(index); if (index === 3) { return { @@ -99,11 +113,11 @@ class App extends React.Component { hasExpandedRowCtrl: false, }); } - onRowOpen(openRowKeys) { + onRowOpen: TableProps['onRowOpen'] = openRowKeys => { this.setState({ openRowKeys }); - } - toggleExpand(record) { - const key = record.id, + }; + toggleExpand(record: RecordItem) { + const key = record.id as number, { openRowKeys } = this.state, index = openRowKeys.indexOf(key); if (index > -1) { @@ -115,12 +129,11 @@ class App extends React.Component { openRowKeys: openRowKeys, }); } - rowProps(record, index) { - console.log('rowProps', record, index); + rowProps: TableProps['rowProps'] = (record, index) => { return { className: `next-myclass-${index}` }; - } + }; render() { - const renderTitle = (value, index, record) => { + const renderTitle: ColumnProps['cell'] = (value, index, record) => { return (
{value} @@ -142,16 +155,15 @@ class App extends React.Component {
diff --git a/components/table/__docs__/demo/expanded-lock/index.tsx b/components/table/__docs__/demo/expanded-lock/index.tsx index 01fe3038ee..c360e45aa4 100644 --- a/components/table/__docs__/demo/expanded-lock/index.tsx +++ b/components/table/__docs__/demo/expanded-lock/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Button } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -14,7 +15,7 @@ const dataSource = () => { } return result; }, - expandedRowRender = (record, rowIndex) => { + expandedRowRender: TableProps['expandedRowRender'] = (record, rowIndex) => { if (rowIndex === 0) { return record.title; } @@ -30,18 +31,15 @@ const dataSource = () => { ); }, - render = (value, index, record) => { + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - loading: false, - }; - } + state = { + dataSource: dataSource(), + loading: false, + }; toggleLoading = () => { this.setState({ @@ -63,7 +61,7 @@ class App extends React.Component { // expandedRowIndent 仅在IE下才会生效,非IE模式下为[0,0]且不可修改 expandedRowIndent={[2, 0]} expandedRowRender={expandedRowRender} - rowExpandable={record => record.expandable} + rowExpandable={record => record.expandable as boolean} onRowClick={() => console.log('rowClick')} > diff --git a/components/table/__docs__/demo/expanded/index.tsx b/components/table/__docs__/demo/expanded/index.tsx index 4bb08fa7f8..09b7a1fe3d 100644 --- a/components/table/__docs__/demo/expanded/index.tsx +++ b/components/table/__docs__/demo/expanded/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Button } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -13,18 +14,16 @@ const dataSource = () => { } return result; }, - render = (value, index, record) => { + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - }; - } - onSort(dataIndex, order) { + state = { + dataSource: dataSource(), + expandedRowIndent: undefined, + }; + onSort: TableProps['onSort'] = (dataIndex: 'id', order) => { const dataSource = this.state.dataSource.sort(function (a, b) { const result = a[dataIndex] - b[dataIndex]; return order === 'asc' ? (result > 0 ? 1 : -1) : result > 0 ? -1 : 1; @@ -32,7 +31,7 @@ class App extends React.Component { this.setState({ dataSource, }); - } + }; toggleIndent() { this.setState({ expandedRowIndent: [2, 1], @@ -51,9 +50,8 @@ class App extends React.Component {

record.title} onRowClick={() => console.log('rowClick')} expandedRowIndent={this.state.expandedRowIndent} diff --git a/components/table/__docs__/demo/filter&sort/index.tsx b/components/table/__docs__/demo/filter&sort/index.tsx index 476bdc9b12..c0936d9d27 100644 --- a/components/table/__docs__/demo/filter&sort/index.tsx +++ b/components/table/__docs__/demo/filter&sort/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Button, Icon } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -13,22 +14,19 @@ const dataSource = () => { } return result; }, - render = (value, index, record) => { + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - filterMode: 'multiple', - sort: { - id: 'desc', - }, - }; - } - onSort(dataIndex, order) { + state = { + dataSource: dataSource(), + filterMode: 'multiple' as const, + sort: { + id: 'desc' as const, + }, + }; + onSort: TableProps['onSort'] = (dataIndex: 'id', order) => { console.log(dataIndex, order, '======'); const dataSource = this.state.dataSource.sort(function (a, b) { const result = a[dataIndex] - b[dataIndex]; @@ -38,10 +36,11 @@ class App extends React.Component { dataSource, sort: { id: order }, }); - } - onFilter(filterParams) { + }; + onFilter: TableProps['onFilter'] = filterParams => { let ds = dataSource(); - Object.keys(filterParams).forEach(key => { + console.log(filterParams); + Object.keys(filterParams).forEach((key: 'title') => { const selectedKeys = filterParams[key].selectedKeys; if (selectedKeys.length) { ds = ds.filter(record => { @@ -52,12 +51,12 @@ class App extends React.Component { } }); this.setState({ dataSource: ds }); - } - changeMode() { + }; + changeMode = () => { this.setState({ filterMode: 'single', }); - } + }; render() { const filters = [ { @@ -106,15 +105,13 @@ class App extends React.Component { return (

- +

{ +const dataSource = (length: number) => { const result = []; for (let i = 0; i < length; i++) { result.push({ @@ -15,10 +16,10 @@ const dataSource = length => { return result; }; -const render = (value, index, record) => { +const render: ColumnProps['cell'] = (value, index, record) => { return ( node.parentNode} + popupContainer={node => node.parentNode as HTMLElement} popupProps={{ autoFit: true }} defaultValue="jack" aria-label="name is" @@ -66,12 +67,7 @@ class App extends React.Component { fixedHeader stickyHeader > - + diff --git a/components/table/__docs__/demo/stickylock-complex/index.tsx b/components/table/__docs__/demo/stickylock-complex/index.tsx index 097bc81bc2..c9986c531a 100644 --- a/components/table/__docs__/demo/stickylock-complex/index.tsx +++ b/components/table/__docs__/demo/stickylock-complex/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Table, Button } from '@alifd/next'; +import { Table } from '@alifd/next'; +import type { ColumnProps, RecordItem, TableProps } from '@alifd/next/types/table'; -const dataSource = j => { +const dataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -15,46 +16,40 @@ const dataSource = j => { } return result; }; -const render = (value, index, record) => { +const render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - rowSelection: { - onChange: this.onChange.bind(this), - onSelect: function (selected, record, records) { - console.log('onSelect', selected, record, records); - }, - onSelectAll: function (selected, records) { - console.log('onSelectAll', selected, records); - }, - selectedRowKeys: [100306660940, 100306660941], - getProps: record => { - return { - disabled: record.id === 100306660941, - }; - }, + state: { + rowSelection: NonNullable; + openRowKeys: string[]; + scrollToRow: number; + } = { + rowSelection: { + onChange: this.onChange.bind(this), + onSelect: function (selected, record, records) { + console.log('onSelect', selected, record, records); }, - openRowKeys: [], - }; - this.onRowMouseEnter = (_, i) => { - // this.setState({ - // openRowKeys: [i] - // }) - }; - } - state = { + onSelectAll: function (selected, records) { + console.log('onSelectAll', selected, records); + }, + selectedRowKeys: [100306660940, 100306660941], + getProps: record => { + return { + disabled: record.id === 100306660941, + }; + }, + }, + openRowKeys: [], scrollToRow: 20, }; - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); }; - onChange(ids, records) { + onChange(ids: (string | number)[], records: RecordItem[]) { const { rowSelection } = this.state; rowSelection.selectedRowKeys = ids; console.log('onChange', ids, records); @@ -66,7 +61,6 @@ class App extends React.Component { dataSource={dataSource(200)} maxBodyHeight={400} useVirtual - // scrollToRow={this.state.scrollToRow} onBodyScroll={this.onBodyScroll} expandedRowRender={() => (
@@ -76,7 +70,6 @@ class App extends React.Component { hasExpandedRowCtrl={false} expandedRowIndent={[0, 0]} rowSelection={this.state.rowSelection} - onRowMouseEnter={this.onRowMouseEnter} openRowKeys={this.state.openRowKeys} primaryKey="index" > diff --git a/components/table/__docs__/demo/style/index.tsx b/components/table/__docs__/demo/style/index.tsx index 2fed99dd05..7c6da275ae 100644 --- a/components/table/__docs__/demo/style/index.tsx +++ b/components/table/__docs__/demo/style/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Table, Button, Box } from '@alifd/next'; +import { Table, Button } from '@alifd/next'; +import type { ColumnProps } from '@alifd/next/types/table'; const dataSource = () => { const result = []; @@ -13,19 +14,23 @@ const dataSource = () => { } return result; }, - render = (value, index, record) => { + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; - -class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - className: '', - align: 'left', - }; - } +type AppState = { + dataSource: ReturnType; + className: string; + align: 'left' | 'right'; + wordBreak?: 'word' | 'all'; + isZebra?: boolean; + hasBorder?: boolean; +}; +class App extends React.Component { + state: AppState = { + dataSource: dataSource(), + className: '', + align: 'left' as const, + }; toggleZebra() { this.setState({ isZebra: !this.state.isZebra, diff --git a/components/table/__docs__/demo/tree-onload/index.tsx b/components/table/__docs__/demo/tree-onload/index.tsx index 357068e4d2..548ce00b9b 100644 --- a/components/table/__docs__/demo/tree-onload/index.tsx +++ b/components/table/__docs__/demo/tree-onload/index.tsx @@ -10,7 +10,7 @@ const data = [ name: 'Jack', age: 32, address: 'Zhejiang Province, China', - // 非叶子结点需要带着children,并且设置一条空数据先 + // 非叶子结点需要带着 children,并且设置一条空数据先 children: [{ key: generateRandomKey() }], }, { @@ -56,7 +56,7 @@ class App extends React.Component { name: `${currentRecord.name}-son`, age: 10, address: 'Earth', - // 非叶子结点需要带着children,并且设置一条空数据先 + // 非叶子结点需要带着 children,并且设置一条空数据先 children: [{ key: generateRandomKey() }], }, { diff --git a/components/table/__docs__/demo/virtual-rowspan/index.tsx b/components/table/__docs__/demo/virtual-rowspan/index.tsx index b84bc917cc..6b1f56545f 100644 --- a/components/table/__docs__/demo/virtual-rowspan/index.tsx +++ b/components/table/__docs__/demo/virtual-rowspan/index.tsx @@ -1,10 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table, Button } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; -const noop = () => {}; - -const dataSource = j => { +const dataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -17,7 +16,7 @@ const dataSource = j => { return result; }; -const render = (value, index, record) => { +const render: ColumnProps['cell'] = (value, index, record) => { return remove({record.id}); }; @@ -29,20 +28,19 @@ class App extends React.Component { }; componentDidMount = () => { - console.log('componentDidMount'); setTimeout(() => { this.setState({ dataSource: dataSource(200), }); }, 1000); }; - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); }; - onBodyScroll2 = start => { + onBodyScroll2: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow2: start, }); @@ -65,7 +63,7 @@ class App extends React.Component { render() { return (
-
{`keepForwardRenderRows < rowSpan的值时,滚动到第12行后行合并会失效`}
+
{`keepForwardRenderRows < rowSpan 的值时,滚动到第 12 行后行合并会失效`}
{ + cellProps={(rowIndex: number, colIndex) => { if ([0, 17, 34].includes(rowIndex) && colIndex === 0) { return { rowSpan: 17, @@ -89,7 +87,7 @@ class App extends React.Component {

-
{`keepForwardRenderRows >= rowSpan的值时,滚动到第12行后行合并不会失效`}
+
{`keepForwardRenderRows >= rowSpan 的值时,滚动到第 12 行后行合并不会失效`}
{ + cellProps={(rowIndex: number, colIndex) => { if ([0, 17, 34].includes(rowIndex) && colIndex === 0) { return { rowSpan: 17, diff --git a/components/table/__docs__/demo/virtual/index.tsx b/components/table/__docs__/demo/virtual/index.tsx index 9667194041..2551137745 100644 --- a/components/table/__docs__/demo/virtual/index.tsx +++ b/components/table/__docs__/demo/virtual/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Table } from '@alifd/next'; +import type { ColumnProps, TableProps } from '@alifd/next/types/table'; -const dataSource = j => { +const dataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -14,7 +15,7 @@ const dataSource = j => { } return result; }; -const render = (value, index, record) => { +const render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; @@ -22,7 +23,7 @@ class App extends React.Component { state = { scrollToRow: 20, }; - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); diff --git a/components/table/__docs__/index.en-us.md b/components/table/__docs__/index.en-us.md index 0fcf9bee64..411b2bef90 100644 --- a/components/table/__docs__/index.en-us.md +++ b/components/table/__docs__/index.en-us.md @@ -21,12 +21,14 @@ The following code will create a two-column data table: ```js import { Table } from '@alifd/next'; -const dataSource = [{id: 1, time: '2016'}]; +const dataSource = [{ id: 1, time: '2016' }]; ReactDOM.render(
- - -
, mountNode); + + + , + mountNode +); ``` #### Column Configuration @@ -38,7 +40,7 @@ The following code will make cells render different views based on values: ```js import { Table } from '@alifd/next'; -const dataSource = [{id: 1, time: '2016'}]; +const dataSource = [{ id: 1, time: '2016' }]; const renderTime = value => { if (value === '2016') { return 'this year'; @@ -47,9 +49,11 @@ const renderTime = value => { }; ReactDOM.render( - - -
, mountNode); + + + , + mountNode +); ``` #### Multiple Table Header @@ -59,17 +63,19 @@ Use Table.ColumnGroup to wrap Table.Column to create a table with multiple heade ```js import { Table } from '@alifd/next'; -const dataSource = [{id: 1, time: '2016'}]; +const dataSource = [{ id: 1, time: '2016' }]; ReactDOM.render( - - + + - + -
, mountNode); + , + mountNode +); ``` ### Know Issues @@ -82,87 +88,164 @@ ReactDOM.render( ### Table -| Param | Descripiton | Type | Default Value | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | --------- | -| tableLayout | table-layout attribute of table element

**options**:
'fixed', 'auto' | Enum | - | -| tableWidth | totoal width of table | Number | - | -| dataSource | data source shown in the table | Array | \[] | -| onRowClick | callback triggered when click each row

**signatures**:
Function(record: Object, index: Number, e: Event) => void
**params**:
_record_: {Object} the data corresponding to the row
_index_: {Number} the index corresponding to the row
_e_: {Event} event object | Function | () => { } | -| onRowMouseEnter | callback triggered when mouse enter each row

**signatures**:
Function(record: Object, index: Number, e: Event) => void
**params**:
_record_: {Object} the data corresponding to the row
_index_: {Number} the index corresponding to the row
_e_: {Event} event object | Function | () => { } | -| onRowMouseLeave | callback triggered when mouse leave each row

**signatures**:
Function(record: Object, index: Number, e: Event) => void
**params**:
_record_: {Object} the data corresponding to the row
_index_: {Number} the index corresponding to the row
_e_: {Event} event object | Function | () => { } | -| onSort | callback triggered when sort

**signatures**:
Function(dataIndex: String, order: String) => void
**params**:
_dataIndex_: {String} the specified sorted field
_order_: {String} order, whether is 'desc' or 'asc' | Function | () => { } | -| onFilter | callback triggered when click filter confirmation button

**signatures**:
Function(filterParams: Object) => void
**params**:
_filterParams_: {Object} filter params | Function | () => { } | -| onResizeChange | callback triggered when resize column

**signatures**:
Function(dataIndex: String, value: Number) => void
**params**:
_dataIndex_: {String} the specify resize field
_value_: {Number} column width variation | Function | () => { } | -| rowProps | get the properties of row, the return value will not work if it is in conflict with other properties

**signatures**:
Function(record: Object, index: Number) => Object
**params**:
_record_: {Object} the data corresponding to the row
_index_: {Number} the index corresponding to the row
**returns**:
{Object} properties of Row
| Function | () => { } | -| getCellProps | get the properties of cell, through which you can merge cells

**signatures**:
Function(rowIndex: Number, colIndex: Number, dataIndex: String, record: Object) => Object
**params**:
_rowIndex_: {Number} the index corresponding to the row
_colIndex_: {Number} the index corresponding to the column
_dataIndex_: {String} the field corresponding to the column
_record_: {Object} the record corresponding to the row
**returns**:
{Object} properties of td
| Function | () => { } | -| hasBorder | whether the table has a border | Boolean | true | -| hasHeader | whether the table has header | Boolean | true | -| isZebra | whether the table has zebra style | Boolean | false | -| loading | whether data is loading | Boolean | false | -| loadingComponent | customized Loading component

**signatures**:
(props: LoadingProps) => React.ReactNode | Function | - | -| filterParams | currently filtered keys, use this property to control which menu in the table's header filtering options is selected, in the format {dataIndex: {selectedKeys:\[]}}
Example:
Suppose you want to control dataIndex as Select
for the menu item whose key is one in the filtering menu of the id column. | Object | - | -| sort | the currently sorted field, use this property to control the sorting of the table's fields in the format {dataIndex: 'asc'} | Object | - | -| sortIcons | customize sortIcons, to set top and bottom layout e.g.`{desc: , asc: }` | Object | - | -| columns | equals to Table.Column, but Table.Column has a higher priority | Array | - | -| emptyContent | content when the table is empty | ReactNode | - | -| primaryKey | the primary key of data in the dataSource, if the attribute in the given data source does not contain the primary key, it will cause selecting all | String | 'id' | -| expandedRowRender | rendering function for expanded row

**signatures**:
Function(record: Object, index: Number) => Element
**params**:
_record_: {Object} the record corresponding to the row
_index_: {Number} the index corresponding to the row
**returns**:
{Element} render content
| Function | - | -| rowExpandable | row can expand or not

**signatures**:
Function(record: Object, index: Number) => Boolean
**params**:
_record_: {Object} the record corresponding to the row
_index_: {Number} the index corresponding to the row
**returns**:
{Boolean} expandable
| Function | - | -| expandedRowIndent | indent of expanded row | Array | - | -| openRowKeys | (under control)keys of expanded row, usually used with onRowOpen | Array | - | -| onRowOpen | callback triggered when expand row

**signatures**:
Function(openRowKeys: Array, currentRowKey: String, expanded: Boolean, currentRecord: Object) => void
**params**:
_openRowKeys_: {Array} key of expanded row
_currentRowKey_: {String} key of current clicked row
_expanded_: {Boolean} whether is expanded
_currentRecord_: {Object} the data corresponding to the current clicked row | Function | - | -| hasExpandedRowCtrl | whether to show the + button that clicks to expand additional row | Boolean | - | -| getExpandedColProps | get properties of expanded row

**signatures**:
`(record: any, index: number) => object | Record` | Function | - | -| fixedHeader | whether the table header is fixed, this property is used with maxBodyHeight. When the height of the content area exceeds maxBodyHeight, a scroll bar appears in the content area. | Boolean | - | -| maxBodyHeight | the height of the largest content area, when `fixedHeader` is `true`, scroll bars will appear above this height | Number/String | - | -| rowSelection | whether to enable selection mode

**properties**:
_getProps_: {Function} `Function(record, index)=>Object` get default attributes of selection
_onChange_: {Function} `Function(selectedRowKeys:Array, records:Array)` callback triggered when selection change, **Note:** where records only contains the data of the current dataSource, it is likely to be less than the length of the selectedRowKeys.
_onSelect_: {Function} `Function(selected:Boolean, record:Object, records:Array)` callback triggered when user select
_onSelectAll_: {Function} `Function(selected:Boolean, records:Array)` callback triggered when user select all
_selectedRowKeys_: {Array} if this property is set, the rowSelection is controlled, and the received value is the value of the primaryKey of the row's data.
_mode_: {String} the mode of selection, the optional value is `single`, `multiple`, the default is `multiple`
_columnProps_: {Function} `Function()=>Object` props of checkbox columns which inherit from `Table.Column`
_titleProps_: {Function} `Function()=>Object` props of checkbox columns header, works only in `multiple` mode
_titleAddons_: {Function} `Function()=>Node` element added to checkbox columns header, works in both `single` and `multiple` mode | Object | - | -| stickyHeader | whether the table header is sticky | Boolean | - | -| offsetTop | affix triggered after the distance top reaches the specified offset | Number | - | -| affixProps | properties of Affix | Object | - | -| indent | indented in tree mode, work only when isTree is true | Number | - | -| isTree | enable the tree mode of Table, the received data format contains children and renders it into a tree table. | Boolean | - | -| useVirtual | whether use virtual scroll | Boolean | - | -| onBodyScroll | callback triggered when body scroll

**signatures**:
Function(start: Number) => void
**properties**:
start:{Number} The current number of lines scrolled to | Function | - | -| crossline | show corssline when hover,which is suitable for the scene where the header is complex and needs to be classified | Boolean | false | +| Param | Description | Type | Default Value | Required | Supported Version | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | -------- | ----------------- | +| size | Size of table, small is compact mode | 'small' \| 'medium' | - | | - | +| className | Custom class name | string | - | | - | +| style | Custom inline style | React.CSSProperties | - | | - | +| columns | Equivalent to writing child components Table.Column, child components have higher priority | Array\ | - | | - | +| tableLayout | The table | 'fixed' \| 'auto' | - | | - | +| tableWidth | The total length of the table, you can use it like this: set the total length of the table, set the width of some columns, and then the table will automatically allocate the width of the remaining space | number | - | | - | +| dataSource | The data source of the table | Array\ | - | | - | +| hasBorder | Whether the table has borders | boolean | true | | - | +| hasHeader | Whether the table has a header | boolean | true | | - | +| isZebra | Whether the table is zebra | boolean | false | | - | +| loading | Whether the table is loading | boolean | false | | - | +| emptyContent | The content to be displayed when the data is empty | React.ReactNode | - | | - | +| primaryKey | The primary key in the data source, if the property in the given data source does not contain the primary key, it will cause all rows to be selected, etc. | string | 'id' | | - | +| onRowClick | The event on clicking on a table row | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| onRowMouseEnter | The event on hovering on a table row | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| onRowMouseLeave | The event on leaving a table row | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| onSort | The event on clicking on a column sort | (dataIndex: string, order: SortOrder, sort: { [key: string]: SortOrder }) => void | - | | - | +| onFilter | The event on clicking on the filter confirm button | (filterParams: {
[propName: string]: { selectedKeys: string[]; visible: boolean };
}) => void | - | | - | +| onResizeChange | The event on resizing the column | (dataIndex: string, value: number) => void | - | | - | +| rowProps | Set the props of each row, if the return value conflicts with other attributes for row operations, it will be invalid. | (
record: RecordItem,
index: number
) => (Record\ & Partial\) \| undefined \| void | - | | - | +| cellProps | Set the props of each cell, you can use this property to merge cells | (
rowIndex: number \| string,
colIndex: number \| string,
dataIndex: string,
record: RecordItem
) =>
\| Partial\<
Omit\<
CellProps,
\| 'prefix'
\| 'pure'
\| 'primaryKey'
\| 'record'
\| 'value'
\| 'colIndex'
\| 'rowIndex'
\| 'align'
\| 'locale'
\| 'rtl'
\| 'width'
>
>
\| undefined
\| void | - | | - | +| loadingComponent | Custom loading component | React.ElementType\<{ [prop: string]: unknown; className?: string }> | - | | - | +| filterParams | The current filtered keys, using this property can control which menu in the filter menu of the table's header is selected, the format is \{dataIndex: \{selectedKeys: []\}\} | { [propName: string]: { selectedKeys?: string[]; visible?: boolean } } | - | | - | +| sort | The current sorted field, using this property can control the sorting of the table's fields, the format is \{dataIndex: 'asc'\} | { [key: string]: SortOrder } | - | | - | +| sortIcons | Custom sort button, for example: `{desc: , asc: React.ReactNode | - | | - | +| rowExpandable | Set whether the row is expandable, set false to disable expansion | (record: RecordItem, index: number) => boolean | - | | - | +| expandedRowIndent | The indent of extra rows, contains two numbers, the first number is the left indent, the second number is the right indent | [number, number] | stickyLock ? [0, 0] : [1, 0] | | - | +| openRowKeys | Expanded row, after passing, the expanded state is only controlled by this property. | Array\ | - | | - | +| defaultOpenRowKeys | The default expanded row | Array\ | - | | 1.23.22 | +| hasExpandedRowCtrl | Whether to show the + button to expand the extra row | boolean | true | | - | +| getExpandedColProps | Set the props of extra rows | (
record: RecordItem,
index: number
) => React.DetailedHTMLProps\, HTMLSpanElement> & {
disabled?: boolean;
} | - | | - | +| onRowOpen | The event on expanding or collapsing the extra row or tree | (
openRowKeys: Array\,
currentRowKey: string \| number,
expanded: boolean,
currentRecord: RecordItem
) => void | - | | - | +| onExpandedRowClick | The event on clicking on the extra row | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| fixedHeader | Whether the header is fixed, this property is combined with maxBodyHeight, when the content area's height exceeds maxBodyHeight, a scroll bar will appear in the content area | boolean | false | | - | +| maxBodyHeight | The maximum height of the content area, when `fixedHeader` is `true`, a scroll bar will appear when the height of the content area exceeds this height | number \| string | 200 | | - | +| rowSelection | Whether to enable selection mode | RowSelection \| null | - | | - | +| stickyHeader | Whether the header is sticky | boolean | - | | - | +| offsetTop | The header triggers sticky after the distance from the top of the window reaches this property, only valid when stickyHeader is true | number | - | | - | +| affixProps | The props of affix component, stickyHeader is based on Affix component. | AffixProps | - | | - | +| indent | The indent size of tree mode, only valid when isTree is true | number | 12 | | - | +| isTree | Enable tree mode of table, the data format received contains children and is rendered as tree table | boolean | - | | - | +| useVirtual | Enable virtual scroll | boolean | - | | - | +| scrollToRow | Scroll to specified row | number | - | | 1.22.15 | +| scrollToCol | Scroll to specified column | number | - | | - | +| rowHeight | Set the row height | number \| (() => number) | - | | - | +| onBodyScroll | The function triggered when the content area is scrolled | (start: number) => void | - | | - | +| expandedIndexSimulate | When enabled, the second parameter of getExpandedColProps() / getRowProps() / expandedRowRender() will return 0,1,2,3,4... in order, otherwise return the real index (0,2,4,6... / 1,3,5,7...) | boolean | false | | - | +| crossline | When hover, a cross reference axis appears, suitable for complex table headers that need to do header classification. | boolean | false | | - | +| keepForwardRenderRows | The number of rows to be preserved when virtual scrolling | number | 10 | | - | +| components | Custom components, advanced usage, used to replace components inside the table | {
Cell?: CellLike;
Filter?: FilterLike;
Sort?: SortLike;
Resize?: ResizeLike;
Row?: RowLike;
Header?: HeaderLike;
Wrapper?: WrapperLike;
Body?: BodyLike;
} | - | | - | ### Table.Column -| Param | Description | Type | Default Value | -| ---------- | ----------------------------------------------------- | ------------------------------- | ---------------- | -| dataIndex | specify the column corresponding field, support the fast value of `a.b` format | String | - | -| cell | cell rendering logic
Function(value, index, record) => Element | ReactElement/ReactNode/Function | (value) => value | -| title | content of table header | ReactElement/ReactNode/Function | - | -| htmlTitle | the props of title, which will be writen on header cells' dom | String | - | -| sortable | whether to support sorting | Boolean | - | -| width | width of column,width needs to be set in lock style | Number/String | - | -| align | alignment of cell

**options**:
'left', 'center', 'right' | Enum | - | -| alignHeader | alignment of header cell, value of align by default

**options**:
'left', 'center', 'right' | Enum | - | -| filters | generates a title filter menu in the format `[{label:'xxx', value:'xxx'}]` | Array<Object> | - | -| filterMode | whether the filtering mode is single or multiple selection

**options**:
'single', 'multiple' | Enum | 'multiple' | -| filterMenuProps | the props passed to Menu in filter mode, extend `Menu`'s API

**options**:
_subMenuSelectable_: {Boolean} default `false` subMenu can be selected or not
_isSelectIconRight_: {Boolean} default `false` select icon in right or not. (icon on SubMenu always in left) | Object | { subMenuSelectable: false } | -| lock | whether the lock column is supported, the options are `left`, `right`, `true` | Boolean/String | - | -| resizable | whether to support column resizing, when this value is set to true, the layout of the table will be modified to fixed | Boolean | false | -| asyncResizable | whether to support async column resizing, when this value is set to true, the layout of the table will be modified to fixed | Boolean | false | -| colSpan | theader can merge cell by this API, 0 means no th of this column | Number | - | +| Param | Description | Type | Default Value | Required | Supported Version | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------ | -------- | ----------------- | +| dataIndex | Specifies the field corresponding to the column, supporting `a.b` quick retrieval | string | - | | - | +| cell | Row rendering logic | \| React.ReactNode
\| ((value: unknown, rowIndex: number, record: RecordItem) => React.ReactNode) | - | | - | +| title | The content displayed in the header | React.ReactNode \| (() => React.ReactNode) | - | | - | +| htmlTitle | The title attribute written to the header cell | string | - | | - | +| sortable | Whether to support sorting | boolean | - | | - | +| width | Column width, note that the width must be configured in the lock column | number \| string | - | | - | +| align | The alignment of the cell | 'left' \| 'center' \| 'right' | - | | - | +| sortDirections | The direction of sorting. Set ['desc', 'asc'] to indicate descending and ascending. Set ['desc', 'asc', 'default'] to indicate descending, ascending, and not sorting | Array\ | - | | 1.23 | +| alignHeader | The alignment of the title cell, if not configured, the default value is read from align | 'left' \| 'center' \| 'right' | - | | - | +| filters | Generate the menu for title filtering, the format is `[{label: 'xxx', value: 'xxx'}]` | FilterItem[] | - | | - | +| filterMode | The mode of filtering is single or multiple | 'single' \| 'multiple' | 'multiple' | | - | +| lock | Whether to support locking, the value can be `left`, `right`, `true` | boolean \| 'left' \| 'right' | - | | - | +| resizable | Whether to support column width adjustment, when this value is set to true, the layout of table will be modified to fixed. | boolean | false | | - | +| asyncResizable | (Recommended) Whether to support asynchronous column width adjustment, when this value is set to true, the layout of table will be modified to fixed. | boolean | false | | 1.24 | +| colSpan | The number of cells that the header cell spans, set to 0 to not appear this th | number | - | | - | +| wordBreak | Set the word | 'all' \| 'word' | - | | 1.23 | +| filterMenuProps | The properties passed to the Menu menu in the filter mode, the default inherits the API of the Menu component | MenuProps & { subMenuSelectable?: boolean } | \{ subMenuSelectable: false \} | | - | +| filterProps | The properties passed to the Filter Dropdown component | DropdownProps | - | | - | ### Table.ColumnGroup -| Param | Description | Type | Default Value | -| ----- | ------- | ------------------------------- | -------------- | -| title | content of table header | ReactElement/ReactNode/Function | 'column-group' | +| Param | Description | Type | Default Value | Required | +| ----- | ----------------------------------- | ------------------------------------------ | ------------- | -------- | +| title | The content displayed in the header | React.ReactNode \| (() => React.ReactNode) | - | | ### Table.GroupHeader -| Param | Description | Type | Default Value | -| -------------------- | --------------------------- | ------------------------------- | -------- | -| cell | cell rendering logic | ReactElement/ReactNode/Function | () => '' | -| hasChildrenSelection | whether to render selection on Children | Boolean | false | -| hasSelection | whether to render selection on GroupHeader | Boolean | true | -| useFirstLevelDataWhenNoChildren | use the first level data when there is no children in dataSouce | Boolean | false | +| Param | Description | Type | Default Value | Required | +| ------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------- | -------- | +| cell | Row rendering logic | React.ReactNode \| ((value: RecordItem, index: number) => React.ReactNode) | - | | +| hasChildrenSelection | Whether to render selection on Children | boolean | false | | +| hasSelection | Whether to render selection on GroupHeader | boolean | true | | +| useFirstLevelDataWhenNoChildren | When there is no children in dataSource, whether to use content as data | boolean | false | | ### Table.GroupFooter -| Param | Description | Type | Default Value | -| ---- | ------ | ------------------------------- | -------- | -| cell | cell rendering logic | ReactElement/ReactNode/Function | () => '' | +| Param | Description | Type | Default Value | Required | +| ----- | ------------------- | ---------------------------------------------------------------------------------- | ------------- | -------- | +| cell | Row rendering logic | \| React.ReactNode
\| ((value: RecordItem, index: number) => React.ReactNode) | - | | + +### CellProps + +| Param | Description | Type | Default Value | Required | +| --------------------- | ----------- | ----------------------------------- | ------------- | -------- | +| pure | - | boolean | - | | +| prefix | - | TableProps['prefix'] | - | yes | +| className | - | string | - | | +| value | - | unknown | - | | +| record | - | RecordItem | - | | +| context | - | unknown | - | | +| colIndex | - | number | - | | +| rowIndex | - | number | - | | +| \_\_colIndex | - | number \| string | - | | +| style | - | React.CSSProperties | - | | +| component | - | React.ElementType | - | | +| children | - | React.ReactNode | - | | +| innerStyle | - | React.CSSProperties | - | | +| \_\_normalized | - | boolean | - | | +| expandedIndexSimulate | - | TableProps['expandedIndexSimulate'] | - | | +| getCellDomRef | - | React.LegacyRef\ | - | | +| primaryKey | - | TableProps['primaryKey'] | - | | +| rowSpan | - | number | - | | + +### RowProps + +| Param | Description | Type | Default Value | Required | +| ------------ | ----------- | ------------------------------------------------------------------- | ------------- | -------- | +| className | - | string | - | | +| rowIndex | - | number | - | yes | +| \_\_rowIndex | - | number | - | yes | +| onClick | - | (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void | - | yes | +| onMouseEnter | - | (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void | - | yes | +| onMouseLeave | - | (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void | - | yes | +| Cell | - | CellLike | - | yes | +| children | - | React.ReactNode | - | yes | +| record | - | RecordItem | - | yes | +| wrapper | - | (wrapper: React.ReactElement) => React.ReactNode | - | yes | + +### RowSelection + +| Param | Description | Type | Default Value | Required | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------- | -------- | +| getProps | Get the default properties of the selection | (record: RecordItem, index: number) => CheckboxProps \| RadioProps | - | | +| onChange | The event triggered when the selection changes. **Note:** records will only contain the data in the dataSource, and it may be less than the length of selectedRowKeys. | (selectedRowKeys: Array\, records: Array\) => void | - | | +| onSelect | The callback when the selection changes | (selected: boolean, record: RecordItem, records: RecordItem[]) => void | - | | +| onSelectAll | The callback when the select all button is clicked | (selected: boolean, records: RecordItem[]) => void | - | | +| selectedRowKeys | Set the rowSelection to be controlled, the value is the primaryKey value of the row | Array\ | - | | +| mode | The mode of the selection | 'single' \| 'multiple' | 'multiple' | | +| titleProps | The props of the selection header, only effective in `multiple` mode | () => CheckboxProps | - | | +| columnProps | The props of the selection column, such as lock, alignment, etc., you can use all parameters of Table.Column | () => Partial\ | - | | +| titleAddons | The elements added to the selection header, effective in `single` and `multiple` | () => React.ReactNode | - | | + +### RecordItem + +```typescript +export type RecordItem = Record & { children?: RecordItem[] }; +``` + +### SortOrder +```typescript +export type SortOrder = 'desc' | 'asc' | 'default'; +``` diff --git a/components/table/__docs__/index.md b/components/table/__docs__/index.md index b0091a9756..390c4c8bd4 100644 --- a/components/table/__docs__/index.md +++ b/components/table/__docs__/index.md @@ -17,11 +17,11 @@ ### Table.StickyLock -这是 `1.21` 版本推出的子组件,它与 `Table` 用法、API完全一样,只是优化了锁列的实现,推荐升级。 +这是 `1.21` 版本推出的子组件,它与 `Table` 用法、API 完全一样,只是优化了锁列的实现,推荐升级。 区分如下: -- 旧版本锁列通过两层dom来模拟左、右锁列的列,因此滚动、行高的同步等都需要额外逻辑的,逻辑较重; +- 旧版本锁列通过两层 dom 来模拟左、右锁列的列,因此滚动、行高的同步等都需要额外逻辑的,逻辑较重; - 新版本 `Table.StickyLock` 通过 `position: sticky` 来实现锁列,滚动、行高等行为都通过浏览器实现,逻辑轻量; 建议用户在新的页面中使用 `Table.StickyLock`,如果没有深度的样式定制(例如选择到 `.next-table-lock-left` 这一层级),也可以把现有的 `Table` 升级到 `Table.StickyLock` @@ -30,15 +30,15 @@ ### `rowSelection` 模式,选择任意一个都是全选? -给定的数据源中的属性需要有一个唯一标示该条数据的主键,默认值为id,可通过 `primaryKey` 更改 e.g.`
`。 +给定的数据源中的属性需要有一个唯一标示该条数据的主键,默认值为 id,可通过 `primaryKey` 更改 e.g.`
`。 ### `rowSelection` 模式,如何设置默认选中/禁用? -通过受控模式,设置 `rowSelection.selectedRowKeys` 可以默认选中选中;通过 `rowSelection.getProps` 可以自定义每一行checkbox的props,具体可搜索demo`选择可控`。 +通过受控模式,设置 `rowSelection.selectedRowKeys` 可以默认选中选中;通过 `rowSelection.getProps` 可以自定义每一行 checkbox 的 props,具体可搜索 demo`选择可控`。 -### `rowSelection` 模式,如何屏蔽全选按钮/自定义全选按钮? +### `rowSelection` 模式,如何屏蔽全选按钮/自定义全选按钮? -通过`rowSelection.titleProps` 可以自定义选择列的表头的props,可通过 `style: {display: 'none'}` 屏蔽全选按钮;此外还有 `rowSelection.titleAddons` `rowSelection.columnProps`等属性,具体用法可搜索demo `可选择`。 +通过`rowSelection.titleProps` 可以自定义选择列的表头的 props,可通过 `style: {display: 'none'}` 屏蔽全选按钮;此外还有 `rowSelection.titleAddons` `rowSelection.columnProps`等属性,具体用法可搜索 demo `可选择`。 ### 支持行的双击事件/设置每一行的样式?处理整行点击? @@ -47,7 +47,7 @@ ### 已知问题 - 分组 Table 不支持在 Hover 状态和选中状态下显示背景色,无法合并单元格; -- 分组 Table ,`` 没有效果,header不会固定, `
` 才有效果,header可以sticky到页面上 +- 分组 Table,`
` 没有效果,header 不会固定, `
` 才有效果,header 可以 sticky 到页面上 ## 如何使用 @@ -58,24 +58,26 @@ ```jsx import { Table } from '@alifd/next'; -const dataSource = [{id: 1, time: '2016'}]; +const dataSource = [{ id: 1, time: '2016' }]; ReactDOM.render( - - - , mountNode); + + + , + mountNode +); ``` ### 列配置 -Table.Column 提供了非常多的配置属性用于自定义列,最常见的就是使用`cell`自定义单元格的渲染逻辑. 其他的配置选项可以参考下面的 Table.Column 的 API。 +Table.Column 提供了非常多的配置属性用于自定义列,最常见的就是使用`cell`自定义单元格的渲染逻辑。其他的配置选项可以参考下面的 Table.Column 的 API。 下面的代码会让`cell`根据值渲染不同的视图: ```jsx import { Table } from '@alifd/next'; -const dataSource = [{id: 1, time: '2016'}]; +const dataSource = [{ id: 1, time: '2016' }]; const renderTime = value => { if (value === '2016') { return '今年'; @@ -84,9 +86,11 @@ const renderTime = value => { }; ReactDOM.render(
- - -
, mountNode); + + + , + mountNode +); ``` ### 多表头 @@ -96,110 +100,183 @@ ReactDOM.render( ```jsx import { Table } from '@alifd/next'; -const dataSource = [{id: 1, time: '2016'}]; +const dataSource = [{ id: 1, time: '2016' }]; ReactDOM.render( - - + + - + -
, mountNode); + , + mountNode +); ``` ## API ### Table -| 参数 | 说明 | 类型 | 默认值 | 版本支持 | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -------- | ------- | -| tableLayout | 表格元素的 table-layout 属性,设为 fixed 表示内容不会影响列的布局

**可选值**:
'fixed', 'auto' | Enum | - | | -| size | 尺寸 small为紧凑模式

**可选值**:
'small', 'medium' | Enum | 'medium' | | -| tableWidth | 表格的总长度,可以这么用:设置表格总长度 、设置部分列的宽度,这样表格会按照剩余空间大小,自动其他列分配宽度 | Number | - | | -| dataSource | 表格展示的数据源 | Array | \[] | | -| onRowClick | 点击表格每一行触发的事件

**签名**:
Function(record: Object, index: Number, e: Event) => void
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
_e_: {Event} DOM事件对象 | Function | () => {} | | -| onRowMouseEnter | 悬浮在表格每一行的时候触发的事件

**签名**:
Function(record: Object, index: Number, e: Event) => void
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
_e_: {Event} DOM事件对象 | Function | () => {} | | -| onRowMouseLeave | 离开表格每一行的时候触发的事件

**签名**:
Function(record: Object, index: Number, e: Event) => void
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
_e_: {Event} DOM事件对象 | Function | () => {} | | -| onSort | 点击列排序触发的事件

**签名**:
Function(dataIndex: String, order: String) => void
**参数**:
_dataIndex_: {String} 指定的排序的字段
_order_: {String} 排序对应的顺序, 有`desc`和`asc`两种 | Function | () => {} | | -| onFilter | 点击过滤确认按钮触发的事件

**签名**:
Function(filterParams: Object) => void
**参数**:
_filterParams_: {Object} 过滤的字段信息 | Function | () => {} | | -| onResizeChange | 重设列尺寸的时候触发的事件

**签名**:
Function(dataIndex: String, value: Number) => void
**参数**:
_dataIndex_: {String} 指定重设的字段
_value_: {Number} 列宽变动的数值 | Function | () => {} | | -| rowProps | 设置每一行的属性,如果返回值和其他针对行操作的属性冲突则无效。

**签名**:
Function(record: Object, index: Number) => Object
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
**返回值**:
{Object} 需要设置的行属性
| Function | () => {} | | -| cellProps | 设置单元格的属性,通过该属性可以进行合并单元格

**签名**:
Function(rowIndex: Number, colIndex: Number, dataIndex: String, record: Object) => Object
**参数**:
_rowIndex_: {Number} 该行所对应的序列
_colIndex_: {Number} 该列所对应的序列
_dataIndex_: {String} 该列所对应的字段名称
_record_: {Object} 该行对应的记录
**返回值**:
{Object} 返回td元素的所支持的属性对象
| Function | () => {} | | -| keepForwardRenderRows | 虚拟滚动时向前保留渲染的行数 | Number | 10 | | -| hasBorder | 表格是否具有边框 | Boolean | true | | -| hasHeader | 表格是否具有头部 | Boolean | true | | -| isZebra | 表格是否是斑马线 | Boolean | false | | -| loading | 表格是否在加载中 | Boolean | false | | -| loadingComponent | 自定义 Loading 组件
请务必传递 props, 使用方式: loadingComponent={props => <Loading {...props}/>}

**签名**:
Function(props: LoadingProps) => React.ReactNode
**参数**:
_props_: {LoadingProps} 需要透传给组件的参数
**返回值**:
{React.ReactNode} 展示的组件
| Function | - | | -| filterParams | 当前过滤的的keys,使用此属性可以控制表格的头部的过滤选项中哪个菜单被选中,格式为 {dataIndex: {selectedKeys:\[]}}
示例:
假设要控制dataIndex为id的列的过滤菜单中key为one的菜单项选中
`` | Object | - | | -| sort | 当前排序的字段,使用此属性可以控制表格的字段的排序,格式为{[dataIndex]: 'asc' | 'desc' } , 例如 {id: 'desc'} | Object | - | -| sortIcons | 自定义排序按钮,例如上下排布的: `{desc: , asc: }` | Object | - | | -| columns | 等同于写子组件 Table.Column ,子组件优先级更高 | Array | - | | -| emptyContent | 设置数据为空的时候的表格内容展现 | ReactNode | - | | -| primaryKey | dataSource当中数据的主键,如果给定的数据源中的属性不包含该主键,会造成选择状态全部选中 | Symbol/String | 'id' | | -| expandedRowRender | 额外渲染行的渲染函数

**签名**:
Function(record: Object, index: Number) => Element
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
**返回值**:
{Element} 渲染内容
| Function | - | | -| rowExpandable | 设置行是否可展开,设置 false 为不可展开

**签名**:
Function(record: Object, index: Number) => Boolean
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
**返回值**:
{Boolean} 是否可展开
| Function | - | | -| expandedRowIndent | 额外渲染行的缩进, 是个二维数组(eg:[1,1]) 分别表示左右两边的缩进 | Array | - | | -| hasExpandedRowCtrl | 是否显示点击展开额外渲染行的+号按钮 | Boolean | - | | -| getExpandedColProps | 设置额外渲染行的属性

**签名**:
Function(record: Object, index: Number) => Object
**参数**:
_record_: {Object} 该行所对应的数据
_index_: {Number} 该行所对应的序列
**返回值**:
{Object} 额外渲染行的属性
| Function | - | | -| openRowKeys | 当前展开的 Expand行 或者 Tree行 , 传入此属性为受控状态,一般配合 onRowOpen 使用 | Array | - | | -| defaultOpenRowKeys | 默认情况下展开的 Expand行 或者 Tree行,非受控模式 | Array | - | 1.23.22 | -| onRowOpen | 在 Expand行 或者 Tree行 展开或者收起的时候触发的事件

**签名**:
Function(openRowKeys: Array, currentRowKey: String, expanded: Boolean, currentRecord: Object) => void
**参数**:
_openRowKeys_: {Array} 展开的渲染行的key
_currentRowKey_: {String} 当前点击的渲染行的key
_expanded_: {Boolean} 当前点击是展开还是收起
_currentRecord_: {Object} 当前点击额外渲染行的记录 | Function | - | | -| fixedHeader | 表头是否固定,该属性配合maxBodyHeight使用,当内容区域的高度超过maxBodyHeight的时候,在内容区域会出现滚动条 | Boolean | - | | -| maxBodyHeight | 最大内容区域的高度,在`fixedHeader`为`true`的时候,超过这个高度会出现滚动条 | Number/String | - | | -| rowSelection | 是否启用选择模式

**属性**:
_getProps_: {Function} `Function(record, index)=>Object` 获取selection的默认属性
_onChange_: {Function} `Function(selectedRowKeys:Array, records:Array)` 选择改变的时候触发的事件,**注意:** 其中records只会包含当前dataSource的数据,很可能会小于selectedRowKeys的长度。
_onSelect_: {Function} `Function(selected:Boolean, record:Object, records:Array)` 用户手动选择/取消选择某行的回调
_onSelectAll_: {Function} `Function(selected:Boolean, records:Array)` 用户手动选择/取消选择所有行的回调
_selectedRowKeys_: {Array} 设置了此属性,将rowSelection变为受控状态,接收值为该行数据的primaryKey的值
_mode_: {String} 选择selection的模式, 可选值为`single`, `multiple`,默认为`multiple`
_columnProps_: {Function} `Function()=>Object` 选择列 的props,例如锁列、对齐等,可使用`Table.Column` 的所有参数
_titleProps_: {Function} `Function()=>Object` 选择列 表头的props,仅在 `multiple` 模式下生效
_titleAddons_: {Function} `Function()=>Node` 选择列 表头添加的元素,在`single` `multiple` 下都生效 | Object | - | | -| stickyHeader | 表头是否是sticky | Boolean | - | | -| offsetTop | 距离窗口顶部达到指定偏移量后触发 | Number | - | | -| affixProps | affix组件的的属性 | Object | - | | -| indent | 在tree模式下的缩进尺寸, 仅在isTree为true时候有效 | Number | - | | -| isTree | 开启Table的tree模式, 接收的数据格式中包含children则渲染成tree table | Boolean | - | | -| useVirtual | 是否开启虚拟滚动 | Boolean | - | | -| scrollToRow | 滚动到第几行,需要保证行高相同。1.22.15 版本之前仅在虚拟滚动场景下生效,之后在所有情况下生效 | Number | - | 1.22.15 | -| onBodyScroll | 在内容区域滚动的时候触发的函数

**签名**:
Function() => void | Function | - | | -| expandedIndexSimulate | 开启时,getExpandedColProps() / rowProps() / expandedRowRender() 的第二个参数 index (该行所对应的序列) 将按照01,2,3,4...的顺序返回,否则返回真实index(0,2,4,6... / 1,3,5,7...) | Boolean | false | | -| crossline | 在 hover 时出现十字参考轴,适用于表头比较复杂,需要做表头分类的场景。 | Boolean | false | | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | -------- | -------- | +| size | 尺寸,small 为紧凑模式 | 'small' \| 'medium' | - | | - | +| className | 自定义类名 | string | - | | - | +| style | 自定义内联样式 | React.CSSProperties | - | | - | +| columns | 等同于写子组件 Table.Column,子组件优先级更高 | Array\ | - | | - | +| tableLayout | 表格元素的 table-layout 属性,设为 fixed 表示内容不会影响列的布局 | 'fixed' \| 'auto' | - | | - | +| tableWidth | 表格的总长度,可以这么用:设置表格总长度、设置部分列的宽度,这样表格会按照剩余空间大小,自动其他列分配宽度 | number | - | | - | +| dataSource | 表格展示的数据源 | Array\ | - | | - | +| hasBorder | 表格是否具有边框 | boolean | true | | - | +| hasHeader | 表格是否具有头部 | boolean | true | | - | +| isZebra | 表格是否是斑马线 | boolean | false | | - | +| loading | 表格是否在加载中 | boolean | false | | - | +| emptyContent | 设置数据为空的时候的表格内容展现 | React.ReactNode | - | | - | +| primaryKey | dataSource 中的主键,如果给定的数据源中的属性不包含该主键,会造成所有行全部选中等一系列问题 | string | 'id' | | - | +| onRowClick | 点击表格每一行触发的事件 | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| onRowMouseEnter | 悬浮在表格每一行的时候触发的事件 | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| onRowMouseLeave | 离开表格每一行的时候触发的事件 | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| onSort | 点击列排序触发的事件 | (dataIndex: string, order: SortOrder, sort: { [key: string]: SortOrder }) => void | - | | - | +| onFilter | 点击过滤确认按钮触发的事件 | (filterParams: {
[propName: string]: { selectedKeys: string[]; visible: boolean };
}) => void | - | | - | +| onResizeChange | 重设列尺寸的时候触发的事件 | (dataIndex: string, value: number) => void | - | | - | +| rowProps | 设置每一行的属性,如果返回值和其他针对行操作的属性冲突则无效。 | (
record: RecordItem,
index: number
) => (Record\ & Partial\) \| undefined \| void | - | | - | +| cellProps | 设置单元格的属性,通过该属性可以进行合并单元格 | (
rowIndex: number \| string,
colIndex: number \| string,
dataIndex: string,
record: RecordItem
) =>
\| Partial\<
Omit\<
CellProps,
\| 'prefix'
\| 'pure'
\| 'primaryKey'
\| 'record'
\| 'value'
\| 'colIndex'
\| 'rowIndex'
\| 'align'
\| 'locale'
\| 'rtl'
\| 'width'
>
>
\| undefined
\| void | - | | - | +| loadingComponent | 自定义 Loading 组件 | React.ElementType\<{ [prop: string]: unknown; className?: string }> | - | | - | +| filterParams | 当前过滤的 keys,使用此属性可以控制表格的头部的过滤选项中哪个菜单被选中,格式为 \{dataIndex: \{selectedKeys:[]\}\} | { [propName: string]: { selectedKeys?: string[]; visible?: boolean } } | - | | - | +| sort | 当前排序的字段,使用此属性可以控制表格的字段的排序,格式为\{dataIndex: 'asc'\} | { [key: string]: SortOrder } | - | | - | +| sortIcons | 自定义排序按钮,例如上下排布的:`{desc: , asc: }` | { desc?: React.ReactNode; asc?: React.ReactNode } | - | | - | +| expandedRowRender | 额外渲染行的渲染函数 | (record: RecordItem, index: number) => React.ReactNode | - | | - | +| rowExpandable | 设置行是否可展开,设置 false 为不可展开 | (record: RecordItem, index: number) => boolean | - | | - | +| expandedRowIndent | 额外渲染行的缩进,包含两个数字,第一个数字为左侧缩进,第二个数字为右侧缩进 | [number, number] | stickyLock ? [0, 0] : [1, 0] | | - | +| openRowKeys | 展开的行,传入后展开状态只受此属性控制 | Array\ | - | | - | +| defaultOpenRowKeys | 默认展开的行 | Array\ | - | | 1.23.22 | +| hasExpandedRowCtrl | 是否显示点击展开额外渲染行的 + 号按钮 | boolean | true | | - | +| getExpandedColProps | 设置额外的列属性 | (
record: RecordItem,
index: number
) => React.DetailedHTMLProps\, HTMLSpanElement> & {
disabled?: boolean;
} | - | | - | +| onRowOpen | 在额外渲染行或者树展开或者收起的时候触发的事件 | (
openRowKeys: Array\,
currentRowKey: string \| number,
expanded: boolean,
currentRecord: RecordItem
) => void | - | | - | +| onExpandedRowClick | 点击额外渲染行触发的事件 | (record: RecordItem, index: number, e: React.MouseEvent) => void | - | | - | +| fixedHeader | 表头是否固定,该属性配合 maxBodyHeight 使用,当内容区域的高度超过 maxBodyHeight 的时候,在内容区域会出现滚动条 | boolean | false | | - | +| maxBodyHeight | 最大内容区域的高度,在`fixedHeader`为`true`的时候,超过这个高度会出现滚动条 | number \| string | 200 | | - | +| rowSelection | 是否启用选择模式 | RowSelection \| null | - | | - | +| stickyHeader | 表头是否是 sticky | boolean | - | | - | +| offsetTop | 表头在距离窗口顶部达到此属性指定的偏移量后触发 sticky,仅在 stickyHeader 为 true 的时候有效 | number | - | | - | +| affixProps | Affix 组件的的属性,stickyHeader 基于 Affix 组件实现。 | AffixProps | - | | - | +| indent | 在 tree 模式下的缩进尺寸,仅在 isTree 为 true 时候有效 | number | 12 | | - | +| isTree | 开启 Table 的 tree 模式,接收的数据格式中包含 children 则渲染成 tree table | boolean | - | | - | +| useVirtual | 是否开启虚拟滚动 | boolean | - | | - | +| scrollToRow | 滚动到指定行 | number | - | | 1.22.15 | +| scrollToCol | 滚动到指定列 | number | - | | - | +| rowHeight | 设置行高 | number \| (() => number) | - | | - | +| onBodyScroll | 在内容区域滚动的时候触发的函数 | (start: number) => void | - | | - | +| expandedIndexSimulate | 开启时,getExpandedColProps() / getRowProps() / expandedRowRender() 的第二个参数 index (该行所对应的序列) 将按照 0,1,2,3,4...的顺序返回,否则返回真实 index(0,2,4,6... / 1,3,5,7...) | boolean | false | | - | +| crossline | 在 hover 时出现十字参考轴,适用于表头比较复杂,需要做表头分类的场景。 | boolean | false | | - | +| keepForwardRenderRows | 虚拟滚动时向前保留渲染的行数 | number | 10 | | - | +| components | 自定义组件,高级用法,用于替换 Table 内部的组件 | {
Cell?: CellLike;
Filter?: FilterLike;
Sort?: SortLike;
Resize?: ResizeLike;
Row?: RowLike;
Header?: HeaderLike;
Wrapper?: WrapperLike;
Body?: BodyLike;
} | - | | - | ### Table.Column -| 参数 | 说明 | 类型 | 默认值 | 版本支持 | -| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------------------- | ---- | -| dataIndex | 指定列对应的字段,支持`a.b`形式的快速取值 | String | - | | -| cell | 行渲染的逻辑
value, rowIndex, record, context四个属性只可读不可被更改
Function(value, index, record) => Element | ReactElement/ReactNode/Function | value => value | | -| title | 表头显示的内容 | ReactElement/ReactNode/Function | - | | -| htmlTitle | 写到 header 单元格上的title属性 | String | - | | -| sortable | 是否支持排序 | Boolean | - | | -| sortDirections | 排序的方向。
设置 ['desc', 'asc'],表示降序、升序
设置 ['desc', 'asc', 'default'],表示表示降序、升序、不排序 | Array<Enum> | - | 1.23 | -| width | 列宽,注意在锁列的情况下一定需要配置宽度 | Number/String | - | | -| align | 单元格的对齐方式

**可选值**:
'left', 'center', 'right' | Enum | - | | -| alignHeader | 单元格标题的对齐方式, 不配置默认读取align值

**可选值**:
'left', 'center', 'right' | Enum | - | | -| filters | 生成标题过滤的菜单, 格式为`[{label:'xxx', value:'xxx'}]` | Array<Object> | - | | -| filterMode | 过滤的模式是单选还是多选

**可选值**:
'single', 'multiple' | Enum | 'multiple' | | -| filterMenuProps | filter 模式下传递给 Menu 菜单的属性, 默认继承 `Menu` 组件的API

**属性**:
_subMenuSelectable_: {Boolean} 默认为`false` subMenu是否可选择
_isSelectIconRight_: {Boolean} 默认为`false` 是否将选中图标居右。注意:SubMenu 上的选中图标一直居左,不受此API控制 | Object | { subMenuSelectable: false, } | | -| lock | 是否支持锁列,可选值为`left`,`right`, `true` | Boolean/String | - | | -| resizable | 是否支持列宽调整, 当该值设为true,table的布局方式会修改为fixed. | Boolean | false | | -| asyncResizable | (推荐使用)是否支持异步列宽调整, 当该值设为true,table的布局方式会修改为fixed. | Boolean | false | 1.24 | -| colSpan | header cell 横跨的格数,设置为0表示不出现此 th | Number | - | | -| wordBreak | 设置该列单元格的word-break样式,对于id类、中文类适合用all,对于英文句子适合用word

**可选值**:
'all'(all)
'word'(word) | Enum | all | 1.23 | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| --------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------ | -------- | -------- | +| dataIndex | 指定列对应的字段,支持`a.b`形式的快速取值 | string | - | | - | +| cell | 行渲染的逻辑 | \| React.ReactNode
\| ((value: unknown, rowIndex: number, record: RecordItem) => React.ReactNode) | - | | - | +| title | 表头显示的内容 | React.ReactNode \| (() => React.ReactNode) | - | | - | +| htmlTitle | 写到 header 单元格上的 title 属性 | string | - | | - | +| sortable | 是否支持排序 | boolean | - | | - | +| width | 列宽,注意在锁列的情况下一定需要配置宽度 | number \| string | - | | - | +| align | 单元格的对齐方式 | 'left' \| 'center' \| 'right' | - | | - | +| sortDirections | 排序的方向。设置 ['desc', 'asc'],表示降序、升序。设置 ['desc', 'asc', 'default'],表示表示降序、升序、不排序 | Array\ | - | | 1.23 | +| alignHeader | 标题单元格的对齐方式,如果不配置,默认读取 align 值 | 'left' \| 'center' \| 'right' | - | | - | +| filters | 生成标题过滤的菜单,格式为`[{label:'xxx', value:'xxx'}]` | FilterItem[] | - | | - | +| filterMode | 过滤的模式是单选还是多选 | 'single' \| 'multiple' | 'multiple' | | - | +| lock | 是否支持锁列,可选值为`left`,`right`, `true` | boolean \| 'left' \| 'right' | - | | - | +| resizable | 是否支持列宽调整,当该值设为 true,table 的布局方式会修改为 fixed. | boolean | false | | - | +| asyncResizable | (推荐使用)是否支持异步列宽调整,当该值设为 true,table 的布局方式会修改为 fixed. | boolean | false | | 1.24 | +| colSpan | header cell 横跨的格数,设置为 0 表示不出现此 th | number | - | | - | +| wordBreak | 设置该列单元格的 word-break 样式,对于 id 类、中文类适合用 all,对于英文句子适合用 word | 'all' \| 'word' | - | | 1.23 | +| filterMenuProps | filter 模式下传递给 Menu 菜单的属性,默认继承 `Menu` 组件的 API | MenuProps & { subMenuSelectable?: boolean } | \{ subMenuSelectable: false \} | | - | +| filterProps | 传递给 Filter 的下拉组件的属性 | DropdownProps | - | | - | ### Table.ColumnGroup -| 参数 | 说明 | 类型 | 默认值 | -| ----- | ------- | ------------------------------- | -------------- | -| title | 表头显示的内容 | ReactElement/ReactNode/Function | 'column-group' | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ----- | -------------- | ------------------------------------------ | ------ | -------- | +| title | 表头显示的内容 | React.ReactNode \| (() => React.ReactNode) | - | | ### Table.GroupHeader -| 参数 | 说明 | 类型 | 默认值 | -| ------------------------------- | ------------------------------------- | ------------------------------- | -------- | -| cell | 行渲染的逻辑 | ReactElement/ReactNode/Function | () => '' | -| hasChildrenSelection | 是否在Children上面渲染selection | Boolean | false | -| hasSelection | 是否在GroupHeader上面渲染selection | Boolean | true | -| useFirstLevelDataWhenNoChildren | 当 dataSouce 里没有 children 时,是否使用内容作为数据 | Boolean | false | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- | ------ | -------- | +| cell | 行渲染的逻辑 | React.ReactNode \| ((value: RecordItem, index: number) => React.ReactNode) | - | | +| hasChildrenSelection | 是否在 Children 上面渲染 selection | boolean | false | | +| hasSelection | 是否在 GroupHeader 上面渲染 selection | boolean | true | | +| useFirstLevelDataWhenNoChildren | 当 dataSource 里没有 children 时,是否使用内容作为数据 | boolean | false | | ### Table.GroupFooter -| 参数 | 说明 | 类型 | 默认值 | -| ---- | ------ | ------------------------------- | -------- | -| cell | 行渲染的逻辑 | ReactElement/ReactNode/Function | () => '' | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ---- | ------------ | ---------------------------------------------------------------------------------- | ------ | -------- | +| cell | 行渲染的逻辑 | \| React.ReactNode
\| ((value: RecordItem, index: number) => React.ReactNode) | - | | + +### CellProps + +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| --------------------- | ---- | ----------------------------------- | ------ | -------- | +| pure | - | boolean | - | | +| prefix | - | TableProps['prefix'] | - | 是 | +| className | - | string | - | | +| value | - | unknown | - | | +| record | - | RecordItem | - | | +| context | - | unknown | - | | +| colIndex | - | number | - | | +| rowIndex | - | number | - | | +| \_\_colIndex | - | number \| string | - | | +| style | - | React.CSSProperties | - | | +| component | - | React.ElementType | - | | +| children | - | React.ReactNode | - | | +| innerStyle | - | React.CSSProperties | - | | +| \_\_normalized | - | boolean | - | | +| expandedIndexSimulate | - | TableProps['expandedIndexSimulate'] | - | | +| getCellDomRef | - | React.LegacyRef\ | - | | +| primaryKey | - | TableProps['primaryKey'] | - | | +| rowSpan | - | number | - | | + +### RowProps + +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------ | ---- | ------------------------------------------------------------------- | ------ | -------- | +| className | - | string | - | | +| rowIndex | - | number | - | 是 | +| \_\_rowIndex | - | number | - | 是 | +| onClick | - | (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void | - | 是 | +| onMouseEnter | - | (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void | - | 是 | +| onMouseLeave | - | (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void | - | 是 | +| Cell | - | CellLike | - | 是 | +| children | - | React.ReactNode | - | 是 | +| record | - | RecordItem | - | 是 | +| wrapper | - | (wrapper: React.ReactElement) => React.ReactNode | - | 是 | + +### RowSelection + +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| --------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------- | -------- | +| getProps | 获取 selection 的默认属性 | (record: RecordItem, index: number) => CheckboxProps \| RadioProps | - | | +| onChange | 选择改变的时候触发的事件,**注意:** 其中 records 只会包含当前 dataSource 的数据,很可能会小于 selectedRowKeys 的长度。 | (selectedRowKeys: Array\, records: Array\) => void | - | | +| onSelect | 用户手动选择/取消选择某行的回调 | (selected: boolean, record: RecordItem, records: RecordItem[]) => void | - | | +| onSelectAll | 用户手动选择/取消选择所有行的回调 | (selected: boolean, records: RecordItem[]) => void | - | | +| selectedRowKeys | 设置了此属性,将 rowSelection 变为受控状态,接收值为该行数据的 primaryKey 的值 | Array\ | - | | +| mode | 选择 selection 的模式 | 'single' \| 'multiple' | 'multiple' | | +| titleProps | 选择列表头的 props,仅在 `multiple` 模式下生效 | () => CheckboxProps | - | | +| columnProps | 选择列的 props,例如锁列、对齐等,可使用 `Table.Column` 的所有参数 | () => Partial\ | - | | +| titleAddons | 选择列表头添加的元素,在`single` `multiple` 下都生效 | () => React.ReactNode | - | | + +### RecordItem + +```typescript +export type RecordItem = Record & { children?: RecordItem[] }; +``` + +### SortOrder + +```typescript +export type SortOrder = 'desc' | 'asc' | 'default'; +``` diff --git a/components/table/__docs__/theme/index.jsx b/components/table/__docs__/theme/index.jsx deleted file mode 100644 index 5ea8a1907f..0000000000 --- a/components/table/__docs__/theme/index.jsx +++ /dev/null @@ -1,448 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import '../../../demo-helper/style'; -import '../../style'; -import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; -import ConfigProvider from '../../../config-provider'; -import zhCN from '../../../locale/zh-cn'; -import enUS from '../../../locale/en-us'; -import Table from '../../index'; - -const i18nMap = { - 'en-us': { - productDetail: 'Product Details', - price: 'Price', - status: 'Status', - view: 'View', - priced: 'Already Priced', - noPriced: 'No Priced', - source: ['2014 New Fashion Novelty Tank Slim Women\'s Fashion Dresses With Lace', 'Free shipping women Casual dresses lady dress plus size 2014'] - }, - 'zh-cn': { - productDetail: '产品详情', - price: '价格', - status: '状态', - view: '查看', - priced: '已报价', - noPriced: '未报价', - source: ['中文范例,为了那失去的青春,诗和远方', '中文范例,为了那忘却的纪念,你和你的爱情'] - } -}; - - - -const convert = (object) => { - const obj = {}; - Object.keys(object).forEach(key => { - obj[key] = object[key].value; - }); - return obj; -}; - -const normalize = (demoFunction) => { - const ret = demoFunction.reduce((current, value) => { - current[value.name] = {}; - current[value.name].label = value.label; - current[value.name].value = value.value; - current[value.name].enum = value.enum.map(item => ({ - label: item.label ? item.label : (item === 'true' ? '显示' : '隐藏'), - value: item.label ? item.value : item - })); - return current; - }, {}); - return ret; -}; - -class FunctionDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - demoFunction: normalize([ - { - label: '筛选', - name: 'filters', - value: 'false', - enum: ['true', 'false'] - }, - { - label: '排序', - name: 'sortable', - value: 'false', - enum: ['true', 'false'] - }, - { - label: '表格有无竖线', - name: 'hasBorder', - value: 'false', - enum: ['true', 'false'] - } - ]) - }; - - this.onFunctionChange = this.onFunctionChange.bind(this); - } - - onFunctionChange(demoFunction) { - this.setState({ - demoFunction - }); - } - - render() { - const { demoFunction } = this.state; - const functions = convert(demoFunction); - const { lang } = this.props; - const i18n = i18nMap[lang]; - const { size } = functions; - const rowSelection = { - mode: functions.rowSelection, - selectedRowKeys: [4] - }; - let filters, sortable = false, hasBorder = false; - if (functions.filters === 'true') { - filters = [{ label: 'Option1', value: 'Option1' }, { label: 'Option2', value: 'Option2' }]; - } - - if (functions.sortable === 'true') { - sortable = true; - } - - if (functions.hasBorder === 'true') { - hasBorder = true; - } - - function productRender(product) { - return ( -
- -
{product[0].title}
-
- ); - } - - function statusRender(status) { - if (status) { - return i18n.priced; - } else { - return i18n.noPriced; - } - } - - function priceRender(price) { - return {price}; - } - - function operRender() { - return {i18n.view}; - } - function groupHeaderRender(record) { - return
{record.product[0].title}
; - } - function groupFooterRender(record) { - return
{record.product[0].title}
; - } - const listDataSource = [{ - price: '$2.45(USD)', - status: 0, - id: 1, - product: [{ - title: '2014 New Fashion Novelty Tank Slim Women\'s Fashion Dresses With Lace', - avatar: 'https://sc01.alicdn.com/kf/HTB1ravHKXXXXXccXVXXq6xXFXXXJ/Chinese-Style-Fashion-Custom-Digital-Print-Silk.jpg_220x220.jpg' - }], - children: [{ - price: '$2.5(USD)', - status: 1, - id: 2, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }, { - price: '$2.5(USD)', - status: 1, - id: 3, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }] - }, { - price: '$2.5(USD)', - status: 1, - id: 4, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }], - children: [{ - price: '$2.5(USD)', - status: 1, - id: 5, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }, { - price: '$2.5(USD)', - status: 1, - id: 6, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }] - }]; - const getSelectedRowProps = (record, index) => ({ - className: index === 1 ? 'selected' : '' - }); - const cols = [ - , - , - - ]; - - const smallCols = [].concat( - - , cols); - - cols.unshift(); - - const groupCols = cols.slice(); - groupCols.unshift(); - groupCols.push(); - - const smallGroupCols = smallCols.slice(); - smallGroupCols.unshift(); - smallGroupCols.push(); - - return ( - - - -
{}}>{cols}
- - - {}}>{cols}
-
- - {}}>{cols}
-
- - - - {}}>{smallCols}
-
-
- - - {}}>{groupCols}
-
- - {}}>{groupCols}
-
- - {}}>{groupCols}
-
-
- - - {}}>{smallGroupCols}
-
-
- - ); - } -} - -class TableFunctionDemo extends React.Component { - constructor(props) { - super(props); - const demoFunction = [ - { - label: '斑马线', - name: 'zebra', - value: 'false', - enum: ['true', 'false'] - }, - { - label: '选择', - name: 'rowSelection', - value: 'false', - enum: [{ value: 'single', label: '单选' }, { value: 'multiple', label: '多选' }, 'false'] - }, - { - label: '单列对齐方式', - name: 'align', - value: 'left', - enum: [{ value: 'left', label: '左对齐' }, { value: 'center', label: '居中对齐' }, { value: 'right', label: '右对齐' }] - }, - { - label: '表头', - name: 'hasHeader', - value: 'true', - enum: ['true', 'false'] - }, - { - label: '表格有无竖线', - name: 'hasBorder', - value: 'false', - enum: ['true', 'false'] - } - ]; - - - this.state = { - demoFunction: normalize(demoFunction) - }; - - this.onFunctionChange = this.onFunctionChange.bind(this); - } - - onFunctionChange(demoFunction) { - this.setState({ - demoFunction - }); - } - - render() { - const { demoFunction } = this.state; - const functions = convert(demoFunction); - const { lang } = this.props; - const i18n = i18nMap[lang]; - const { size } = functions; - const rowSelection = functions.rowSelection === 'false' ? null : { - mode: functions.rowSelection, - selectedRowKeys: [1] - }; - let filters; - if (functions.filters) { - filters = [{ label: 'Option1', value: 'Option1' }, { label: 'Option2', value: 'Option2' }]; - } - const hasHeader = functions.hasHeader === 'true'; - const hasBorder = functions.hasBorder === 'true'; - const isZebra = functions.zebra === 'true'; - const align = functions.align; - const listDataSource = [{ - price: '$2.45(USD)', - status: 0, - id: 1, - product: [{ - title: '2014 New Fashion Novelty Tank Slim Women\'s Fashion Dresses With Lace', - avatar: 'https://sc01.alicdn.com/kf/HTB1ravHKXXXXXccXVXXq6xXFXXXJ/Chinese-Style-Fashion-Custom-Digital-Print-Silk.jpg_220x220.jpg' - }], - children: [{ - price: '$2.5(USD)', - status: 1, - id: 2, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }, { - price: '$2.5(USD)', - status: 1, - id: 3, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }] - }, { - price: '$2.5(USD)', - status: 1, - id: 4, - product: [{ - title: 'Free shipping women Casual dresses lady dress plus size 2014', - avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg' - }] - }]; - - function productRender(product) { - return ( -
- -
{product[0].title}
-
- ); - } - - function statusRender(status) { - if (status) { - return i18n.priced; - } else { - return i18n.noPriced; - } - } - - function priceRender(price) { - return {price}; - } - - function operRender() { - return {i18n.view}; - } - - function groupHeaderRender(record) { - return
{record.product[0].title}
; - } - - function groupFooterRender(record) { - return
{record.product[0].title}
; - } - - const cols = [ - , - , - - ]; - const smallCols = [].concat( - - , cols); - - cols.unshift(); - const groupCols = cols.slice(); - groupCols.unshift(); - groupCols.push(); - - return ( - - - - {cols}
-
- - {cols}
-
- - record.price} dataSource={listDataSource} hasBorder={hasBorder} hasHeader={hasHeader} isZebra={isZebra}>{cols}
-
- - {cols.slice(1)}
-
- - {cols}
-
-
- - - {smallCols}
-
-
- -
- ); - } -} - -window.renderDemo = function (lang) { - ReactDOM.render(( - -
- - -
-
- ), document.getElementById('container')); -}; -window.renderDemo('en-us'); -initDemo('table'); diff --git a/components/table/__docs__/theme/index.tsx b/components/table/__docs__/theme/index.tsx new file mode 100644 index 0000000000..8768a31cb4 --- /dev/null +++ b/components/table/__docs__/theme/index.tsx @@ -0,0 +1,652 @@ +/* eslint-disable react/prop-types */ +import React, { type ReactElement } from 'react'; +import ReactDOM from 'react-dom'; +import '../../../demo-helper/style'; +import '../../style'; +import { Demo, type DemoFunctionDefineForObject, DemoGroup, initDemo } from '../../../demo-helper'; +import ConfigProvider from '../../../config-provider'; +import zhCN from '../../../locale/zh-cn'; +import enUS from '../../../locale/en-us'; +import Table, { type ColumnProps, type TableProps } from '../../index'; + +const i18nMap = { + 'en-us': { + productDetail: 'Product Details', + price: 'Price', + status: 'Status', + view: 'View', + priced: 'Already Priced', + noPriced: 'No Priced', + source: [ + "2014 New Fashion Novelty Tank Slim Women's Fashion Dresses With Lace", + 'Free shipping women Casual dresses lady dress plus size 2014', + ], + }, + 'zh-cn': { + productDetail: '产品详情', + price: '价格', + status: '状态', + view: '查看', + priced: '已报价', + noPriced: '未报价', + source: [ + '中文范例,为了那失去的青春,诗和远方', + '中文范例,为了那忘却的纪念,你和你的爱情', + ], + }, +}; + +const convert = (object: ReturnType) => { + const obj: { + [key in keyof typeof object]?: string; + } = {}; + Object.keys(object).forEach((key: keyof typeof object) => { + obj[key] = object[key].value as string; + }); + return obj; +}; + +const functions: { + label: string; + name: string; + value: string; + enum: ( + | { + label: string; + value: string; + } + | string + )[]; +}[] = [ + { + label: '筛选', + name: 'filters', + value: 'false', + enum: ['true', 'false'], + }, + { + label: '排序', + name: 'sortable', + value: 'false', + enum: ['true', 'false'], + }, + { + label: '表格有无竖线', + name: 'hasBorder', + value: 'false', + enum: ['true', 'false'], + }, +]; + +const normalize = (demoFunction: typeof functions) => { + const ret = demoFunction.reduce( + (current, value) => { + current[value.name] = {} as DemoFunctionDefineForObject; + current[value.name].label = value.label; + current[value.name].value = value.value; + current[value.name].enum = value.enum.map(item => ({ + label: typeof item === 'object' ? item.label : item === 'true' ? '显示' : '隐藏', + value: typeof item === 'object' ? item.value : item, + })); + return current; + }, + {} as Record + ); + return ret; +}; + +class FunctionDemo extends React.Component<{ lang: 'zh-cn' | 'en-us' }> { + state = { + demoFunction: normalize(functions), + }; + + onFunctionChange = (demoFunction: ReturnType) => { + this.setState({ + demoFunction, + }); + }; + + render() { + const { demoFunction } = this.state; + const functions = convert(demoFunction); + const { lang } = this.props; + const i18n = i18nMap[lang]; + const rowSelection: TableProps['rowSelection'] = { + mode: functions.rowSelection as 'single' | 'multiple', + selectedRowKeys: [4], + }; + let filters, + sortable = false, + hasBorder = false; + if (functions.filters === 'true') { + filters = [ + { label: 'Option1', value: 'Option1' }, + { label: 'Option2', value: 'Option2' }, + ]; + } + + if (functions.sortable === 'true') { + sortable = true; + } + + if (functions.hasBorder === 'true') { + hasBorder = true; + } + + function productRender(product: { avatar: string; title: string }[]) { + return ( +
+ +
{product[0].title}
+
+ ); + } + + function statusRender(status: boolean) { + if (status) { + return i18n.priced; + } else { + return i18n.noPriced; + } + } + + function priceRender(price: string) { + return {price}; + } + + function operRender() { + return {i18n.view}; + } + function groupHeaderRender(record: { product: { title: string }[] }) { + return
{record.product[0].title}
; + } + function groupFooterRender(record: { product: { title: string }[] }) { + return
{record.product[0].title}
; + } + const listDataSource = [ + { + price: '$2.45(USD)', + status: 0, + id: 1, + product: [ + { + title: "2014 New Fashion Novelty Tank Slim Women's Fashion Dresses With Lace", + avatar: 'https://sc01.alicdn.com/kf/HTB1ravHKXXXXXccXVXXq6xXFXXXJ/Chinese-Style-Fashion-Custom-Digital-Print-Silk.jpg_220x220.jpg', + }, + ], + children: [ + { + price: '$2.5(USD)', + status: 1, + id: 2, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + { + price: '$2.5(USD)', + status: 1, + id: 3, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + ], + }, + { + price: '$2.5(USD)', + status: 1, + id: 4, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + children: [ + { + price: '$2.5(USD)', + status: 1, + id: 5, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + { + price: '$2.5(USD)', + status: 1, + id: 6, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + ], + }, + ]; + const getSelectedRowProps: TableProps['rowProps'] = (record, index) => ({ + className: index === 1 ? 'selected' : '', + }); + const cols = [ + , + , + , + ]; + + const smallCols = ([] as ReactElement[]).concat( + , + cols + ); + + cols.unshift( + + ); + + const groupCols = cols.slice(); + groupCols.unshift(); + groupCols.push(); + + const smallGroupCols = smallCols.slice(); + smallGroupCols.unshift(); + smallGroupCols.push(); + + return ( + + + + {}}> + {cols} +
+
+ + {}} + > + {cols} +
+
+ + {}}> + {cols} +
+
+
+ + + {}} + > + {smallCols} +
+
+
+ + + {}}> + {groupCols} +
+
+ + {}} + > + {groupCols} +
+
+ + {}}> + {groupCols} +
+
+
+ + + {}} + > + {smallGroupCols} +
+
+
+
+ ); + } +} +const demoFunction = [ + { + label: '斑马线', + name: 'zebra', + value: 'false', + enum: ['true', 'false'], + }, + { + label: '选择', + name: 'rowSelection', + value: 'false', + enum: [{ value: 'single', label: '单选' }, { value: 'multiple', label: '多选' }, 'false'], + }, + { + label: '单列对齐方式', + name: 'align', + value: 'left', + enum: [ + { value: 'left', label: '左对齐' }, + { value: 'center', label: '居中对齐' }, + { value: 'right', label: '右对齐' }, + ], + }, + { + label: '表头', + name: 'hasHeader', + value: 'true', + enum: ['true', 'false'], + }, + { + label: '表格有无竖线', + name: 'hasBorder', + value: 'false', + enum: ['true', 'false'], + }, +]; + +class TableFunctionDemo extends React.Component<{ lang: 'zh-cn' | 'en-us' }> { + state = { + demoFunction: normalize(demoFunction), + }; + + onFunctionChange = (demoFunction: ReturnType) => { + this.setState({ + demoFunction, + }); + }; + + render() { + const { demoFunction } = this.state; + const functions = convert(demoFunction); + const { lang } = this.props; + const i18n = i18nMap[lang]; + const rowSelection: TableProps['rowSelection'] = + functions.rowSelection === 'false' + ? null + : { + mode: functions.rowSelection as 'single' | 'multiple', + selectedRowKeys: [1], + }; + let filters; + if (functions.filters) { + filters = [ + { label: 'Option1', value: 'Option1' }, + { label: 'Option2', value: 'Option2' }, + ]; + } + const hasHeader = functions.hasHeader === 'true'; + const hasBorder = functions.hasBorder === 'true'; + const isZebra = functions.zebra === 'true'; + const align = functions.align as ColumnProps['align']; + const listDataSource = [ + { + price: '$2.45(USD)', + status: 0, + id: 1, + product: [ + { + title: "2014 New Fashion Novelty Tank Slim Women's Fashion Dresses With Lace", + avatar: 'https://sc01.alicdn.com/kf/HTB1ravHKXXXXXccXVXXq6xXFXXXJ/Chinese-Style-Fashion-Custom-Digital-Print-Silk.jpg_220x220.jpg', + }, + ], + children: [ + { + price: '$2.5(USD)', + status: 1, + id: 2, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + { + price: '$2.5(USD)', + status: 1, + id: 3, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + ], + }, + { + price: '$2.5(USD)', + status: 1, + id: 4, + product: [ + { + title: 'Free shipping women Casual dresses lady dress plus size 2014', + avatar: 'https://sc02.alicdn.com/kf/HTB1efnNLVXXXXbtXpXXq6xXFXXXN/Light-100-acrylic-fashionabe-snood-shawl-weight.jpg_220x220.jpg', + }, + ], + }, + ]; + + function productRender(product: { title: string; avatar: string }[]) { + return ( +
+ +
{product[0].title}
+
+ ); + } + + function statusRender(status: boolean) { + if (status) { + return i18n.priced; + } else { + return i18n.noPriced; + } + } + + function priceRender(price: string) { + return {price}; + } + + function operRender() { + return {i18n.view}; + } + + function groupHeaderRender(record: { product: { title: string }[] }) { + return
{record.product[0].title}
; + } + + function groupFooterRender(record: { product: { title: string }[] }) { + return
{record.product[0].title}
; + } + + const cols = [ + , + , + , + ]; + const smallCols = ([] as ReactElement[]).concat( + , + cols + ); + + cols.unshift( + + ); + const groupCols = cols.slice(); + groupCols.unshift(); + groupCols.push(); + + return ( + + + + + {cols} +
+
+ + + {cols} +
+
+ + record.price} + dataSource={listDataSource} + hasBorder={hasBorder} + hasHeader={hasHeader} + isZebra={isZebra} + > + {cols} +
+
+ + + {cols.slice(1)} +
+
+ + + {cols} +
+
+
+ + + + {smallCols} +
+
+
+
+ ); + } +} + +window.renderDemo = function (lang: 'zh-cn' | 'en-us') { + ReactDOM.render( + +
+ + +
+
, + document.getElementById('container') + ); +}; +window.renderDemo('en-us'); +initDemo('table'); diff --git a/components/table/__tests__/index-spec.js b/components/table/__tests__/index-spec.js deleted file mode 100644 index 83811e784a..0000000000 --- a/components/table/__tests__/index-spec.js +++ /dev/null @@ -1,1283 +0,0 @@ -import React from 'react'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import Promise from 'promise-polyfill'; -import sinon from 'sinon'; -import Loading from '../../loading'; -import Icon from '../../icon'; -import Checkbox from '../../checkbox'; -import Table from '../index'; - -/* eslint-disable */ -Enzyme.configure({ adapter: new Adapter() }); - -describe('Table', () => { - let dataSource = [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }], - table, - wrapper, - timeout, - stickyLock, - stickyLockWrapper, - timeoutStickyLock; - - beforeEach(() => { - table = ( - - - -
- ); - - stickyLock = ( - - - - - ); - - wrapper = mount(table); - stickyLockWrapper = mount(stickyLock); - timeout = (props, callback) => { - return new Promise(resolve => { - wrapper.setProps(props); - setTimeout(function() { - resolve(); - }, 10); - }).then(callback); - }; - - timeoutStickyLock = (props, callback) => { - return new Promise(resolve => { - stickyLockWrapper.setProps(props); - setTimeout(function() { - resolve(); - }, 10); - }).then(callback); - }; - }); - - afterEach(() => { - table = null; - stickyLockWrapper = null; - }); - - it('should mount table', () => { - assert(wrapper.find('.next-table-body .next-table-row').length === 2); - }); - - it('should render checkboxMode', done => { - timeout( - { - rowSelection: { - getProps: record => { - if (record.id === '1') { - return { - disabled: true, - }; - } - }, - }, - }, - () => { - assert(wrapper.find(Checkbox).length === 3); - assert(wrapper.find('.next-checkbox-wrapper.disabled').length === 1); - wrapper - .find('.next-checkbox') - .at(2) - .find('input') - .simulate('click'); - wrapper - .find('.next-checkbox') - .at(2) - .find('input') - .simulate('change'); - done(); - } - ); - }); - - it('should support rowSelect control', done => { - timeout( - { - rowSelection: { - selectedRowKeys: ['1'], - }, - }, - () => { - assert(wrapper.find('.next-checkbox-wrapper.checked').length === 1); - done(); - } - ); - }); - - it('should render when dataSource is [] & children is null', done => { - timeout( - { - dataSource: [], - children: [], - }, - () => { - assert(wrapper); - done(); - } - ); - }); - - it('should render when dataSource is made of string', done => { - timeout( - { - dataSource: ['string1', 'string2'], - children: [ record} />], - }, - () => { - assert(wrapper); - done(); - } - ); - }); - - it('should render RadioMode', done => { - timeout( - { - rowSelection: { - mode: 'single', - }, - }, - () => { - assert(wrapper.find('.next-radio').length === 2); - done(); - } - ); - }); - - it('should support columnProps/titleProps/titleAddons of rowSelection', done => { - timeout( - { - rowSelection: { - columnProps: () => { - return { - lock: 'right', - width: 90, - align: 'center', - }; - }, - titleAddons: () => { - return
请选择
; - }, - titleProps: () => { - return { - disabled: true, - children: '>', - }; - }, - }, - }, - () => { - assert( - wrapper - .find('#table-titleAddons') - .at(0) - .text() === '请选择' - ); - assert( - wrapper - .find('colgroup') - .at(2) - .props().children[0].props.style.width === 90 - ); - assert( - wrapper - .find('th .next-checkbox-wrapper') - .at(1) - .hasClass('disabled') - ); - assert( - wrapper - .find('th .next-checkbox-wrapper .next-checkbox-label') - .at(0) - .text() === '>' - ); - done(); - } - ); - }); - - it('should support events', done => { - const onRowClick = sinon.spy(); - const onRowMouseEnter = sinon.spy(); - const onRowMouseLeave = sinon.spy(); - timeout( - { - onRowClick, - onRowMouseEnter, - onRowMouseLeave, - }, - () => { - const row = wrapper.find('.next-table-body .next-table-row').first(); - row.simulate('click'); - assert(onRowClick.called); - row.simulate('mouseenter'); - assert(onRowMouseEnter.called); - row.simulate('mouseleave'); - assert(onRowMouseLeave.called); - done(); - } - ); - }); - - it('should support sort', done => { - const onSort = (dataIndex, order) => { - assert(dataIndex === 'id'); - assert(order === 'desc'); - }; - - timeout( - { - children: [, ], - onSort, - }, - () => { - const sortNode = wrapper.find('.next-table-header .next-table-sort'); - sortNode.simulate('click'); - done(); - } - ); - }); - - it('should support tableLayout&tableWidth', done => { - timeout( - { - children: [, ], - tableLayout: 'fixed', - tableWidth: 1200, - }, - () => { - const tablewrapper = wrapper.find('.next-table'); - const table = wrapper.find('.next-table table'); - - assert(tablewrapper.hasClass('next-table-layout-fixed')); - assert(table.at(0).props().style.width === 1200); - done(); - } - ); - }); - it('should support sortIcons', done => { - const onSort = (dataIndex, order) => { - assert(dataIndex === 'id'); - assert(order === 'desc'); - }; - - timeout( - { - children: [, ], - onSort, - sortIcons: { - desc: , - asc: , - }, - }, - () => { - const sortNode = wrapper.find('.next-table-header .next-table-sort'); - assert(sortNode.find('.next-icon-arrow-down')); - assert(sortNode.find('.next-icon-arrow-up')); - sortNode.simulate('click'); - done(); - } - ); - }); - - it('should support getRowProps for setting className', done => { - const getRowProps = record => { - if (record.id === '1') { - return { className: 'rowClassName' }; - } - }; - timeout( - { - getRowProps, - }, - () => { - const row = wrapper.find('.next-table-body .next-table-row').first(); - assert(row.hasClass('rowClassName')); - done(); - } - ); - }); - - it('should support rowProps for setting className', done => { - const rowProps = record => { - if (record.id === '1') { - return { className: 'rowClassName' }; - } - }; - timeout( - { - rowProps, - }, - () => { - const row = wrapper.find('.next-table-body .next-table-row').first(); - assert(row.hasClass('rowClassName')); - done(); - } - ); - }); - - it('should support fixedHeader, isZebra, hasBorder, loading', done => { - timeout( - { - fixedHeader: true, - }, - () => { - assert(wrapper.find('div.next-table-fixed').length === 1); - } - ) - .then(() => { - return timeout( - { - isZebra: true, - }, - () => { - assert(wrapper.find('div.zebra').length === 1); - } - ); - }) - .then(() => { - return timeout( - { - hasBorder: false, - }, - () => { - assert(wrapper.find('div.only-bottom-border').length === 1); - } - ); - }) - .then(() => { - return timeout( - { - loading: true, - }, - () => { - wrapper.debug(); - assert(wrapper.find(Loading).length === 1); - } - ); - }) - .then(() => { - const loadingIndicator =
Loading...
; - const CustomLoading = ({ className }) => ; - - return timeout( - { - loading: true, - loadingComponent: CustomLoading, - }, - () => { - wrapper.debug(); - assert(wrapper.find(CustomLoading).length === 1); - assert(wrapper.find('div.test-custom-loading').length === 1); - done(); - } - ); - }); - }); - - it('should support expandedRowRender getExpandedColProps with expandedIndexSimulate', done => { - const arr = []; - timeout( - { - children: [ - , - { - arr.push(index); - }} - width={200} - />, - ], - expandedRowRender: (record, index) => record.name + index, - getRowProps: (record, index) => { - assert(record.id == index + 1); - return { className: `next-myclass-${index}` }; - }, - getExpandedColProps: (record, index) => { - assert(record.id == index + 1); - }, - expandedIndexSimulate: true, - }, - () => { - assert(arr.toString() === '0,1'); - - let expandedCtrl0 = wrapper.find('.next-table-body .next-table-expanded-ctrl').at(0); - expandedCtrl0.simulate('click'); - let expandedRow0 = wrapper.find('.next-table-body .next-table-expanded-row').at(0); - - assert(expandedRow0.text().replace(/\s$|^\s/g, '') === 'test' + '0'); - - let expandedCtrl1 = wrapper.find('.next-table-body .next-table-expanded-ctrl').at(1); - expandedCtrl1.simulate('click'); - let expandedRow1 = wrapper.find('.next-table-body .next-table-expanded-row').at(1); - - assert(expandedRow1.text().replace(/\s$|^\s/g, '') === 'test2' + '1'); - } - ).then(() => { - return timeout( - { - expandedRowIndent: [2, 1], - }, - () => { - let expandedRowTdFirst = wrapper.find('.next-table-body .next-table-expanded-row td').first(), - expandedRowTdSecond = wrapper.find('.next-table-body .next-table-expanded-row td').at(1); - assert(expandedRowTdFirst.text().replace(/\s$|^\s/g, '') === ''); - assert(expandedRowTdSecond.text().replace(/\s$|^\s/g, '') === ''); - done(); - } - ); - }); - }); - - it('should support expandedRowEvents', done => { - timeout( - { - expandedRowRender: record => record.name, - onRowOpen: rowKeys => { - assert(rowKeys[0] === '1'); - done(); - }, - }, - () => { - let expandedRow = wrapper.find('.next-table-body .next-table-expanded-row').first(); - assert(expandedRow.length === 0); - let expandedCtrl = wrapper.find('.next-table-body .next-table-expanded-ctrl').first(); - expandedCtrl.simulate('click'); - } - ); - }); - - it('should support rowExpandable', done => { - timeout( - { - dataSource: [ - { id: '1', name: 'test', expandable: false }, - { id: '2', name: 'test2', expandable: true }, - { id: '3', name: 'test3', expandable: true }, - ], - expandedRowRender: record => record.name, - rowExpandable: record => record.expandable, - }, - () => { - let expandedTotal = wrapper.find('.next-table-row'); - let expandedIcon = wrapper.find('.next-table-prerow .next-table-cell-wrapper .next-icon'); - - assert(expandedTotal.length - expandedIcon.length === 1); - done(); - } - ); - }); - - it('should support multiple header', done => { - timeout( - { - children: [ - - - - , - - - - , - ], - }, - () => { - let header = wrapper.find('.next-table-header tr'); - assert(header.length === 2); - assert( - header - .first() - .text() - .replace(/\s/g, '') === 'group1group2' - ); - done(); - } - ); - }); - - it('should support filter', () => { - let id; - const onFilter = (...args) => { - console.log('on filter', args); - id = args[0].id.selectedKeys[0]; - }, - filters = [ - { - label: 'Nano 包含1', - value: 1, - }, - { - label: 'Nano 包含3', - value: 3, - }, - { - label: 'Nano 包含2', - value: 2, - children: [ - { - label: 'Nano 包含12', - value: 22, - }, - { - label: 'Nano 包含23', - value: 23, - }, - ], - }, - { - label: '其他', - children: [ - { - label: 'Nano 包含4', - value: 4, - }, - { - label: 'Nano 包含5', - value: 5, - }, - ], - }, - ]; - wrapper.setProps({ - onFilter, - children: [], - }); - - assert(wrapper.find('next-table-filter-active').length === 0); - - wrapper.find('.next-icon-filter').simulate('click'); - wrapper - .find('.next-btn') - .at(0) - .simulate('click'); - - assert.deepEqual(id, undefined); - wrapper.find('.next-icon-filter').simulate('click'); - wrapper - .find('.next-menu-item') - .at(1) - .simulate('click'); - wrapper - .find('.next-btn') - .at(0) - .simulate('click'); - assert.deepEqual(id, '3'); - - assert(wrapper.find('next-table-filter-active')); - wrapper.setProps({ - filterParams: { - id: { - selectedKeys: '1', - }, - }, - }); - wrapper.find('.next-icon-filter').simulate('click'); - wrapper - .find('.next-btn') - .at(0) - .simulate('click'); - assert.deepEqual(id, '1'); - wrapper.find('.next-icon-filter').simulate('click'); - assert.deepEqual( - wrapper - .find('.next-menu-item') - .at(0) - .props() - .className.indexOf('next-selected') > -1, - true - ); - }); - - it('should support lock', () => { - wrapper.setProps({ - children: [ - , - , - ], - }); - wrapper.debug(); - assert(wrapper.find('div.next-table-lock-left').length === 1); - assert(wrapper.find('div.next-table-lock-right').length === 1); - assert(wrapper.find('div.next-table-empty').length === 0); - //Fix #21 - wrapper.setProps({ - dataSource: [], - }); - assert(wrapper.find('div.next-table-empty').length !== 0); - }); - - it('should support async virtual', () => { - wrapper.setProps({ - dataSource: [], - useVirtual: true, - children: [ - , - , - ], - }); - assert(wrapper.find('div.next-table-empty').length !== 0); - - const dataSource = new Array(40).fill(i => { - return { - id: i + '', - name: `test${i}`, - }; - }); - wrapper.setProps({ - useVirtual: true, - dataSource, - }); - - assert(wrapper.find('div.next-table-empty').length === 0); - assert(wrapper.find('tr.next-table-row').length < 40); - }); - - it('should support virtual + list table', () => { - timeout({ - children: [ - header} />, - , - footer} />, - ], - useVirtual: true, - dataSource: [ - { - id: '1', - name: 'test', - children: [ - { - id: '12', - name: '12test', - }, - ], - }, - { - id: '2', - name: 'test2', - }, - ], - }).then(() => { - assert(wrapper.find('tr.next-table-group-header').length === 2); - assert(wrapper.find('tr.next-table-group-footer').length === 2); - done(); - }); - }); - - it('should support lock row mouseEnter mouseLeave', done => { - wrapper.setProps({ - children: [ - , - , - ], - }); - const onRowClick = sinon.spy(); - const onRowMouseEnter = sinon.spy(); - const onRowMouseLeave = sinon.spy(); - timeout( - { - onRowClick, - onRowMouseEnter, - onRowMouseLeave, - }, - () => { - const row = wrapper.find('.next-table-body .next-table-row').first(); - row.simulate('click'); - assert(onRowClick.called); - row.simulate('mouseenter'); - assert(onRowMouseEnter.called); - row.simulate('mouseleave'); - assert(onRowMouseLeave.called); - done(); - } - ); - }); - - it('should support treeMode', done => { - timeout( - { - dataSource: [ - { - id: '1', - name: 'test', - children: [ - { - id: '12', - name: '12test', - }, - ], - }, - { - id: '2', - name: 'test2', - }, - ], - isTree: true, - }, - () => { - assert(wrapper.find('.next-table-row.hidden').length === 1); - let treeNode = wrapper.find('.next-table-row .next-icon-arrow-right.next-table-tree-arrow'); - treeNode.simulate('click'); - assert(wrapper.find('.next-table-row.hidden').length === 0); - assert( - wrapper - .find('.next-table-row') - .at(1) - .find('.next-table-cell-wrapper') - .first() - .props().style.paddingLeft === 24 - ); - done(); - } - ); - }); - - it('header should support colspan', done => { - wrapper.setProps({}); - - timeout( - { - children: [, ], - }, - () => { - assert(wrapper.find('.next-table-header th').length === 2); - } - ).then(() => { - timeout( - { - children: [ - , - , - ], - }, - () => { - assert(wrapper.find('.next-table-header th').length === 1); - done(); - } - ); - }); - }); - - it('should support colspan & rowspan', done => { - wrapper.setProps({}); - timeout( - { - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }, { id: '3', name: 'test3' }], - cellProps: (rowIndex, colIndex) => { - if (rowIndex == 0 && colIndex == 1) { - return { - rowSpan: 3, - }; - } - }, - }, - () => { - assert( - /test/.test( - wrapper - .find('.next-table-row') - .at(0) - .find('td') - .at(1) - .text() - ) - ); - assert( - wrapper - .find('.next-table-row') - .at(1) - .find('td').length === 1 - ); - assert( - wrapper - .find('.next-table-row') - .at(2) - .find('td').length === 1 - ); - } - ).then(() => { - timeout( - { - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }, { id: '3', name: 'test3' }], - cellProps: (rowIndex, colIndex) => { - if (rowIndex == 0 && colIndex == 0) { - return { - colSpan: 2, - }; - } - }, - }, - () => { - done(); - assert( - /1/.test( - wrapper - .find('.next-table-row') - .at(0) - .find('td') - .at(0) - .text() - ) - ); - } - ); - }); - }); - - it('should support colspan & rowspan', done => { - wrapper.setProps({}); - timeout( - { - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }, { id: '3', name: 'test3' }], - getCellProps: (rowIndex, colIndex) => { - if (rowIndex == 0 && colIndex == 1) { - return { - rowSpan: 3, - }; - } - }, - }, - () => { - assert( - /test/.test( - wrapper - .find('.next-table-row') - .at(0) - .find('td') - .at(1) - .text() - ) - ); - assert( - wrapper - .find('.next-table-row') - .at(1) - .find('td').length === 1 - ); - assert( - wrapper - .find('.next-table-row') - .at(2) - .find('td').length === 1 - ); - } - ).then(() => { - timeout( - { - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }, { id: '3', name: 'test3' }], - getCellProps: (rowIndex, colIndex) => { - if (rowIndex == 0 && colIndex == 0) { - return { - colSpan: 2, - }; - } - }, - }, - () => { - done(); - assert( - /1/.test( - wrapper - .find('.next-table-row') - .at(0) - .find('td') - .at(0) - .text() - ) - ); - } - ); - }); - }); - - it('should support getRowProps', done => { - timeout( - { - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }, { id: '3', name: 'test3' }], - getRowProps: (record, index) => { - if (index == 0) { - return { - 'data-props': 'rowprops', - }; - } - }, - }, - () => { - assert( - wrapper - .find('.next-table-row') - .at(0) - .prop('data-props') === 'rowprops' - ); - done(); - } - ); - }); - - it('should support rowProps', done => { - timeout( - { - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }, { id: '3', name: 'test3' }], - rowProps: (record, index) => { - if (index == 0) { - return { - 'data-props': 'rowprops', - }; - } - }, - }, - () => { - assert( - wrapper - .find('.next-table-row') - .at(0) - .prop('data-props') === 'rowprops' - ); - done(); - } - ); - }); - - it('should support list Header', done => { - timeout({ - children: [ - header} />, - , - footer} />, - ], - dataSource: [ - { - id: '1', - name: 'test', - children: [ - { - id: '12', - name: '12test', - }, - ], - }, - { - id: '2', - name: 'test2', - }, - ], - }).then(() => { - assert(wrapper.find('tr.next-table-group-header').length === 2); - assert(wrapper.find('tr.next-table-group-footer').length === 2); - done(); - }); - }); - - it('should render null when ColGroup, GroupHeader, Col', () => { - const colgroup = mount(); - const col = mount(); - const groupHeader = mount(); - }); - - it('should support stickyHeader', done => { - timeout( - { - stickyHeader: true, - }, - () => { - assert(wrapper.find('div.next-table-affix').length === 1); - done(); - } - ); - }); - - it('should support resize', done => { - timeout( - { - children: [ - , - , - ], - onResizeChange: (dataIndex, value) => { - console.log(dataIndex, value); - }, - }, - () => { - wrapper.find('.next-table-resize-handler').simulate('mousedown', { pageX: 0 }); - assert(document.body.style.cursor === 'ew-resize'); - document.dispatchEvent(new Event('mousemove', { pageX: 0 })); - document.dispatchEvent(new Event('mouseup')); - - setTimeout(() => { - assert(document.body.style.cursor === ''); - done(); - }, 100); - } - ); - }); - - it('should support rtl', done => { - timeout( - { - children: [ - , - , - ], - rtl: true, - }, - () => { - assert(wrapper.find('.next-table[dir="rtl"]').length === 3); - done(); - } - ); - }); - - it('should support rtl resize', done => { - timeout( - { - children: [ - , - , - ], - rtl: true, - onResizeChange: (dataIndex, value) => { - console.log(dataIndex, value); - }, - }, - () => { - wrapper.find('.next-table-resize-handler').simulate('mousedown', { pageX: 0 }); - assert(document.body.style.cursor === 'ew-resize'); - document.dispatchEvent(new Event('mousemove', { pageX: 0 })); - document.dispatchEvent(new Event('mouseup')); - - setTimeout(() => { - assert(document.body.style.cursor === ''); - done(); - }, 100); - } - ); - }); - - it('should support dataSource [] => [{},{}] => []', () => { - wrapper.setProps({ - children: [ - , - , - ], - }); - wrapper.debug(); - assert(wrapper.find('div.next-table-lock-left').length === 1); - assert(wrapper.find('div.next-table-lock-right').length === 1); - assert(wrapper.find('div.next-table-empty').length === 0); - - wrapper.setProps({ - dataSource: [], - }); - assert(wrapper.find('div.next-table-empty').length !== 0); - - wrapper.setProps({ - dataSource: [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }], - }); - - assert(wrapper.find('div.next-table-lock-left').length === 1); - assert(wrapper.find('div.next-table-lock-right').length === 1); - assert(wrapper.find('div.next-table-empty').length === 0); - }); - - it('should support lock scroll x', () => { - const ds = new Array(30).fill(1).map((val, i) => { - return { id: i, name: 'test' + i }; - }); - wrapper.setProps({ - children: [ - , - , - , - ], - fixedHeader: true, - dataSource: ds, - }); - - assert(wrapper.find('div.next-table-lock-left').length === 1); - assert(wrapper.find('div.next-table-lock-right').length === 1); - - wrapper - .find('div.next-table-lock .next-table-inner .next-table-body') - .at(1) - .props() - .onLockScroll({ - target: { - scrollLeft: 30, - scrollTop: 20, - }, - }); - - wrapper - .find('div.next-table-lock-right .next-table-body') - .at(1) - .props() - .onLockScroll({ - target: { - scrollLeft: 30, - scrollTop: 20, - }, - }); - }); - - it('should support StickyLock', done => { - const ds = new Array(30).fill(1).map((val, i) => { - return { id: i, name: 'test' + i }; - }); - stickyLockWrapper.setProps({ - children: [ - , - , - , - ], - dataSource: ds, - }); - - wrapper.setProps({ - children: [ - , - , - , - , - ], - dataSource: ds, - tableWidth: 1000, - }); - - assert(stickyLockWrapper.find('div.next-table-lock-left').length === 0); - assert(stickyLockWrapper.find('div.next-table-lock-right').length === 0); - assert( - stickyLockWrapper - .find('div.next-table-lock tr td.next-table-fix-left.next-table-fix-left-last') - .at(0) - .instance().style.position === 'sticky' - ); - wrapper.update(); - stickyLockWrapper.update(); - - setTimeout(() => { - // assert(wrapper.find('div.next-table-lock.next-table-scrolling-to-right').length === 1); - // assert(stickyLockWrapper.find('div.next-table-lock.next-table-scrolling-to-right').length === 1); - assert( - stickyLockWrapper.find('div.next-table-lock tr td.next-table-fix-left.next-table-fix-left-last') - .length === ds.length - ); - done(); - }, 500); - }); - - it('should support align alignHeader', () => { - wrapper.setProps({ - children: [ - , - , - , - ], - }); - - assert( - wrapper - .find('thead tr th') - .at(0) - .props().style.textAlign === 'left' - ); - assert( - wrapper - .find('thead tr th') - .at(1) - .props().style.textAlign === 'left' - ); - assert( - wrapper - .find('thead tr th') - .at(2) - .props().style.textAlign === 'right' - ); - - assert( - wrapper - .find('tbody tr') - .at(0) - .find('td') - .at(0) - .props().style.textAlign === 'right' - ); - assert( - wrapper - .find('tbody tr') - .at(0) - .find('td') - .at(1) - .props().style.textAlign === 'left' - ); - assert( - wrapper - .find('tbody tr') - .at(0) - .find('td') - .at(2) - .props().style.textAlign === undefined - ); - }); - - it('should support align alignHeader rtl', () => { - wrapper.setProps({ - children: [ - , - , - , - ], - rtl: true, - }); - - assert( - wrapper - .find('thead tr th') - .at(0) - .props().style.textAlign === 'right' - ); - assert( - wrapper - .find('thead tr th') - .at(1) - .props().style.textAlign === 'right' - ); - assert( - wrapper - .find('thead tr th') - .at(2) - .props().style.textAlign === 'left' - ); - - assert( - wrapper - .find('tbody tr') - .at(0) - .find('td') - .at(0) - .props().style.textAlign === 'left' - ); - assert( - wrapper - .find('tbody tr') - .at(0) - .find('td') - .at(1) - .props().style.textAlign === 'right' - ); - assert( - wrapper - .find('tbody tr') - .at(0) - .find('td') - .at(2) - .props().style.textAlign === undefined - ); - }); -}); diff --git a/components/table/__tests__/index-spec.tsx b/components/table/__tests__/index-spec.tsx new file mode 100644 index 0000000000..0021122619 --- /dev/null +++ b/components/table/__tests__/index-spec.tsx @@ -0,0 +1,884 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Loading from '../../loading'; +import Icon from '../../icon'; +import Table, { type TableProps } from '../index'; +import '../style'; + +const dataSource = [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, +]; + +const table = ( + + + +
+); + +const stickyLock = ( + + + + +); + +describe('Table', () => { + it('should mount table', () => { + cy.mount(table); + cy.get('.next-table-body .next-table-row').should('have.length', 2); + }); + + it('should render checkboxMode', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rowSelection: { + getProps: record => { + if (record.id === '1') { + return { + disabled: true, + }; + } + }, + }, + }); + cy.get('.next-checkbox').should('have.length', 3); + cy.get('.next-checkbox-wrapper.disabled').should('have.length', 1); + }); + + it('should support rowSelect control', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rowSelection: { + selectedRowKeys: ['1'], + }, + }); + cy.get('.next-checkbox-wrapper.checked').should('have.length', 1); + }); + + it('should render when dataSource is [] & children is null', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [], + children: [], + }); + cy.get('.next-table-empty').should('exist'); + cy.rerender('Demo', { + dataSource: [], + children: null, + }); + cy.get('.next-table-empty').should('exist'); + }); + + it('should render when dataSource is made of string', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + // @ts-expect-error 参考 types 里 RecordItem 的描述 + dataSource: ['string1', 'string2'], + children: [ record} />], + }); + cy.get('.next-table-body .next-table-row').should('have.length', 2); + }); + + it('should render RadioMode', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rowSelection: { + mode: 'single', + }, + }); + cy.get('.next-radio').should('have.length', 2); + }); + + it('should support columnProps/titleProps/titleAddons of rowSelection', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rowSelection: { + columnProps: () => { + return { + lock: 'right', + width: 90, + align: 'center', + }; + }, + titleAddons: () => { + return
请选择
; + }, + titleProps: () => { + return { + disabled: true, + children: '>', + }; + }, + }, + }); + cy.get('#table-titleAddons').eq(0).should('have.text', '请选择'); + cy.get('colgroup col').eq(2).should('have.css', 'width', '90px'); + cy.get('th .next-checkbox-wrapper').eq(0).should('have.class', 'disabled'); + cy.get('th .next-checkbox-wrapper .next-checkbox-label').eq(0).should('have.text', '>'); + }); + + it('should support events', () => { + const onRowClick = cy.spy(); + const onRowMouseEnter = cy.spy(); + const onRowMouseLeave = cy.spy(); + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + onRowClick, + onRowMouseEnter, + onRowMouseLeave, + }); + cy.get('.next-table-body .next-table-row').first().trigger('click'); + cy.wrap(onRowClick).should('be.called'); + // React 的 mouseEnter 事件是通过监听 mouseover 实现的 + cy.get('.next-table-body .next-table-row').first().trigger('mouseover'); + cy.wrap(onRowMouseEnter).should('be.called'); + // React 的 mouseLeave 事件是通过监听 mouseout 实现的 + cy.get('.next-table-body .next-table-row').first().trigger('mouseout'); + cy.wrap(onRowMouseLeave).should('be.called'); + }); + + it('should support sort', () => { + const onSort = cy.spy(); + + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + onSort, + }); + cy.get('.next-table-header .next-table-sort').first().click(); + cy.wrap(onSort).should('be.calledWith', 'id', 'desc'); + }); + + it('should support tableLayout&tableWidth', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + tableLayout: 'fixed', + tableWidth: 1200, + }); + cy.get('.next-table').should('have.class', 'next-table-layout-fixed'); + cy.get('.next-table table').should('have.css', 'width', '1200px'); + }); + it('should support sortIcons', () => { + const onSort = cy.spy(); + + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + onSort, + sortIcons: { + desc: , + asc: , + }, + }); + cy.get('.next-table-header .next-table-sort .next-icon-arrow-down').should('exist'); + cy.get('.next-table-header .next-table-sort .next-icon-arrow-up').should('exist'); + cy.get('.next-table-header .next-table-sort').first().click(); + cy.wrap(onSort).should('be.calledWith', 'id', 'desc'); + }); + + it('should support getRowProps for setting className', () => { + const getRowProps: TableProps['getRowProps'] = record => { + if (record.id === '1') { + return { className: 'rowClassName' }; + } + }; + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + getRowProps, + }); + cy.get('.next-table-body .next-table-row').first().should('have.class', 'rowClassName'); + }); + + it('should support rowProps for setting className', () => { + const rowProps: TableProps['rowProps'] = record => { + if (record.id === '1') { + return { className: 'rowClassName' }; + } + }; + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rowProps, + }); + cy.get('.next-table-body .next-table-row').first().should('have.class', 'rowClassName'); + }); + + it('should support fixedHeader, isZebra, hasBorder, loading', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + fixedHeader: true, + }); + cy.get('.next-table-fixed').should('exist'); + cy.rerender('Demo', { + isZebra: true, + }); + cy.get('.zebra').should('exist'); + cy.rerender('Demo', { + hasBorder: false, + }); + cy.get('.only-bottom-border').should('exist'); + cy.rerender('Demo', { + loading: true, + }); + cy.get('.next-loading').should('exist'); + const loadingIndicator =
Loading...
; + const CustomLoading: TableProps['loadingComponent'] = ({ className }) => ( + + ); + CustomLoading.propTypes = { + className: PropTypes.string, + }; + cy.rerender('Demo', { + loading: true, + loadingComponent: CustomLoading, + }); + cy.get('.test-custom-loading').should('exist'); + }); + + it('should support expandedRowRender getExpandedColProps with expandedIndexSimulate', () => { + cy.mount(table).as('Demo'); + const cellHandler = cy.spy(); + cy.rerender('Demo', { + children: [ + , + { + cellHandler(index); + }} + width={200} + />, + ], + expandedRowRender: (record: { name: string }, index) => record.name + index, + getRowProps: (record: { id: string }, index) => { + expect(Number(record.id)).equal(index + 1); + return { className: `next-myclass-${index}` }; + }, + getExpandedColProps: (record: { id: string }, index) => { + expect(Number(record.id)).equal(index + 1); + }, + expandedIndexSimulate: true, + expandedRowIndent: [1, 1], + }); + cy.wrap(cellHandler).should('be.calledTwice'); + cy.get('.next-table-body .next-table-expanded-ctrl').eq(0).click(); + cy.get('.next-table-body .next-table-expanded-row') + .eq(0) + .invoke('text') + .then(text => { + expect(text.trim()).equal('test0'); + }); + cy.get('.next-table-body .next-table-expanded-ctrl').eq(1).click(); + cy.get('.next-table-body .next-table-expanded-row') + .eq(1) + .invoke('text') + .then(text => { + expect(text.trim()).equal('test21'); + }); + cy.get('.next-table-body .next-table-expanded-row td') + .eq(0) + .invoke('text') + .then(text => { + expect(text.trim()).equal(''); + }); + cy.get('.next-table-body .next-table-expanded-row') + .eq(0) + .find('td') + .last() + .invoke('text') + .then(text => { + expect(text.trim()).equal(''); + }); + }); + + it('should support expandedRowEvents', () => { + const onRowOpen = cy.spy(); + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + expandedRowRender: (record: { name: string }, index) => record.name + index, + onRowOpen: rowKeys => { + onRowOpen(rowKeys[0]); + }, + }); + cy.get('.next-table-body .next-table-expanded-row').should('not.exist'); + cy.get('.next-table-body .next-table-expanded-ctrl').first().click(); + cy.wrap(onRowOpen).should('be.calledWith', '1'); + }); + + it('should support rowExpandable', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [ + { id: '1', name: 'test', expandable: false }, + { id: '2', name: 'test2', expandable: true }, + { id: '3', name: 'test3', expandable: true }, + ], + expandedRowRender: (record: { name: string }) => record.name, + rowExpandable: (record: { expandable: boolean }) => record.expandable, + }); + cy.get('.next-table-row').should('have.length', 3); + cy.get('.next-table-prerow .next-table-cell-wrapper .next-icon').should('have.length', 2); + }); + + it('should support multiple header', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + + + + , + + + + , + ], + }); + cy.get('.next-table-header tr').should('have.length', 2); + cy.get('.next-table-header tr') + .first() + .invoke('text') + .then(text => { + expect(text.trim()).equal('group1group2'); + }); + }); + + it('should support filter', () => { + const onFilterSpy = cy.spy(); + const onFilter: TableProps['onFilter'] = (...args) => { + console.log('on filter', args); + onFilterSpy(args[0].id.selectedKeys[0]); + }, + filters = [ + { + label: 'Nano 包含 1', + value: 1, + }, + { + label: 'Nano 包含 3', + value: 3, + }, + { + label: 'Nano 包含 2', + value: 2, + children: [ + { + label: 'Nano 包含 12', + value: 22, + }, + { + label: 'Nano 包含 23', + value: 23, + }, + ], + }, + { + label: '其他', + children: [ + { + label: 'Nano 包含 4', + value: 4, + }, + { + label: 'Nano 包含 5', + value: 5, + }, + ], + }, + ]; + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + onFilter, + children: [], + }).as('Demo2'); + + cy.get('next-table-filter-active').should('not.exist'); + + cy.get('.next-icon-filter').click(); + cy.get('.next-btn').first().click(); + cy.wrap(onFilterSpy).should('be.calledWith', undefined); + cy.get('.next-icon-filter').click(); + cy.get('.next-menu-item').eq(1).click(); + cy.get('.next-btn').first().click(); + cy.wrap(onFilterSpy).should('be.calledWith', '3'); + cy.get('.next-table-filter-active').should('exist'); + cy.rerender('Demo2', { + filterParams: { + id: { + selectedKeys: '1', + }, + }, + }); + cy.get('.next-icon-filter').click(); + cy.get('.next-btn').eq(0).click(); + cy.wrap(onFilterSpy).should('be.calledWith', '1'); + cy.get('.next-icon-filter').click(); + cy.get('.next-menu-item').eq(0).should('have.class', 'next-selected'); + }); + + it('should support lock', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + }).as('Demo2'); + cy.get('.next-table-lock-left').should('exist'); + cy.get('.next-table-lock-right').should('exist'); + cy.get('.next-table-empty').should('not.exist'); + cy.rerender('Demo2', { + dataSource: [], + }); + cy.get('.next-table-empty').should('exist'); + }); + + it('should support async virtual', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [], + useVirtual: true, + children: [ + , + , + ], + }).as('Demo2'); + cy.rerender('Demo2', { + dataSource: new Array(40).fill((i: number) => { + return { + id: `${i}`, + name: `test${i}`, + }; + }), + }); + cy.get('.next-table-empty').should('not.exist'); + cy.get('.next-table-row').its('length').should('be.lt', 40); + }); + + it('should support virtual + list table', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + header} />, + , + footer} />, + ], + useVirtual: true, + dataSource: [ + { + id: '1', + name: 'test', + children: [ + { + id: '12', + name: '12test', + }, + ], + }, + { + id: '2', + name: 'test2', + }, + ], + }); + cy.get('.next-table-group-header').should('have.length', 2); + cy.get('.next-table-group-footer').should('have.length', 2); + }); + + it('should support lock row mouseEnter mouseLeave', () => { + const onRowClick = cy.spy(); + const onRowMouseEnter = cy.spy(); + const onRowMouseLeave = cy.spy(); + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + onRowClick, + onRowMouseEnter, + onRowMouseLeave, + }); + cy.get('.next-table-body .next-table-row').first().click(); + cy.wrap(onRowClick).should('be.called'); + cy.get('.next-table-body .next-table-row').first().trigger('mouseover'); + cy.wrap(onRowMouseEnter).should('be.called'); + cy.get('.next-table-body .next-table-row').first().trigger('mouseout'); + cy.wrap(onRowMouseLeave).should('be.called'); + }); + + it('should support treeMode', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [ + { + id: '1', + name: 'test', + children: [ + { + id: '12', + name: '12test', + }, + ], + }, + { + id: '2', + name: 'test2', + }, + ], + isTree: true, + }); + cy.get('.next-table-row.hidden').should('have.length', 1); + cy.get('.next-table-row .next-icon-arrow-right.next-table-tree-arrow').click(); + cy.get('.next-table-row.hidden').should('have.length', 0); + cy.get('.next-table-row') + .eq(1) + .find('.next-table-cell-wrapper') + .first() + .should('have.css', 'padding-left', '24px'); + }); + + it('header should support colspan', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + }); + cy.get('.next-table-header th').should('have.length', 2); + cy.rerender('Demo', { + children: [ + , + , + ], + }); + cy.get('.next-table-header th').should('have.length', 1); + }); + + it('should support colspan & rowspan', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, + { id: '3', name: 'test3' }, + ], + cellProps: (rowIndex, colIndex) => { + if (rowIndex === 0 && colIndex === 1) { + return { + rowSpan: 3, + }; + } + }, + }); + cy.get('.next-table-row').eq(0).find('td').eq(1).should('have.text', 'test'); + cy.get('.next-table-row').eq(1).find('td').should('have.length', 1); + cy.get('.next-table-row').eq(2).find('td').should('have.length', 1); + cy.rerender('Demo', { + dataSource: [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, + { id: '3', name: 'test3' }, + ], + cellProps: (rowIndex, colIndex) => { + if (rowIndex === 0 && colIndex === 0) { + return { + colSpan: 2, + }; + } + }, + }); + cy.get('.next-table-row').eq(0).find('td').eq(0).should('have.text', '1'); + }); + + it('should support getRowProps', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, + { id: '3', name: 'test3' }, + ], + getRowProps: (record, index) => { + if (index === 0) { + return { + 'data-props': 'rowprops', + }; + } + }, + }); + cy.get('.next-table-row').eq(0).should('have.attr', 'data-props', 'rowprops'); + }); + + it('should support rowProps', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + dataSource: [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, + { id: '3', name: 'test3' }, + ], + rowProps: (record, index) => { + if (index === 0) { + return { + 'data-props': 'rowprops', + }; + } + }, + }); + cy.get('.next-table-row').eq(0).should('have.attr', 'data-props', 'rowprops'); + }); + + it('should support list Header', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + header} />, + , + footer} />, + ], + dataSource: [ + { + id: '1', + name: 'test', + children: [ + { + id: '12', + name: '12test', + }, + ], + }, + { + id: '2', + name: 'test2', + }, + ], + }); + cy.get('.next-table-group-header').should('have.length', 2); + cy.get('.next-table-group-footer').should('have.length', 2); + }); + + it('should render null when ColGroup, GroupHeader, Col', () => { + cy.mount(); + cy.get('div').should('have.length', 1); + cy.mount(); + cy.get('div').should('have.length', 1); + cy.mount(); + cy.get('div').should('have.length', 1); + }); + + it('should support stickyHeader', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + stickyHeader: true, + }); + cy.get('.next-table-affix').should('exist'); + }); + + it('should support resize', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + onResizeChange: (dataIndex, value) => { + console.log(dataIndex, value); + }, + }); + cy.get('.next-table-resize-handler').first().trigger('mousedown', { pageX: 0 }); + cy.get('body').should('have.css', 'cursor', 'ew-resize'); + cy.get('body').trigger('mousemove', { pageX: 0 }); + cy.get('body').trigger('mouseup'); + cy.get('body').should('have.css', 'cursor', 'auto'); + }); + + it('should support rtl', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rtl: true, + children: [ + , + , + ], + }); + cy.get('.next-table[dir="rtl"]').should('have.length', 3); + }); + + it('should support rtl resize', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rtl: true, + children: [ + , + , + ], + onResizeChange: (dataIndex, value) => { + console.log(dataIndex, value); + }, + }); + cy.get('.next-table-resize-handler').first().trigger('mousedown', { pageX: 0 }); + cy.get('body').should('have.css', 'cursor', 'ew-resize'); + cy.get('body').trigger('mousemove', { pageX: 0 }); + cy.get('body').trigger('mouseup'); + cy.get('body').should('have.css', 'cursor', 'auto'); + }); + + it('should support dataSource [] => [{},{}] => []', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + ], + }).as('Demo2'); + cy.get('.next-table-lock-left').should('have.length', 1); + cy.get('.next-table-lock-right').should('have.length', 1); + cy.get('.next-table-empty').should('have.length', 0); + cy.rerender('Demo2', { + dataSource: [], + }).as('Demo3'); + cy.get('.next-table-empty').should('have.length', 1); + cy.rerender('Demo3', { + dataSource: [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, + ], + }).as('Demo4'); + cy.get('.next-table-lock-left').should('have.length', 1); + cy.get('.next-table-lock-right').should('have.length', 1); + cy.get('.next-table-empty').should('have.length', 0); + }); + + it('should support lock scroll x', () => { + const ds = new Array(30).fill(1).map((val, i) => { + return { id: i, name: `test${i}` }; + }); + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + , + ], + dataSource: ds, + fixedHeader: true, + }); + cy.get('.next-table-lock-left').should('have.length', 1); + cy.get('.next-table-lock-right').should('have.length', 1); + cy.get('div.next-table-lock-left .next-table-body').then($ele => { + $ele.scrollTop(100); + }); + cy.get('.next-table-body').eq(0).should('have.prop', 'scrollTop', 100); + cy.get('div.next-table-lock-right .next-table-body').should('have.prop', 'scrollTop', 100); + cy.get('div.next-table-lock-right .next-table-body').then($ele => { + $ele.scrollTop(200); + }); + cy.get('.next-table-body').eq(0).should('have.prop', 'scrollTop', 200); + cy.get('div.next-table-lock-left .next-table-body').should('have.prop', 'scrollTop', 200); + cy.get('.next-table-body') + .eq(0) + .then($ele => { + $ele.scrollTop(300); + }); + cy.get('div.next-table-lock-left .next-table-body').should('have.prop', 'scrollTop', 300); + cy.get('div.next-table-lock-right .next-table-body').should('have.prop', 'scrollTop', 300); + }); + + it('should support StickyLock', () => { + const ds = new Array(30).fill(1).map((val, i) => { + return { id: i, name: `test${i}` }; + }); + cy.mount(stickyLock).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + , + ], + dataSource: ds, + }); + cy.get('.next-table-lock-left').should('not.exist'); + cy.get('.next-table-lock-right').should('not.exist'); + cy.get('div.next-table-lock tr td.next-table-fix-left.next-table-fix-left-last') + .eq(0) + .should('have.css', 'position', 'sticky'); + cy.get('div.next-table-lock tr td.next-table-fix-left.next-table-fix-left-last').should( + 'have.length', + ds.length + ); + }); + + it('should support align alignHeader', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + children: [ + , + , + , + ], + }); + cy.get('thead tr th').eq(0).should('have.css', 'text-align', 'left'); + cy.get('thead tr th').eq(1).should('have.css', 'text-align', 'left'); + cy.get('thead tr th').eq(2).should('have.css', 'text-align', 'right'); + cy.get('tbody tr').eq(0).find('td').eq(0).should('have.css', 'text-align', 'right'); + cy.get('tbody tr').eq(0).find('td').eq(1).should('have.css', 'text-align', 'left'); + cy.get('tbody tr').eq(0).find('td').eq(2).should('have.css', 'text-align', 'start'); + }); + + it('should support align alignHeader rtl', () => { + cy.mount(table).as('Demo'); + cy.rerender('Demo', { + rtl: true, + children: [ + , + , + , + ], + }); + cy.get('thead tr th').eq(0).should('have.css', 'text-align', 'right'); + cy.get('thead tr th').eq(1).should('have.css', 'text-align', 'right'); + cy.get('thead tr th').eq(2).should('have.css', 'text-align', 'left'); + cy.get('tbody tr').eq(0).find('td').eq(0).should('have.css', 'text-align', 'left'); + cy.get('tbody tr').eq(0).find('td').eq(1).should('have.css', 'text-align', 'right'); + cy.get('tbody tr').eq(0).find('td').eq(2).should('have.css', 'text-align', 'start'); + }); +}); diff --git a/components/table/__tests__/issue-spec.js b/components/table/__tests__/issue-spec.tsx similarity index 53% rename from components/table/__tests__/issue-spec.js rename to components/table/__tests__/issue-spec.tsx index 158ceea05d..c37fdeb217 100644 --- a/components/table/__tests__/issue-spec.js +++ b/components/table/__tests__/issue-spec.tsx @@ -1,19 +1,11 @@ import React, { useState } from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import Promise from 'promise-polyfill'; -import Table from '../index'; +import Table, { type ColumnProps, type TableProps } from '../index'; import Button from '../../button/index'; import ConfigProvider from '../../config-provider'; import Input from '../../input'; import '../style'; -/* eslint-disable */ -Enzyme.configure({ adapter: new Adapter() }); -const delay = duration => new Promise(resolve => setTimeout(resolve, duration)); -const generateDataSource = j => { + +const generateDataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -27,86 +19,52 @@ const generateDataSource = j => { } return result; }; +const dataSource = [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, +]; + +const table = ( + + + +
+); describe('Issue', () => { - let dataSource = [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }], - table, - timeout, - wrapper; - beforeEach(() => { - table = ( - - - -
- ); - - wrapper = mount(table); - timeout = (props, callback) => { - return new Promise(resolve => { - wrapper.setProps(props); - setTimeout(function() { - resolve(); - }, 10); - }).then(callback); - }; + cy.mount(table).as('Demo'); }); - afterEach(() => { - table = null; - }); - - it('should support not display empty when table is Loading', done => { - wrapper.setProps({}); - timeout( - { - loading: true, - dataSource: [], - }, - () => { - assert(wrapper.find('.next-table-empty').text() === ' '); - } - ).then(() => { - timeout( - { - loading: false, - }, - () => { - assert(wrapper.find('.next-table-empty').text() === '没有数据'); - done(); - } - ); + it('should support not display empty when table is Loading', () => { + cy.rerender('Demo', { + loading: true, + dataSource: [], + }).as('Demo2'); + cy.get('.next-table-empty').should('have.text', ' '); + cy.rerender('Demo2', { + loading: false, }); + cy.get('.next-table-empty').should('have.text', '没有数据'); }); - it('should support rowSelection without children and columns', done => { - const container = document.createElement('div'); - document.body.appendChild(container); - class App extends React.Component { - render() { - return ( - {} }} - expandedRowRender={record => record.title} - /> - ); - } - } - - ReactDOM.render(, container, function() { - setTimeout(() => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); - }); + it('should support rowSelection without children and columns', () => { + const onRowSelect = cy.spy(); + cy.mount( +
{ + onRowSelect(selectedRowKeys); + }, + }} + /> + ); + cy.get('.next-table-row .next-checkbox-input').eq(0).click(); + cy.wrap(onRowSelect).should('be.calledWith', ['1']); }); - it('should support columns with lock', done => { - const container = document.createElement('div'); - document.body.appendChild(container); + it('should support columns with lock', () => { const columns = [ { title: 'Title6', @@ -135,7 +93,12 @@ describe('Issue', () => { render() { return (
-
+
{ ); } } - - ReactDOM.render(, container, function() { - assert( - container.querySelectorAll('#normal-table .next-table-lock-left .next-table-body tbody tr').length === 2 - ); - assert(container.querySelectorAll('#sticky-table .next-table-fix-left')[0].style.position === 'sticky'); - setTimeout(() => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); - }); + cy.mount(); + cy.get('#normal-table .next-table-lock-left .next-table-body tbody tr').should( + 'have.length', + 2 + ); + cy.get('#sticky-table .next-table-fix-left').should('have.css', 'position', 'sticky'); }); - it('should fix onChange reRender bug', done => { - const container = document.createElement('div'); - document.body.appendChild(container); + it('should fix onChange reRender bug', () => { + // const container = document.createElement('div'); + // document.body.appendChild(container); class App extends React.Component { state = { selected: [], }; - onSelectionChange = (ids, records) => { + onSelectionChange: NonNullable['onChange'] = ( + ids, + records + ) => { this.setState({ selected: records, }); }; render() { return ( -
+
@@ -182,195 +145,129 @@ describe('Issue', () => { } } - ReactDOM.render(, container, function() { - const input = container.querySelector('.next-table-header .next-checkbox input'); - input.click(); - setTimeout(() => { - assert(container.querySelectorAll('.next-table-body .next-checkbox-wrapper.checked').length === 2); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); - }); + cy.mount(); + cy.get('.next-table-header .next-checkbox input').eq(0).click(); + cy.get('.next-table-body .next-checkbox-wrapper.checked').should('have.length', 2); }); it('should support null child', () => { - wrapper.setProps({ - children: [null, ], + cy.rerender('Demo', { + children: [null, ], }); - assert(wrapper.find('.next-table-body tr').length === 2); + cy.get('.next-table-body tr').should('have.length', 2); }); - it('should support rowSelection & tree', done => { - timeout( - { - isTree: true, - rowSelection: { - onChange: () => {}, - }, - dataSource: [ - { - id: '1', - name: 'test', - children: [ - { - id: '12', - name: '12test', - }, - ], - }, - { - id: '2', - name: 'test2', - }, - ], + it('should support rowSelection & tree', () => { + cy.rerender('Demo', { + isTree: true, + rowSelection: { + onChange: () => {}, }, - () => { - assert(wrapper.find('.next-table-row').find('.hidden').length === 1); - done(); - } - ); + dataSource: [ + { + id: '1', + name: 'test', + children: [ + { + id: '12', + name: '12test', + }, + ], + }, + { + id: '2', + name: 'test2', + }, + ], + }); + cy.get('.next-table-row.hidden').should('have.length', 1); }); it('should support rowSelection click', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - const rowSelection = { - onChange: () => {}, - }; - - ReactDOM.render( - - -
, - div - ); - - div.querySelectorAll('.next-checkbox-wrapper')[1].click(); - assert(div.querySelectorAll('.next-checkbox-wrapper.checked').length === 1); - div.querySelectorAll('.next-checkbox-wrapper')[0].click(); - assert(div.querySelectorAll('.next-checkbox-wrapper.checked').length === 3); - - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.rerender('Demo', { + rowSelection: { + onChange: () => {}, + }, + children: , + }); + cy.get('.next-checkbox-wrapper').eq(1).click(); + cy.get('.next-checkbox-wrapper.checked').should('have.length', 1); + cy.get('.next-checkbox-wrapper').eq(0).click(); + cy.get('.next-checkbox-wrapper.checked').should('have.length', 3); }); it('should ignore lock as `true` string', () => { - wrapper.setProps({ + cy.rerender('Demo', { rowSelection: { onChange: () => {}, }, children: , }); + cy.get('.next-table-lock-left').should('not.exist'); }); - // it('should support optimization', (done) => { - // class App extends React.Component { - // state = { - // extra: 'abc' - // } - // cellRender = (value) => { - // return value + this.state.extra; - // } - // render() { - // return - // - //
- // } - // componentDidMount() { - // setTimeout(() => { - // this.setState({ - // extra: 'bcd' - // }); - // }, 10); - // } - // } - // const wrapper = mount(); - // setTimeout(() => { - // assert(/bcd/.test(wrapper.find('.next-table-body td').at(0).text())); - // done(); - // }, 100); - // }); - - it('should ignore lock when colWidth < tableWidth', done => { - class App extends React.Component { - state = { - cols: [ - , - , - , - ], - }; - cellRender = value => { - return value; + it('should ignore lock when colWidth < tableWidth', () => { + const cellRender = (value: unknown) => { + return value; + }; + class App extends React.Component<{ more: boolean }> { + static defaultProps = { + more: true, }; render() { return (
- {this.state.cols}
+ + + {this.props.more + ? [ + , + , + ] + : null} +
); } - componentDidMount() { - setTimeout(() => { - this.setState({ - cols: , - }); - }, 100); - } } - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - assert(div.querySelectorAll('.next-table-lock-left')[0].children.length !== 0); - assert(div.querySelectorAll('.next-table-lock-right')[0].children.length === 0); - assert(div.querySelectorAll('div.next-table-lock.next-table-scrolling-to-right').length === 1); - - setTimeout(() => { - assert(div.querySelectorAll('.next-table-lock-left')[0].children.length === 0); - assert(div.querySelectorAll('.next-table-lock-right')[0].children.length === 0); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); - done(); - }, 200); + cy.mount().as('Demo'); + cy.get('.next-table-lock-left').children().should('have.length.gt', 0); + cy.get('.next-table-lock-right').children().should('have.length', 0); + cy.rerender('Demo', { more: false }); + cy.get('.next-table-lock-left').children().should('have.length', 0); + cy.get('.next-table-lock-right').children().should('have.length', 0); }); it('should has border when set hasHeader as false', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render( - - -
, - div - ); - //Hack firefox,IE10,IE11 render error; - div.querySelectorAll('.next-table table')[0].style.borderCollapse = 'separate'; - assert(parseInt(window.getComputedStyle(div.querySelectorAll('.next-table')[0]).borderTopWidth, 10) === 1); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.rerender('Demo', { + hasHeader: false, + }); + cy.get('.next-table').should('have.css', 'border-top-width', '1px'); }); it('should support style config for Table.Column', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render( + cy.mount( -
, - div + ); - assert(div.querySelectorAll('.next-table table td')[0].style.textAlign === ''); - assert(div.querySelectorAll('.next-table table th')[0].style.textAlign === 'left'); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.get('.next-table td').eq(0).should('have.css', 'text-align', 'start'); + cy.get('.next-table th').eq(0).should('have.css', 'text-align', 'left'); }); it('should support pass null to sort and any others', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render( + cy.mount( { expandedRowKeys={null} > -
, - div + ); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.get('.next-table').should('exist'); }); it('should support virtual list', () => { @@ -399,15 +294,8 @@ describe('Issue', () => { ); } } - - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - assert(div.querySelectorAll('.next-table-body tbody tr').length < 100); - - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('.next-table-body tbody tr').should('have.length.lt', 100); }); it('should support defaultOpenRowKeys', () => { @@ -427,15 +315,8 @@ describe('Issue', () => { } } - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - let expandedTotal = div.querySelectorAll('tbody tr'); - assert(expandedTotal.length === 5); - - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('tbody tr').should('have.length', 5); }); it('sort should be singleton', () => { @@ -450,18 +331,10 @@ describe('Issue', () => { ); } } - - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - const sortBtn = div.querySelectorAll('.next-table-header .next-table-sort'); - sortBtn[0].click(); - sortBtn[1].click(); - - assert(div.getElementsByClassName('current').length === 1); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('.next-table-header .next-table-sort').eq(0).click(); + cy.get('.next-table-header .next-table-sort').eq(1).click(); + cy.get('.current').should('have.length', 1); }); it('should sortDirections work', () => { @@ -481,24 +354,16 @@ describe('Issue', () => { ); } } - - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - const sortBtn = div.querySelectorAll('.next-table-header .next-table-sort'); - sortBtn[0].click(); - assert(div.querySelectorAll('a.current .next-icon-descending')); - sortBtn[0].click(); - assert(div.querySelectorAll('a.current .next-icon-ascending')); - sortBtn[0].click(); - assert(div.querySelectorAll('a.current').length === 0); - - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('.next-table-header .next-table-sort').eq(0).click(); + cy.get('a.current .next-icon-descending').should('exist'); + cy.get('.next-table-header .next-table-sort').eq(0).click(); + cy.get('a.current .next-icon-ascending').should('exist'); + cy.get('.next-table-header .next-table-sort').eq(0).click(); + cy.get('a.current').should('not.exist'); }); - it('sort should have only one empty when datasorce=[] && enough width', () => { + it('there should be only one empty block with lock config when datasource=[] && enough width', () => { class App extends React.Component { render() { return ( @@ -511,16 +376,11 @@ describe('Issue', () => { } } - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - assert(div.querySelectorAll('div.next-table-empty').length === 1); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('.next-table-empty').should('exist'); }); - it('fix #466, stickHeader + lock with enough space', () => { + it('fix #466, stickyHeader + lock with enough space', () => { class App extends React.Component { render() { return ( @@ -532,18 +392,13 @@ describe('Issue', () => { } } - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - assert(div.querySelectorAll('div.next-table-empty').length === 1); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('.next-table-empty').should('have.length', 1); }); - it('should support crossline hover', done => { - const container = document.createElement('div'); - document.body.appendChild(container); + it('should support crossline hover', () => { + // const container = document.createElement('div'); + // document.body.appendChild(container); class App extends React.Component { render() { return ( @@ -565,46 +420,19 @@ describe('Issue', () => { } } - ReactDOM.render(, container, function() { - const cell = container.querySelector('td[data-next-table-col="1"][data-next-table-row="1"]'); - const mouseover = new MouseEvent('mouseover', { - view: window, - bubbles: true, - cancelable: true, - }); - - cell.dispatchEvent(mouseover); - - assert(container.querySelectorAll('td.next-table-cell.hovered').length === 2); - - assert(container.querySelectorAll('tr.next-table-row.hovered').length === 1); - - const mouseout = new MouseEvent('mouseout', { - view: window, - bubbles: true, - cancelable: true, - }); - - cell.dispatchEvent(mouseout); - - assert(container.querySelectorAll('td.next-table-cell.hovered').length === 0); - - // target is in inner - const renderA = container.querySelector('#name-0'); - renderA.dispatchEvent(mouseover); - - assert(container.querySelectorAll('td.next-table-cell.hovered').length === 2); - - assert(container.querySelectorAll('tr.next-table-row.hovered').length === 1); - - renderA.dispatchEvent(mouseout); - - assert(container.querySelectorAll('td.next-table-cell.hovered').length === 0); - - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }); + cy.mount(); + cy.get('td[data-next-table-col="1"][data-next-table-row="1"]') + .as('cell') + .trigger('mouseover'); + cy.get('.next-table-cell.hovered').should('have.length', 2); + cy.get('.next-table-row.hovered').should('have.length', 1); + cy.get('@cell').trigger('mouseout'); + cy.get('.next-table-cell.hovered').should('have.length', 0); + cy.get('#name-0').as('renderA').trigger('mouseover'); + cy.get('.next-table-cell.hovered').should('have.length', 2); + cy.get('.next-table-row.hovered').should('have.length', 1); + cy.get('@renderA').trigger('mouseout'); + cy.get('.next-table-cell.hovered').should('have.length', 0); }); it('should support useFirstLevelDataWhenNoChildren', () => { @@ -621,8 +449,7 @@ describe('Issue', () => { product: [ { title: "2014 New Fashion Novelty Tank Slim Women's Fashion Dresses With Lace", - avatar: - 'https://sc01.alicdn.com/kf/HTB1ravHKXXXXXccXVXXq6xXFXXXJ/Chinese-Style-Fashion-Custom-Digital-Print-Silk.jpg_220x220.jpg', + avatar: 'https://sc01.alicdn.com/kf/HTB1ravHKXXXXXccXVXXq6xXFXXXJ/Chinese-Style-Fashion-Custom-Digital-Print-Silk.jpg_220x220.jpg', }, ], }, @@ -635,7 +462,7 @@ describe('Issue', () => { }} /> { + cell={(product: Array<{ title: string }>) => { return product[0].title; }} title="Product Details" @@ -648,20 +475,14 @@ describe('Issue', () => { ); } } - - const div = document.createElement('div'); - document.body.appendChild(div); - ReactDOM.render(, div); - - assert(div.querySelectorAll('.next-table-group-header + tr').length === 1); - ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); + cy.mount(); + cy.get('.next-table-group-header + tr').should('have.length', 1); }); // fix https://github.com/alibaba-fusion/next/issues/4396 - it('should support Header and Body follow the TableGroupHeader when it locks the columns', async () => { - const container = document.createElement('div'); - document.body.appendChild(container); + it('should support Header and Body follow the TableGroupHeader when it locks the columns', () => { + // const container = document.createElement('div'); + // document.body.appendChild(container); const dataSource = () => { const result = []; @@ -681,12 +502,12 @@ describe('Issue', () => { { title: 'Title2', dataIndex: 'id', - lock: 'left', + lock: 'left' as const, width: 140, }, { title: 'Title3', - lock: 'left', + lock: 'left' as const, dataIndex: 'time', width: 200, }, @@ -703,40 +524,33 @@ describe('Issue', () => { width: 500, }, ]; - - ReactDOM.render( + cy.mount( { return
title
; }} /> -
, - container + ); - const tableHeader = container.querySelector('.next-table-header'); - const tableBody = container.querySelector('.next-table-body'); - assert(tableHeader); - assert(tableBody); - // wait for initial scroll align - await delay(200); - tableHeader.scrollLeft = 100; - ReactTestUtils.Simulate.scroll(tableHeader); - await delay(200); - assert(tableHeader.scrollLeft === 100); - assert(tableBody.scrollLeft === 100); - - tableBody.scrollLeft = 0; - ReactTestUtils.Simulate.scroll(tableBody); - await delay(200); - assert(tableHeader.scrollLeft === 0); - assert(tableBody.scrollLeft === 0); + cy.get('.next-table-header').as('tableHeader').should('exist'); + cy.get('.next-table-body').as('tableBody').should('exist'); + // table 内部有定时锁阻止同一时间多次设置 scroll,需要等待一段时间 + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get('@tableHeader').then(tableHeader => { + tableHeader.scrollLeft(100); + }); + cy.get('@tableBody').should('have.prop', 'scrollLeft', 100); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get('@tableBody').then(tableHeader => { + tableHeader.scrollLeft(0); + }); + cy.get('@tableHeader').should('have.prop', 'scrollLeft', 0); }); - it('should support multiple header lock', done => { - const container = document.createElement('div'); - document.body.appendChild(container); - + it('should support multiple header lock', () => { const dataSource = () => { const result = []; for (let i = 0; i < 5; i++) { @@ -748,7 +562,7 @@ describe('Issue', () => { } return result; }; - const render = (value, index, record) => { + const render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; @@ -794,20 +608,13 @@ describe('Issue', () => { }, ]; - ReactDOM.render(, container, function() { - setTimeout(() => { - assert(parseInt(container.querySelector('#target-line').style.left) - 340 < 1); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); + cy.mount(); + cy.get('#target-line').then($ele => { + expect(parseInt($ele.css('left'))).to.be.closeTo(340, 1); }); }); - it('should set right offset, fix #2276', done => { - const container = document.createElement('div'); - document.body.appendChild(container); - + it('should set right offset, fix #2276', () => { const dataSource = () => { const result = []; for (let i = 0; i < 5; i++) { @@ -819,7 +626,7 @@ describe('Issue', () => { } return result; }; - const render = (value, index, record) => { + const render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; @@ -894,24 +701,16 @@ describe('Issue', () => { }, ]; - ReactDOM.render(, container, function() { - setTimeout(() => { - assert( - parseInt(container.querySelectorAll('.next-table-cell.next-table-fix-right.next-table-fix-right-first')[3] - .style.right) - 200 < 1 - ); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); - }); + cy.mount(); + cy.get('.next-table-cell.next-table-fix-right.next-table-fix-right-first') + .eq(3) + .then($ele => { + expect(parseInt($ele.css('right'))).to.be.closeTo(200, 1); + }); }); - it('should work with expanded virtual table, fix #2646', done => { - const container = document.createElement('div'); - document.body.appendChild(container); - - const dataSource = n => { + it('should work with expanded virtual table, fix #2646', () => { + const dataSource = (n: number) => { const result = []; for (let i = 0; i < n; i++) { result.push({ @@ -922,7 +721,7 @@ describe('Issue', () => { } return result; }; - const render = (value, index, record) => { + const render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; @@ -930,7 +729,7 @@ describe('Issue', () => { state = { scrollToRow: 20, }; - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); @@ -956,30 +755,19 @@ describe('Issue', () => { } } - ReactDOM.render(, container, function() { - setTimeout(() => { - const trCount = container.querySelectorAll('.next-table .next-table-body table tr.next-table-row') - .length; - assert(trCount > 10); - assert(trCount < 100); - - const ctrl = container.querySelectorAll( - '.next-table .next-table-body table tr.next-table-row .next-table-expanded-ctrl' - )[0]; - ctrl.click(); - - assert(container.querySelectorAll('.next-table .next-table-body table tr.next-table-expanded-row')); - - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); - }); + cy.mount(); + cy.get('.next-table .next-table-body table tr.next-table-row') + .its('length') + .should('be.greaterThan', 10); + cy.get('.next-table .next-table-body table tr.next-table-row') + .its('length') + .should('be.lessThan', 100); + cy.get('.next-table .next-table-body table tr.next-table-row .next-table-expanded-ctrl') + .eq(0) + .click({ force: true }); + cy.get('.next-table .next-table-body table tr.next-table-expanded-row').should('exist'); }); - it("should set expanded row's width after stickylock table toggle loading, close #3000", done => { - const container = document.createElement('div'); - document.body.appendChild(container); - + it("should set expanded row's width after stickyLock table toggle loading, close #3000", () => { const dataSource = () => { const result = []; for (let i = 0; i < 5; i++) { @@ -992,19 +780,16 @@ describe('Issue', () => { } return result; }, - expandedRowRender = record => record.title, - render = (value, index, record) => { + expandedRowRender: TableProps['expandedRowRender'] = record => record.title, + render: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class App extends React.Component { - constructor(props) { - super(props); - this.state = { - dataSource: dataSource(), - loading: false, - }; - } + state = { + dataSource: dataSource(), + loading: false, + }; toggleLoading = () => { this.setState({ @@ -1039,32 +824,19 @@ describe('Issue', () => { } } - ReactDOM.render(, container, function() { - setTimeout(() => { - const expandedRows = container.querySelectorAll('.next-table-expanded-row .next-table-cell-wrapper'); - expandedRows.forEach(row => { - assert(row.style.width === '499px'); - }); - - const btn = container.querySelector('#sticky-expanded-row-width'); - btn.click(); - setTimeout(() => { - btn.click(); - - expandedRows.forEach(row => { - assert(row.style.width === '499px'); - }); - - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 100); - }, 100); + cy.mount(); + cy.get('.next-table-expanded-row .next-table-cell-wrapper').each($row => { + expect(parseInt($row.css('width'))).to.be.closeTo(499, 1); + }); + cy.get('#sticky-expanded-row-width').click(); + cy.get('#sticky-expanded-row-width').click(); + cy.get('.next-table-expanded-row .next-table-cell-wrapper').each($row => { + expect(parseInt($row.css('width'))).to.be.closeTo(499, 1); }); }); - it('Different sorts have different className of table header , close #3386', done => { - const container = document.createElement('div'); - document.body.appendChild(container); + it('Different sorts have different className of table header , close #3386', () => { + // const container = document.createElement('div'); + // document.body.appendChild(container); class App extends React.Component { render() { return ( @@ -1076,28 +848,14 @@ describe('Issue', () => { } } - ReactDOM.render(, container, function() { - const input = container.querySelector('.next-table-header .next-table-sort'); - input.click(); - setTimeout(() => { - assert(container.querySelectorAll(`.next-table-header-node.next-table-header-sort-desc`).length === 1); - input.click(); - setTimeout(() => { - assert( - container.querySelectorAll(`.next-table-header-node.next-table-header-sort-asc`).length === 1 - ); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 10); - }, 10); - }); + cy.mount(); + cy.get('.next-table-header .next-table-sort').click(); + cy.get('.next-table-header-node.next-table-header-sort-desc').should('have.length', 1); + cy.get('.next-table-header .next-table-sort').click(); + cy.get('.next-table-header-node.next-table-header-sort-asc').should('have.length', 1); }); it('should not modify columns props passed from outside, close #4062', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const columns = [ { title: '商品编码', @@ -1115,20 +873,20 @@ describe('Issue', () => { barcode: 'Bar16858180524079952', itemCode: 'code16858180524079952', itemId: 128581419, - itemName: '测试商品16858180524065799', + itemName: '测试商品 16858180524065799', ownerId: 624144, - ownerName: '快消-商家测试帐号86', + ownerName: '快消 - 商家测试帐号 86', }, { barcode: 'Bar16858755068847002', itemCode: 'code16858755068847002', itemId: 128581770, - itemName: '测试商品16858755068835325', + itemName: '测试商品 16858755068835325', ownerId: 624144, - ownerName: '快消-商家测试帐号86', + ownerName: '快消 - 商家测试帐号 86', }, ]; - const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState>([]); return ( { ); } - ReactDOM.render(, container); - - assert(columns.length === 2); + cy.mount(); + expect(columns.length).to.equal(2); }); it('should support ConfigProvider prefix, close #4073', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const dataSource = () => { const result = []; for (let i = 0; i < 5; i++) { @@ -1175,73 +929,49 @@ describe('Issue', () => { } return result; }; - - ReactDOM.render( + cy.mount(
- , - container + ); - - assert(container.querySelectorAll('.my-table').length >= 1); + cy.get('.my-table').should('exist'); }); it('should not crash when dataSource is undefined, close #4073', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - ReactDOM.render( + cy.mount( -
, - container + ); - assert(container.querySelector('.next-table-empty')); + cy.get('.next-table-empty').should('exist'); }); - it('should not crash when columns is undefined, close #4070', done => { - wrapper.setProps({}); - timeout( - { - columns: undefined, - }, - () => { - assert(wrapper.find('.next-table-empty')); - done(); - } - ); + it('should not crash when columns is undefined, close #4070', () => { + cy.rerender('Demo', { children: undefined }); + cy.get('.next-table').should('exist'); }); it('should can use Input at column.title, close #4370', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - ReactDOM.render( + cy.mount( } lock htmlTitle="Unique Id" dataIndex="id" /> -
, - container + ); - const input = container.querySelector('input'); - assert(input); - ReactTestUtils.Simulate.change(input, { target: { value: 'aa' } }); - assert(input.value === 'aa'); + cy.get('input').type('aa'); + cy.get('input').should('have.value', 'aa'); }); it('should support locking columns when the data source is empty and in the same grouping, close #4282', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - ReactDOM.render( + cy.mount( @@ -1254,72 +984,59 @@ describe('Issue', () => { - , - container +
); - - const title1Cell = container.querySelector('th.next-table-fix-left[rowspan="1"]'); - const title1CellLeft = title1Cell.getBoundingClientRect().left; - const title1CellWidth = title1Cell.getBoundingClientRect().width; - const title2CellLeft = container - .querySelector('th.next-table-fix-left-last[rowspan="1"]') - .getBoundingClientRect().left; - assert(title1CellLeft + title1CellWidth === title2CellLeft); + cy.get('th.next-table-fix-left[rowspan="1"]').eq(0).as('title1Cell'); + cy.get('th.next-table-fix-left-last[rowspan="1"]').as('title2Cell'); + cy.get('@title1Cell').then($title1Cell => { + const title1CellLeft = $title1Cell.get(0).getBoundingClientRect().left; + const title1CellWidth = $title1Cell.get(0).getBoundingClientRect().width; + cy.get('@title2Cell').then($title2Cell => { + const title2CellLeft = $title2Cell.get(0).getBoundingClientRect().left; + expect(title1CellLeft + title1CellWidth).to.equal(title2CellLeft); + }); + }); }); it('should support when dataSource item id 0, close #3740', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - function CheckTable() { - const dataSource = [{ id: 0, name: 'test' }, { id: 1, name: 'test1' },{ id: 2, name: 'test2' }]; + const dataSource = [ + { id: 0, name: 'test' }, + { id: 1, name: 'test1' }, + { id: 2, name: 'test2' }, + ]; return ( -
- ) - }; - ReactDOM.render( - , - container - ); - const getRowCell = function(row, index = 1) { - return row.querySelectorAll('.next-table-cell')[index]; + ); } - const rows = container.querySelectorAll('tr.next-table-row'); - assert(container.querySelectorAll('.next-checkbox-wrapper.checked').length === 1); - assert(container.querySelectorAll('.next-table-body .next-table-row').length === 3); - assert(getRowCell(rows[0]).textContent === '0' && getRowCell(rows[0], 2).textContent === 'test'); - assert(getRowCell(rows[1]).textContent === '1' && getRowCell(rows[1], 2).textContent === 'test1'); - assert(getRowCell(rows[2]).textContent === '2' && getRowCell(rows[2], 2).textContent === 'test2'); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); + cy.mount(); + const getRowCell = function (row: Cypress.Chainable>, index = 1) { + return row.find('.next-table-cell').eq(index); + }; + cy.get('tr.next-table-row').as('rows'); + cy.get('.next-checkbox-wrapper.checked').should('have.length', 1); + cy.get('.next-table-body .next-table-row').should('have.length', 3); + getRowCell(cy.get('@rows').eq(0)).should('have.text', '0'); + getRowCell(cy.get('@rows').eq(0), 2).should('have.text', 'test'); + getRowCell(cy.get('@rows').eq(1)).should('have.text', '1'); + getRowCell(cy.get('@rows').eq(1), 2).should('have.text', 'test1'); + getRowCell(cy.get('@rows').eq(2)).should('have.text', '2'); + getRowCell(cy.get('@rows').eq(2), 2).should('have.text', 'test2'); }); - }); describe('TableScroll', () => { - let mountNode; - - beforeEach(() => { - mountNode = document.createElement('div'); - document.body.appendChild(mountNode); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(mountNode); - document.body.removeChild(mountNode); - }); - it('scroll position error, close #4484', () => { - const dataSource = j => { + const dataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -1331,17 +1048,14 @@ describe('TableScroll', () => { } return result; }; - const relockColumn = (value, index, record) => { + const reLockColumn: ColumnProps['cell'] = (value, index, record) => { return Remove({record.id}); }; class Demo extends React.Component { - constructor(props) { - super(props); - } state = { scrollToRow: 20, }; - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); @@ -1364,27 +1078,29 @@ describe('TableScroll', () => { - + - + ); } } - ReactDOM.render(, mountNode); - const scrollNode = mountNode.querySelector('.next-table-body'); - const rowHeight = scrollNode.querySelector('.next-table-cell').clientHeight; - scrollNode.scrollTop = 200; - ReactTestUtils.Simulate.click(mountNode.querySelector('.next-btn')); - assert(rowHeight * 100 === scrollNode.scrollTop); + cy.mount(); + cy.get('.next-table-body') + .eq(0) + .then($scrollNode => { + const rowHeight = $scrollNode + .get(0) + .querySelector('.next-table-cell')!.clientHeight; + cy.get('.next-table-body').eq(0).scrollTo(0, 200); + cy.get('.next-btn').click(); + cy.get('.next-table-body').should('have.prop', 'scrollTop', rowHeight * 100); + }); }); // fix https://github.com/alibaba-fusion/next/issues/4394 - it('should support onBodyScroll under the condition that useVirtual, dataSource is returned asynchronously, close #4394', async () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - const dataSource = j => { + it('should support onBodyScroll under the condition that useVirtual, dataSource is returned asynchronously, close #4394', () => { + const dataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -1408,7 +1124,7 @@ describe('TableScroll', () => { }); }, 50); }; - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); @@ -1432,46 +1148,53 @@ describe('TableScroll', () => { onBodyScroll={this.onBodyScroll} > - + ); } } - ReactDOM.render(, container); - - await delay(200); - const button = container.querySelector('tr.next-table-row.first button'); - assert(button); - button.click(); - await delay(200); - - const getBodyTop = () => { - const { top, height } = container.querySelector('thead').getBoundingClientRect(); - return top + height; - }; - const skipRow = container.querySelectorAll('tr.next-table-row')[10]; - assert(skipRow.children[0].textContent === '180'); - await delay(200); - const skipRowTop = skipRow.getBoundingClientRect().top; - assert(skipRowTop >= getBodyTop()); - const tbody = container.querySelector('.next-table-body'); - tbody.scrollTop += 10; - ReactTestUtils.Simulate.scroll(tbody); - await delay(200); - const scrollRow = container.querySelectorAll('tr.next-table-row')[10]; - assert(scrollRow.children[0].textContent === '180'); - const scrollRowTop = scrollRow.getBoundingClientRect().top; - assert(scrollRowTop >= getBodyTop() - 10); + cy.mount(); + cy.get('tr.next-table-row.first button').click(); + cy.get('tr.next-table-row') + .eq(10) + .as('skipRow') + .children() + .eq(0) + .should('have.text', '180'); + cy.get('@skipRow').then($skipRow => { + const skipRowTop = $skipRow.get(0).getBoundingClientRect().top; + cy.get('thead').then($thead => { + const theadTop = $thead.get(0).getBoundingClientRect().top; + const theadHeight = $thead.get(0).getBoundingClientRect().height; + expect(skipRowTop).to.gte(theadTop + theadHeight); + }); + }); + cy.get('.next-table-body').then($tbody => { + $tbody.get(0).scrollTop += 10; + }); + cy.get('@skipRow').then($skipRow => { + const skipRowTop = $skipRow.get(0).getBoundingClientRect().top; + cy.get('thead').then($thead => { + const theadTop = $thead.get(0).getBoundingClientRect().top; + const theadHeight = $thead.get(0).getBoundingClientRect().height; + expect(skipRowTop).to.gte(theadTop + theadHeight - 10); + }); + }); }); // fix https://github.com/alibaba-fusion/next/issues/4264 // fix https://github.com/alibaba-fusion/next/issues/4716 - it('should support for merging cells in locked columns, close #4264, #4716', async () => { - const container = document.createElement('div'); - document.body.appendChild(container); + it('should support for merging cells in locked columns, close #4264, #4716', () => { + // const container = document.createElement('div'); + // document.body.appendChild(container); - const dataSource = j => { + const dataSource = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -1484,7 +1207,7 @@ describe('TableScroll', () => { return result; }; - const mergeCell = (rowIndex, colIndex) => { + const mergeCell: TableProps['cellProps'] = (rowIndex, colIndex) => { if (colIndex === 0 && rowIndex === 0) { return { rowSpan: 2, @@ -1493,8 +1216,8 @@ describe('TableScroll', () => { } }; - ReactDOM.render( -
+ cy.mount( +
@@ -1502,26 +1225,29 @@ describe('TableScroll', () => { -
, - container +
); - - const titleHeaderNode = container.querySelectorAll('thead .next-table-header-node')[1]; - assert(titleHeaderNode); - const idHeaderNode = container.querySelectorAll('thead .next-table-header-node')[0]; - assert(idHeaderNode); - assert(titleHeaderNode.getBoundingClientRect().left === idHeaderNode.getBoundingClientRect().right); - - const tableNode = container.querySelector('.next-table-body'); - tableNode.scrollLeft = 900; - ReactTestUtils.Simulate.scroll(tableNode); - await delay(200); - const timeNode = container.querySelectorAll('thead .next-table-header-node')[2]; - assert(timeNode); - assert(timeNode.getBoundingClientRect().left === titleHeaderNode.getBoundingClientRect().right); + cy.get('thead .next-table-header-node').eq(0).as('idHeaderNode').should('exist'); + cy.get('thead .next-table-header-node').eq(1).as('titleHeaderNode').should('exist'); + cy.get('thead .next-table-header-node').eq(2).as('timeHeaderNode').should('exist'); + cy.get('@idHeaderNode').then($id => { + const idRight = $id.get(0).getBoundingClientRect().right; + cy.get('@titleHeaderNode').then($title => { + const titleLeft = $title.get(0).getBoundingClientRect().left; + expect(titleLeft).to.equal(idRight); + }); + }); + cy.get('.next-table-body').scrollTo(900, 0); + cy.get('@timeHeaderNode').then($time => { + const timeLeft = $time.get(0).getBoundingClientRect().left; + cy.get('@titleHeaderNode').then($title => { + const titleRight = $title.get(0).getBoundingClientRect().right; + expect(timeLeft).to.equal(titleRight); + }); + }); }); - it('set keepForwardRenderRows to support large rowSpan when useVirtual, close #4395', async () => { - const datas = j => { + it('set keepForwardRenderRows to support large rowSpan when useVirtual, close #4395', () => { + const datas = (j: number) => { const result = []; for (let i = 0; i < j; i++) { result.push({ @@ -1533,21 +1259,25 @@ describe('TableScroll', () => { } return result; }; - class App extends React.Component { + type AppProps = { scrollToRow?: number; keepForwardRenderRows?: number }; + class App extends React.Component { state = { scrollToRow: 0, dataSource: datas(200), }; - componentDidUpdate(prevProps) { - if ('scrollToRow' in this.props && this.props.scrollToRow !== prevProps.scrollToRow) { + componentDidUpdate(prevProps: AppProps) { + if ( + 'scrollToRow' in this.props && + this.props.scrollToRow !== prevProps.scrollToRow + ) { this.setState({ scrollToRow: this.props.scrollToRow, }); } } - onBodyScroll = start => { + onBodyScroll: TableProps['onBodyScroll'] = start => { this.setState({ scrollToRow: start, }); @@ -1563,7 +1293,7 @@ describe('TableScroll', () => { keepForwardRenderRows={this.props.keepForwardRenderRows} scrollToRow={this.state.scrollToRow} onBodyScroll={this.onBodyScroll} - cellProps={(rowIndex, colIndex) => { + cellProps={(rowIndex: number, colIndex) => { if ([0, 17, 34].includes(rowIndex) && colIndex === 0) { return { rowSpan: 17, @@ -1578,18 +1308,10 @@ describe('TableScroll', () => { ); } } - - const container = document.createElement('div'); - document.body.appendChild(container); - const wrapper = mount(, { attachTo: container }); - await delay(100); - - wrapper.setProps({ scrollToRow: 15 }); - assert(!container.querySelector('[data-next-table-row="0"]')); - - wrapper.setProps({ keepForwardRenderRows: 17 }); - assert(container.querySelector('[data-next-table-row="0"]')); - - wrapper.unmount(); + cy.mount().as('Demo'); + cy.rerender('Demo', { scrollToRow: 15 }).as('Demo2'); + cy.get('[data-next-table-row="0"]').should('not.exist'); + cy.rerender('Demo2', { keepForwardRenderRows: 17 }); + cy.get('[data-next-table-row="0"]').should('exist'); }); }); diff --git a/components/table/base-pre.jsx b/components/table/base-pre.tsx similarity index 70% rename from components/table/base-pre.jsx rename to components/table/base-pre.tsx index 29abc6e4ba..505701e25d 100644 --- a/components/table/base-pre.jsx +++ b/components/table/base-pre.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +// 这个文件看起来没有被使用到 +import React, { type Ref } from 'react'; import Loading from '../loading'; import BodyComponent from './base/body'; import HeaderComponent from './base/header'; @@ -11,9 +12,12 @@ import Column from './column'; import ColumnGroup from './column-group'; import Table from './base'; import { statics } from './util'; +import type { BaseTableProps } from './types'; -function HOC(WrappedComponent) { - class PreTable extends React.Component { +function HOC(WrappedComponent: typeof Table) { + class PreTable extends React.Component< + BaseTableProps & { loading?: boolean; forwardedRef?: Ref> } + > { static Column = Column; static ColumnGroup = ColumnGroup; static Header = HeaderComponent; @@ -39,17 +43,19 @@ function HOC(WrappedComponent) { } } - // 当前版本大于 16.6.3 (有forwardRef的那个版本) + // 当前版本大于 16.6.3(有 forwardRef 的那个版本) if (typeof React.forwardRef === 'function') { - const HocTable = React.forwardRef((props, ref) => { - return ; - }); + const HocTable = React.forwardRef, BaseTableProps>( + (props, ref) => { + return ; + } + ); statics(HocTable, WrappedComponent); return HocTable; } statics(PreTable, WrappedComponent); - // 对于没有低版本用户来说,获取底层Table的ref,可以通过 forwardedRef 这个props获取 + // 对于没有低版本用户来说,获取底层 Table 的 ref,可以通过 forwardedRef 这个 props 获取 return PreTable; } diff --git a/components/table/base.jsx b/components/table/base.tsx similarity index 59% rename from components/table/base.jsx rename to components/table/base.tsx index a9c595b78f..a4dd71bd47 100644 --- a/components/table/base.jsx +++ b/components/table/base.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { type MouseEvent, type ReactElement, type Ref } from 'react'; import PropTypes from 'prop-types'; import { findDOMNode } from 'react-dom'; import classnames from 'classnames'; @@ -7,7 +7,7 @@ import { polyfill } from 'react-lifecycles-compat'; import Loading from '../loading'; import ConfigProvider from '../config-provider'; import zhCN from '../locale/zh-cn'; -import { log, obj, dom } from '../util'; +import { log, obj, dom, type ClassPropsWithDefault } from '../util'; import BodyComponent from './base/body'; import HeaderComponent from './base/header'; import WrapperComponent from './base/wrapper'; @@ -17,6 +17,20 @@ import FilterComponent from './base/filter'; import SortComponent from './base/sort'; import Column from './column'; import ColumnGroup from './column-group'; +import type { + CellLike, + RowLike, + BaseTableContext, + BaseTableProps, + BaseTableState, + BodyProps, + ColumnProps, + HeaderProps, + NormalizedColumnProps, + TableChildProps, + WrapperLike, +} from './types'; +import type Affix from '../affix'; const Children = React.Children, noop = () => {}; @@ -29,8 +43,10 @@ const Children = React.Children, //
// +type InnerBaseTableProps = ClassPropsWithDefault; + /** Table */ -class Table extends React.Component { +class Table extends React.Component { static Column = Column; static ColumnGroup = ColumnGroup; static Header = HeaderComponent; @@ -43,271 +59,63 @@ class Table extends React.Component { static propTypes = { ...ConfigProvider.propTypes, - /** - * 样式类名的品牌前缀 - */ prefix: PropTypes.string, pure: PropTypes.bool, rtl: PropTypes.bool, - /** - * 表格元素的 table-layout 属性,设为 fixed 表示内容不会影响列的布局 - */ tableLayout: PropTypes.oneOf(['fixed', 'auto']), - /** - * 表格的总长度,可以这么用:设置表格总长度 、设置部分列的宽度,这样表格会按照剩余空间大小,自动其他列分配宽度 - */ tableWidth: PropTypes.number, - /** - * 自定义类名 - */ className: PropTypes.string, - /** - * 自定义内联样式 - */ style: PropTypes.object, - /** - * 尺寸 small为紧凑模式 - */ size: PropTypes.oneOf(['small', 'medium']), - /** - * 表格展示的数据源 - */ dataSource: PropTypes.array, entireDataSource: PropTypes.array, - /** - * 点击表格每一行触发的事件 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @param {Event} e DOM事件对象 - */ onRowClick: PropTypes.func, - /** - * 悬浮在表格每一行的时候触发的事件 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @param {Event} e DOM事件对象 - */ onRowMouseEnter: PropTypes.func, - /** - * 离开表格每一行的时候触发的事件 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @param {Event} e DOM事件对象 - */ onRowMouseLeave: PropTypes.func, - /** - * 点击列排序触发的事件 - * @param {String} dataIndex 指定的排序的字段 - * @param {String} order 排序对应的顺序, 有`desc`和`asc`两种 - */ onSort: PropTypes.func, - /** - * 点击过滤确认按钮触发的事件 - * @param {Object} filterParams 过滤的字段信息 - */ onFilter: PropTypes.func, - /** - * 重设列尺寸的时候触发的事件 - * @param {String} dataIndex 指定重设的字段 - * @param {Number} value 列宽变动的数值 - */ onResizeChange: PropTypes.func, - /** - * 设置每一行的属性,如果返回值和其他针对行操作的属性冲突则无效。 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @returns {Object} 需要设置的行属性 - */ rowProps: PropTypes.func, - /** - * 设置单元格的属性,通过该属性可以进行合并单元格 - * @param {Number} rowIndex 该行所对应的序列 - * @param {Number} colIndex 该列所对应的序列 - * @param {String} dataIndex 该列所对应的字段名称 - * @param {Object} record 该行对应的记录 - * @returns {Object} 返回td元素的所支持的属性对象 - */ cellProps: PropTypes.func, - /** - * 虚拟滚动时向前保留渲染的行数 - */ keepForwardRenderRows: PropTypes.number, - /** - * 表格是否具有边框 - */ hasBorder: PropTypes.bool, - /** - * 表格是否具有头部 - */ hasHeader: PropTypes.bool, - /** - * 表格是否是斑马线 - */ isZebra: PropTypes.bool, - /** - * 表格是否在加载中 - */ loading: PropTypes.bool, - /** - * 自定义 Loading 组件 - * 请务必传递 props, 使用方式: loadingComponent={props => } - * @param {LoadingProps} props 需要透传给组件的参数 - * @return {React.ReactNode} 展示的组件 - */ loadingComponent: PropTypes.func, - /** - * 当前过滤的的keys,使用此属性可以控制表格的头部的过滤选项中哪个菜单被选中,格式为 {dataIndex: {selectedKeys:[]}} - * 示例: - * 假设要控制dataIndex为id的列的过滤菜单中key为one的菜单项选中 - * `` - */ filterParams: PropTypes.object, - /** - * 当前排序的字段,使用此属性可以控制表格的字段的排序,格式为{[dataIndex]: 'asc' | 'desc' } , 例如 {id: 'desc'} - */ sort: PropTypes.object, - /** - * 自定义排序按钮,例如上下排布的: `{desc: , asc: }` - */ sortIcons: PropTypes.object, - /** - * 自定义国际化文案对象 - * @property {String} ok 过滤器中确认按钮文案 - * @property {String} reset 过滤器中重置按钮文案 - * @property {String} empty 没有数据情况下 table内的文案 - * @property {String} asc 排序升序状态下的文案 - * @property {String} desc 排序将序状态下的文案 - * @property {String} expanded 可折叠行,展开状态下的文案 - * @property {String} folded 可折叠行,折叠状态下的文案 - * @property {String} filter 过滤器文案 - * @property {String} selectAll header里全选的按钮文案 - */ locale: PropTypes.object, components: PropTypes.object, - /** - * 等同于写子组件 Table.Column ,子组件优先级更高 - */ columns: PropTypes.array, - /** - * 设置数据为空的时候的表格内容展现 - */ emptyContent: PropTypes.node, - /** - * dataSource当中数据的主键,如果给定的数据源中的属性不包含该主键,会造成选择状态全部选中 - */ primaryKey: PropTypes.oneOfType([PropTypes.symbol, PropTypes.string]), lockType: PropTypes.oneOf(['left', 'right']), wrapperContent: PropTypes.any, refs: PropTypes.object, - /** - * 额外渲染行的渲染函数 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @returns {Element} 渲染内容 - */ expandedRowRender: PropTypes.func, - /** - * 设置行是否可展开,设置 false 为不可展开 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @returns {Boolean} 是否可展开 - */ rowExpandable: PropTypes.func, - /** - * 额外渲染行的缩进, 是个二维数组(eg:[1,1]) 分别表示左右两边的缩进 - */ expandedRowIndent: PropTypes.array, - /** - * 是否显示点击展开额外渲染行的+号按钮 - */ hasExpandedRowCtrl: PropTypes.bool, - /** - * 设置额外渲染行的属性 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @returns {Object} 额外渲染行的属性 - */ getExpandedColProps: PropTypes.func, - /** - * 当前展开的 Expand行 或者 Tree行 , 传入此属性为受控状态,一般配合 onRowOpen 使用 - */ openRowKeys: PropTypes.array, - /** - * 默认情况下展开的 Expand行 或者 Tree行,非受控模式 - * @version 1.23.22 - */ defaultOpenRowKeys: PropTypes.array, - /** - * 在 Expand行 或者 Tree行 展开或者收起的时候触发的事件 - * @param {Array} openRowKeys 展开的渲染行的key - * @param {String} currentRowKey 当前点击的渲染行的key - * @param {Boolean} expanded 当前点击是展开还是收起 - * @param {Object} currentRecord 当前点击额外渲染行的记录 - */ onRowOpen: PropTypes.func, onExpandedRowClick: PropTypes.func, - /** - * 表头是否固定,该属性配合maxBodyHeight使用,当内容区域的高度超过maxBodyHeight的时候,在内容区域会出现滚动条 - */ fixedHeader: PropTypes.bool, - /** - * 最大内容区域的高度,在`fixedHeader`为`true`的时候,超过这个高度会出现滚动条 - */ maxBodyHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * 是否启用选择模式 - * @property {Function} getProps `Function(record, index)=>Object` 获取selection的默认属性 - * @property {Function} onChange `Function(selectedRowKeys:Array, records:Array)` 选择改变的时候触发的事件,**注意:** 其中records只会包含当前dataSource的数据,很可能会小于selectedRowKeys的长度。 - * @property {Function} onSelect `Function(selected:Boolean, record:Object, records:Array)` 用户手动选择/取消选择某行的回调 - * @property {Function} onSelectAll `Function(selected:Boolean, records:Array)` 用户手动选择/取消选择所有行的回调 - * @property {Array} selectedRowKeys 设置了此属性,将rowSelection变为受控状态,接收值为该行数据的primaryKey的值 - * @property {String} mode 选择selection的模式, 可选值为`single`, `multiple`,默认为`multiple` - * @property {Function} columnProps `Function()=>Object` 选择列 的props,例如锁列、对齐等,可使用`Table.Column` 的所有参数 - * @property {Function} titleProps `Function()=>Object` 选择列 表头的props,仅在 `multiple` 模式下生效 - * @property {Function} titleAddons `Function()=>Node` 选择列 表头添加的元素,在`single` `multiple` 下都生效 - */ rowSelection: PropTypes.object, - /** - * 表头是否是sticky - */ stickyHeader: PropTypes.bool, - /** - * 距离窗口顶部达到指定偏移量后触发 - */ offsetTop: PropTypes.number, - /** - * affix组件的的属性 - */ affixProps: PropTypes.object, - /** - * 在tree模式下的缩进尺寸, 仅在isTree为true时候有效 - */ indent: PropTypes.number, - /** - * 开启Table的tree模式, 接收的数据格式中包含children则渲染成tree table - */ isTree: PropTypes.bool, - /** - * 是否开启虚拟滚动 - */ useVirtual: PropTypes.bool, rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - /** - * 滚动到第几行,需要保证行高相同。1.22.15 版本之前仅在虚拟滚动场景下生效,之后在所有情况下生效 - * @version 1.22.15 - */ scrollToRow: PropTypes.number, - /** - * 在内容区域滚动的时候触发的函数 - */ onBodyScroll: PropTypes.func, - /** - * 开启时,getExpandedColProps() / rowProps() / expandedRowRender() 的第二个参数 index (该行所对应的序列) 将按照01,2,3,4...的顺序返回,否则返回真实index(0,2,4,6... / 1,3,5,7...) - */ expandedIndexSimulate: PropTypes.bool, - /** - * 在 hover 时出现十字参考轴,适用于表头比较复杂,需要做表头分类的场景。 - */ crossline: PropTypes.bool, lengths: PropTypes.object, }; @@ -348,7 +156,18 @@ class Table extends React.Component { getTableInstanceForExpand: PropTypes.func, }; - constructor(props, context) { + readonly props: InnerBaseTableProps; + notRenderCellIndex: number[]; + groupChildren: ColumnProps[][]; + flatChildren: ColumnProps[]; + wrapper: InstanceType | null; + resizeProxyDomRef: HTMLDivElement | null; + tableEl: HTMLElement | null; + affixRef: InstanceType | null; + colIndex: number; + rowIndex: number; + + constructor(props: BaseTableProps, context: BaseTableContext) { super(props, context); const { getTableInstance, @@ -363,7 +182,7 @@ class Table extends React.Component { this.notRenderCellIndex = []; } - state = { + state: BaseTableState = { sort: this.props.sort || {}, }; @@ -374,8 +193,8 @@ class Table extends React.Component { }; } - static getDerivedStateFromProps(nextProps) { - const state = {}; + static getDerivedStateFromProps(nextProps: InnerBaseTableProps) { + const state: Partial = {}; if (typeof nextProps.sort !== 'undefined') { state.sort = nextProps.sort; @@ -388,7 +207,11 @@ class Table extends React.Component { this.notRenderCellIndex = []; } - shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdate( + nextProps: BaseTableProps, + nextState: BaseTableState, + nextContext: BaseTableContext + ) { if (nextProps.pure) { const isEqual = shallowElementEquals(nextProps, this.props) && @@ -404,7 +227,11 @@ class Table extends React.Component { this.notRenderCellIndex = []; } - normalizeChildrenState(props) { + [headerCellKey: `header_cell_${number}_${number}`]: InstanceType | null; + [rowKey: `row_${number | string}`]: InstanceType | null; + [cellKey: `cell_${number}_${number}`]: InstanceType | null; + + normalizeChildrenState(props: InnerBaseTableProps) { let columns = props.columns; if (props.children) { columns = this.normalizeChildren(props); @@ -412,34 +239,43 @@ class Table extends React.Component { return this.fetchInfoFromBinaryChildren(columns); } - // 将React结构化数据提取props转换成数组 - normalizeChildren(props) { + // 将 React 结构化数据提取 props 转换成数组 + normalizeChildren(props: InnerBaseTableProps) { let { columns } = props; - const getChildren = children => { - const ret = []; - Children.forEach(children, child => { - if (child) { - const props = { ...child.props }; - - if (child.ref) { - props.ref = child.ref; - } - - if ( - !( - child && - ['function', 'object'].indexOf(typeof child.type) > -1 && - (child.type._typeMark === 'column' || child.type._typeMark === 'columnGroup') - ) - ) { - log.warning('Use , as child.'); + const getChildren = (children: InnerBaseTableProps['children']) => { + const ret: NormalizedColumnProps[] = []; + Children.forEach( + children, + ( + child: ReactElement & { + ref: Ref; + type: { _typeMark: string }; } - ret.push(props); - if (child.props.children) { - props.children = getChildren(child.props.children); + ) => { + if (child) { + const props = { ...child.props } as NormalizedColumnProps; + + if (child.ref) { + props.ref = child.ref; + } + + if ( + !( + child && + ['function', 'object'].indexOf(typeof child.type) > -1 && + (child.type._typeMark === 'column' || + child.type._typeMark === 'columnGroup') + ) + ) { + log.warning('Use , as child.'); + } + ret.push(props); + if ('children' in child.props && child.props.children) { + props.children = getChildren(child.props.children); + } } } - }); + ); return ret; }; if (props.children) { @@ -448,11 +284,11 @@ class Table extends React.Component { return columns; } - fetchInfoFromBinaryChildren(children) { + fetchInfoFromBinaryChildren(children: BaseTableProps['columns']) { let hasGroupHeader = false; - const flatChildren = [], - groupChildren = [], - getChildren = (propsChildren = [], level) => { + const flatChildren: NormalizedColumnProps[] = [], + groupChildren: NormalizedColumnProps[][] = [], + getChildren = (propsChildren: BaseTableProps['columns'] = [], level: number) => { groupChildren[level] = groupChildren[level] || []; propsChildren.forEach(child => { child.headerCellRowIndex = level; @@ -466,13 +302,13 @@ class Table extends React.Component { groupChildren[level].push(child); }); }, - getColSpan = (children, colSpan) => { + getColSpan = (children: NormalizedColumnProps[], colSpan?: number) => { colSpan = colSpan || 0; children.forEach(child => { if (child.children) { colSpan = getColSpan(child.children, colSpan); } else { - colSpan += 1; + colSpan! += 1; } }); return colSpan; @@ -504,7 +340,7 @@ class Table extends React.Component { }; } - renderColGroup(flatChildren) { + renderColGroup(flatChildren: NormalizedColumnProps[]) { const cols = flatChildren.map((col, index) => { const width = col.width; let style = {}; @@ -519,7 +355,7 @@ class Table extends React.Component { return {cols}; } - onSort = (dataIndex, order, sort) => { + onSort: HeaderProps['onSort'] = (dataIndex, order, sort) => { if (typeof this.props.sort === 'undefined') { this.setState( { @@ -534,16 +370,16 @@ class Table extends React.Component { } }; - onFilter = filterParams => { + onFilter: HeaderProps['onFilter'] = filterParams => { this.props.onFilter(filterParams); }; - onResizeChange = (dataIndex, value) => { + onResizeChange: HeaderProps['onResizeChange'] = (dataIndex, value) => { this.props.onResizeChange(dataIndex, value); }; // 通过头部和扁平的结构渲染表格 - renderTable(groupChildren, flatChildren) { + renderTable(groupChildren: NormalizedColumnProps[][], flatChildren: NormalizedColumnProps[]) { if (flatChildren.length || (!flatChildren.length && !this.props.lockType)) { const { hasHeader, @@ -569,7 +405,11 @@ class Table extends React.Component { tableWidth, } = this.props; const { sort } = this.state; - const { Header = HeaderComponent, Wrapper = WrapperComponent, Body = BodyComponent } = components; + const { + Header = HeaderComponent, + Wrapper = WrapperComponent as WrapperLike, + Body = BodyComponent, + } = components; const colGroup = this.renderColGroup(flatChildren); return [ @@ -643,52 +483,56 @@ class Table extends React.Component { } } - getResizeProxyDomRef = resizeProxyDom => { + getResizeProxyDomRef = (resizeProxyDom: HTMLDivElement | null) => { if (!resizeProxyDom) { return this.resizeProxyDomRef; } this.resizeProxyDomRef = resizeProxyDom; }; - getWrapperRef = wrapper => { + getWrapperRef = (wrapper: InstanceType | null) => { if (!wrapper) { return this.wrapper; } this.wrapper = wrapper; }; - getAffixRef = affixRef => { + getAffixRef = (affixRef: InstanceType | null) => { if (!affixRef) { return this.affixRef; } this.affixRef = affixRef; }; - getHeaderCellRef = (i, j, cell) => { - const cellRef = `header_cell_${i}_${j}`; + getHeaderCellRef = (i: number, j: number, cell?: InstanceType | null) => { + const cellRef = `header_cell_${i}_${j}` as const; if (!cell) { return this[cellRef]; } this[cellRef] = cell; }; - getRowRef = (i, row) => { - const rowRef = `row_${i}`; + getRowRef: NonNullable = (i, row) => { + const rowRef = `row_${i}` as const; if (!row) { return this[rowRef]; } this[rowRef] = row; }; - getCellRef = (i, j, cell) => { - const cellRef = `cell_${i}_${j}`; + getCellRef = ( + i: number, + j: number, + cell?: InstanceType | HTMLTableCellElement | null + ) => { + const cellRef = `cell_${i}_${j}` as const; if (!cell) { return this[cellRef]; } - this[cellRef] = cell; + this[cellRef] = cell as InstanceType; }; - handleColHoverClass = (rowIndex, colIndex, isAdd) => { + handleColHoverClass = (rowIndex: number, colIndex: number, isAdd: boolean) => { const { crossline } = this.props; const funcName = isAdd ? 'addClass' : 'removeClass'; if (crossline) { @@ -697,7 +541,7 @@ class Table extends React.Component { // in case of finding an unmounted component due to cached data // need to clear refs of this.tableInc when dataSource Changed // in virtual table - const currentCol = findDOMNode(this.getCellRef(index, colIndex)); + const currentCol = findDOMNode(this.getCellRef(index, colIndex)) as Element; currentCol && dom[funcName](currentCol, 'hovered'); } catch (error) { return null; @@ -706,13 +550,9 @@ class Table extends React.Component { } }; - /** - * @param event - * @returns {Object} { rowIndex: string; colIndex: string } - */ - findEventTarget = e => { + findEventTarget = (e: MouseEvent): { colIndex?: number; rowIndex?: number } => { const { prefix } = this.props; - const target = dom.getClosest(e.target, `td.${prefix}table-cell`); + const target = dom.getClosest(e.target as HTMLElement, `td.${prefix}table-cell`); const colIndex = target && target.getAttribute('data-next-table-col'); const rowIndex = target && target.getAttribute('data-next-table-row'); @@ -720,10 +560,13 @@ class Table extends React.Component { // in case of finding an unmounted component due to cached data // need to clear refs of this.tableInc when dataSource Changed // in virtual table + // @ts-expect-error rowIndex, colIndex 应该转为 number const currentCol = findDOMNode(this.getCellRef(rowIndex, colIndex)); if (currentCol === target) { return { + // @ts-expect-error rowIndex, colIndex 应该转为 number colIndex, + // @ts-expect-error rowIndex, colIndex 应该转为 number rowIndex, }; } @@ -734,7 +577,7 @@ class Table extends React.Component { return {}; }; - onBodyMouseOver = e => { + onBodyMouseOver = (e: MouseEvent) => { const { crossline } = this.props; if (!crossline) { return; @@ -750,7 +593,7 @@ class Table extends React.Component { this.rowIndex = rowIndex; }; - onBodyMouseOut = e => { + onBodyMouseOut = (e: MouseEvent) => { const { crossline } = this.props; if (!crossline) { return; @@ -766,13 +609,13 @@ class Table extends React.Component { this.rowIndex = -1; }; - addColIndex = (children, start = 0) => { + addColIndex = (children: NormalizedColumnProps[], start = 0) => { children.forEach((child, i) => { child.__colIndex = start + i; }); }; - getTableEl = ref => { + getTableEl = (ref: HTMLElement | null) => { this.tableEl = ref; }; @@ -780,8 +623,7 @@ class Table extends React.Component { const ret = this.normalizeChildrenState(this.props); this.groupChildren = ret.groupChildren; this.flatChildren = ret.flatChildren; - /* eslint-disable no-unused-vars, prefer-const */ - let table = this.renderTable(ret.groupChildren, ret.flatChildren), + const table = this.renderTable(ret.groupChildren, ret.flatChildren), { className, style, @@ -808,6 +650,7 @@ class Table extends React.Component { lockType, locale, expandedIndexSimulate, + // @ts-expect-error refs 没有实际作用,应该可以去掉 refs, pure, rtl, @@ -818,6 +661,7 @@ class Table extends React.Component { loadingComponent: LoadingComponent = Loading, tableLayout, tableWidth, + // @ts-expect-error props 并不能拿到 ref,这里的实现应该是无效的 ref, ...others } = this.props, @@ -829,10 +673,11 @@ class Table extends React.Component { 'only-bottom-border': !hasBorder, 'no-header': !hasHeader, zebra: isZebra, - [className]: className, + [className!]: className, }); if (rtl) { + // @ts-expect-error others 无法附其他值,这里应该重新做一个变量 others.dir = 'rtl'; } diff --git a/components/table/base/body.jsx b/components/table/base/body.tsx similarity index 70% rename from components/table/base/body.jsx rename to components/table/base/body.tsx index 4e71bfaf77..b5f70ba534 100644 --- a/components/table/base/body.jsx +++ b/components/table/base/body.tsx @@ -1,13 +1,16 @@ -import React from 'react'; +import React, { type ReactNode, type MouseEvent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import RowComponent from './row'; import CellComponent from './cell'; -import { dom, events } from '../../util'; +import { type ClassPropsWithDefault, dom, events } from '../../util'; +import type { BodyProps, CellLike, RecordItem, RowLike, RowProps } from '../types'; const noop = () => {}; -export default class Body extends React.Component { +type InnerBodyProps = ClassPropsWithDefault; + +export default class Body extends React.Component { static propTypes = { loading: PropTypes.bool, emptyContent: PropTypes.any, @@ -49,6 +52,9 @@ export default class Body extends React.Component { columns: [], }; + readonly props: InnerBodyProps; + emptyNode: HTMLElement | null; + componentDidMount() { events.on(window, 'resize', this.setEmptyDomStyle); } @@ -61,46 +67,45 @@ export default class Body extends React.Component { events.off(window, 'resize', this.setEmptyDomStyle); } - getRowRef = (i, row) => { + getRowRef = (i: number, row: InstanceType | null) => { this.props.rowRef(i, row); }; - onRowClick = (record, index, e) => { - this.props.onRowClick(record, index, e); + onRowClick = (record: RecordItem, index: number, e: MouseEvent) => { + this.props.onRowClick!(record, index, e); }; - onRowMouseEnter = (record, index, e) => { - this.props.onRowMouseEnter(record, index, e); + onRowMouseEnter = (record: RecordItem, index: number, e: MouseEvent) => { + this.props.onRowMouseEnter!(record, index, e); }; - onRowMouseLeave = (record, index, e) => { - this.props.onRowMouseLeave(record, index, e); + onRowMouseLeave = (record: RecordItem, index: number, e: MouseEvent) => { + this.props.onRowMouseLeave!(record, index, e); }; - onBodyMouseOver = e => { + onBodyMouseOver = (e: MouseEvent) => { this.props.onBodyMouseOver(e); }; - onBodyMouseOut = e => { + onBodyMouseOut = (e: MouseEvent) => { this.props.onBodyMouseOut(e); }; - getEmptyNode = ref => { + getEmptyNode = (ref: HTMLElement | null) => { this.emptyNode = ref; }; setEmptyDomStyle = () => { const { tableEl } = this.props; - // getboundingclientRect 获取的是除 margin 之外的内容区,可能带小数点,不四舍五入 - const borderLeftWidth = dom.getStyle(tableEl, 'borderLeftWidth'); + // getBoundingClientRect 获取的是除 margin 之外的内容区,可能带小数点,不四舍五入 + const borderLeftWidth = dom.getStyle(tableEl!, 'borderLeftWidth') as number; const tableWidth = tableEl && tableEl.getBoundingClientRect().width; - const totalWidth = tableWidth - borderLeftWidth - 1 || '100%'; + const totalWidth = tableWidth! - borderLeftWidth - 1 || '100%'; dom.setStyle(this.emptyNode, { width: totalWidth }); }; render() { - /*eslint-disable no-unused-vars */ const { prefix, className, @@ -132,17 +137,22 @@ export default class Body extends React.Component { ...others } = this.props; - const totalWidth = +(tableEl && tableEl.clientWidth) - 1 || '100%'; + const totalWidth = +(tableEl! && tableEl.clientWidth) - 1 || '100%'; - const { Row = RowComponent, Cell = CellComponent } = components; - const empty = loading ?   : emptyContent || locale.empty; - let rows = ( + const { Row = RowComponent as RowLike, Cell = CellComponent as CellLike } = components; + const empty = loading ?   : emptyContent || locale!.empty; + let rows: ReactNode = (
{empty}
@@ -158,9 +168,12 @@ export default class Body extends React.Component { } if (dataSource.length) { rows = dataSource.map((record, index) => { - let rowProps = {}; + let rowProps: Partial | undefined | void = {}; // record may be a string - const rowIndex = typeof record === 'object' && '__rowIndex' in record ? record.__rowIndex : index; + const rowIndex = + typeof record === 'object' && '__rowIndex' in record + ? (record.__rowIndex as number) + : index; if (expandedIndexSimulate) { rowProps = record.__expanded ? {} : getRowProps(record, index / 2); @@ -174,14 +187,19 @@ export default class Body extends React.Component { const className = classnames({ first: index === 0, last: index === dataSource.length - 1, - [rowClass]: rowClass, + [rowClass!]: rowClass, }); const expanded = record.__expanded ? 'expanded' : ''; return ( ; + +export default class Cell extends React.Component { static propTypes = { prefix: PropTypes.string, pure: PropTypes.bool, @@ -14,7 +17,7 @@ export default class Cell extends React.Component { isIconLeft: PropTypes.bool, colIndex: PropTypes.number, rowIndex: PropTypes.number, - // 经过锁列调整后的列索引,lock right的列会从非0开始 + // 经过锁列调整后的列索引,lock right 的列会从非 0 开始 __colIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), title: PropTypes.any, width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), @@ -42,11 +45,13 @@ export default class Cell extends React.Component { component: 'td', type: 'body', isIconLeft: false, - cell: value => value, + cell: (value: unknown) => value, prefix: 'next-', }; - shouldComponentUpdate(nextProps) { + readonly props: InnerCellProps; + + shouldComponentUpdate(nextProps: CellProps) { if (nextProps.pure) { const isEqual = obj.shallowEqual(this.props, nextProps); return !isEqual; @@ -55,7 +60,6 @@ export default class Cell extends React.Component { } render() { - /* eslint-disable no-unused-vars */ const { prefix, className, @@ -96,9 +100,9 @@ export default class Cell extends React.Component { } = this.props; const tagStyle = { ...style }; const cellProps = { value, index: rowIndex, record, context }; - let content = cell; + let content: ReactNode = cell; if (React.isValidElement(content)) { - // header情况下, props.cell为 column.title,不需要传递这些props + // header 情况下,props.cell 为 column.title,不需要传递这些 props content = React.cloneElement(content, type === 'header' ? undefined : cellProps); } else if (typeof content === 'function') { content = content(value, rowIndex, record, context); @@ -106,13 +110,14 @@ export default class Cell extends React.Component { if (align) { tagStyle.textAlign = align; if (rtl) { - tagStyle.textAlign = align === 'left' ? 'right' : align === 'right' ? 'left' : align; + tagStyle.textAlign = + align === 'left' ? 'right' : align === 'right' ? 'left' : align; } } const cls = classnames({ [`${prefix}table-cell`]: true, [`${prefix}table-word-break-${wordBreak}`]: !!wordBreak, - [className]: className, + [className!]: className, }); return ( diff --git a/components/table/base/filter.jsx b/components/table/base/filter.tsx similarity index 67% rename from components/table/base/filter.jsx rename to components/table/base/filter.tsx index 745c260d28..95ffbe1a85 100644 --- a/components/table/base/filter.jsx +++ b/components/table/base/filter.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { type Key, type ReactNode, type KeyboardEvent } from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classnames from 'classnames'; @@ -7,9 +7,10 @@ import Menu from '../../menu'; import Button from '../../button'; import Icon from '../../icon'; import { KEYCODE } from '../../util'; +import type { FilterItem, FilterProps, FilterState } from '../types'; // 共享状态的组件需要变成非受控组件 -class Filter extends React.Component { +class Filter extends React.Component { static propTypes = { dataIndex: PropTypes.string, filters: PropTypes.array, @@ -26,11 +27,12 @@ class Filter extends React.Component { static defaultProps = { onFilter: () => {}, }; + _selectedKeys: string[]; - constructor(props) { + constructor(props: FilterProps) { super(props); const filterParams = props.filterParams || {}; - const filterConfig = filterParams[props.dataIndex] || {}; + const filterConfig = filterParams[props.dataIndex!] || {}; this.state = { visible: filterConfig.visible || false, selectedKeys: filterConfig.selectedKeys || [], @@ -39,9 +41,13 @@ class Filter extends React.Component { this._selectedKeys = [...this.state.selectedKeys]; } - static getDerivedStateFromProps(nextProps, prevState) { - const state = {}; - if (nextProps.hasOwnProperty('filterParams') && typeof nextProps.filterParams !== 'undefined') { + static getDerivedStateFromProps(nextProps: FilterProps, prevState: FilterState) { + const state: Partial = {}; + if ( + nextProps.hasOwnProperty('filterParams') && + typeof nextProps.filterParams !== 'undefined' + ) { + // @ts-expect-error getDerivedStateFromProps 是类方法,这里不应该使用 this const dataIndex = nextProps.dataIndex || this.props.dataIndex; const filterParams = nextProps.filterParams || {}; const filterConfig = filterParams[dataIndex] || {}; @@ -59,12 +65,12 @@ class Filter extends React.Component { return state; } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: FilterProps, prevState: FilterState) { const { selectedKeys } = prevState; this._selectedKeys = [...selectedKeys]; } - filterKeydown = e => { + filterKeydown = (e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); @@ -75,7 +81,7 @@ class Filter extends React.Component { } }; - onFilterVisible = visible => { + onFilterVisible = (visible: boolean) => { this.setState({ visible, }); @@ -90,7 +96,7 @@ class Filter extends React.Component { } }; - onFilterSelect = selectedKeys => { + onFilterSelect = (selectedKeys: string[]) => { this.setState({ visible: true, selectedKeysChangedByInner: true, @@ -100,10 +106,15 @@ class Filter extends React.Component { onFilterConfirm = () => { const selectedKeys = this.state.selectedKeys; - const filterParams = {}, + const filterParams = {} as { + [key: string]: { + visible: boolean; + selectedKeys: string[]; + }; + }, { dataIndex } = this.props; - filterParams[dataIndex] = { + filterParams[dataIndex!] = { visible: false, selectedKeys: selectedKeys, }; @@ -113,14 +124,19 @@ class Filter extends React.Component { selectedKeysChangedByInner: true, }); // 兼容之前的格式 - this.props.onFilter(filterParams); + this.props.onFilter!(filterParams); }; onFilterClear = () => { - const filterParams = {}, + const filterParams = {} as { + [key: string]: { + visible: boolean; + selectedKeys: string[]; + }; + }, { dataIndex } = this.props; - filterParams[dataIndex] = { + filterParams[dataIndex!] = { visible: false, selectedKeys: [], }; @@ -131,11 +147,20 @@ class Filter extends React.Component { selectedKeysChangedByInner: true, }); // 兼容之前的格式 - this.props.onFilter(filterParams); + this.props.onFilter!(filterParams); }; render() { - const { filters, prefix, locale, className, filterMode, filterMenuProps, filterProps, rtl } = this.props; + const { + filters, + prefix, + locale, + className, + filterMode, + filterMenuProps, + filterProps, + rtl, + } = this.props; const dropdownClassname = classnames(filterProps && filterProps.className, { [`${prefix}table-filter-menu`]: true, @@ -143,19 +168,27 @@ class Filter extends React.Component { const { visible, selectedKeys } = this.state; const { subMenuSelectable, ...others } = filterMenuProps || {}; - function renderMenuItem(item) { + function renderMenuItem(item: { value?: Key; label: ReactNode }) { return {item.label}; } - function renderSubMenu(parent, children) { + function renderSubMenu(parent: { value?: Key; label: ReactNode }, children: FilterItem[]) { return ( - - {renderMenuContent(children)} + + { + // renderSubMenu 和 renderMenuContent 存在互相调用的问题,谁排在前面都不合适 + // eslint-disable-next-line @typescript-eslint/no-use-before-define + renderMenuContent(children) + } ); } - function renderMenuContent(list) { + function renderMenuContent(list: FilterItem[]) { return list.map(item => { if (item.children) { return renderSubMenu(item, item.children); @@ -165,19 +198,19 @@ class Filter extends React.Component { }); } - const content = renderMenuContent(filters), + const content = renderMenuContent(filters!), footer = (
- +
); const cls = classnames({ [`${prefix}table-filter`]: true, - [className]: className, + [className!]: className, }); const filterIconCls = classnames({ @@ -189,9 +222,9 @@ class Filter extends React.Component { trigger={ diff --git a/components/table/base/header.jsx b/components/table/base/header.tsx similarity index 84% rename from components/table/base/header.jsx rename to components/table/base/header.tsx index 1955463c00..27272a51f8 100644 --- a/components/table/base/header.jsx +++ b/components/table/base/header.tsx @@ -5,9 +5,14 @@ import FilterComponent from './filter'; import SortComponent from './sort'; import CellComponent from './cell'; import ResizeComponent from './resize'; +import type { CellLike, CellProps, HeaderProps, SortProps } from '../types'; +import { type ClassPropsWithDefault } from '../../util'; const noop = () => {}; -export default class Header extends React.Component { + +type InnerHeaderProps = ClassPropsWithDefault; + +export default class Header extends React.Component { static propTypes = { children: PropTypes.any, prefix: PropTypes.string, @@ -37,12 +42,17 @@ export default class Header extends React.Component { onSort: noop, onResizeChange: noop, }; - constructor() { - super(); + hasLock: boolean; + readonly props: InnerHeaderProps; + + constructor(props: HeaderProps) { + super(props); this.hasLock = false; } + [cellKey: `header_cell_${number}_${number}`]: { current?: HTMLElement }; + checkHasLock = () => { const { columns } = this.props; let hasLock = false; @@ -62,7 +72,7 @@ export default class Header extends React.Component { this.hasLock = hasLock; }; - getCellRef = (i, j, cell) => { + getCellRef = (i: number, j: number, cell: InstanceType) => { this.props.headerCellRef(i, j, cell); const { columns } = this.props; @@ -72,7 +82,7 @@ export default class Header extends React.Component { } }; - createCellDomRef = (i, j) => { + createCellDomRef = (i: number, j: number) => { const cellRefKey = this.getCellDomRefKey(i, j); if (!this[cellRefKey]) { this[cellRefKey] = {}; @@ -81,7 +91,7 @@ export default class Header extends React.Component { return this[cellRefKey]; }; - getCellDomRef = (cellRef, cellDom) => { + getCellDomRef = (cellRef: { current?: CellLike }, cellDom: CellLike) => { if (!cellRef) { return; } @@ -89,16 +99,15 @@ export default class Header extends React.Component { cellRef.current = cellDom; }; - getCellDomRefKey = (i, j) => { - return `header_cell_${i}_${j}`; + getCellDomRefKey = (i: number, j: number) => { + return `header_cell_${i}_${j}` as const; }; - onSort = (dataIndex, order, sort) => { + onSort: SortProps['onSort'] = (dataIndex, order, sort) => { this.props.onSort(dataIndex, order, sort); }; render() { - /*eslint-disable no-unused-vars */ const { prefix, className, @@ -137,8 +146,7 @@ export default class Header extends React.Component { const header = columns.map((cols, index) => { const col = cols.map((col, j) => { const cellRef = this.createCellDomRef(index, j); - /* eslint-disable no-unused-vars, prefer-const */ - let { + const { title, colSpan, sortable, @@ -153,7 +161,6 @@ export default class Header extends React.Component { width, align, alignHeader, - className, __normalized, lock, cellStyle, @@ -161,22 +168,23 @@ export default class Header extends React.Component { ...others } = col; - const order = sort ? sort[dataIndex] : ''; + let { className } = col; + + const order = sort ? sort[dataIndex!] : ''; className = classnames({ [`${prefix}table-header-node`]: true, [`${prefix}table-header-resizable`]: resizable || asyncResizable, [`${prefix}table-word-break-${wordBreak}`]: !!wordBreak, [`${prefix}table-header-sort-${order}`]: sortable && order, - [className]: className, + [className!]: className, }); - let attrs = {}, - sortElement, - filterElement, - resizeElement; + let sortElement, filterElement, resizeElement; + const attrs: Partial = {}; attrs.colSpan = colSpan; // column.group doesn't have sort resize filter + // @ts-expect-error children 不一定有 length 属性 if (!(col.children && col.children.length)) { if (sortable) { sortElement = ( @@ -199,11 +207,11 @@ export default class Header extends React.Component { asyncResizable={asyncResizable} hasLock={this.hasLock} col={col} - tableEl={tableEl} + tableEl={tableEl!} prefix={prefix} rtl={rtl} - dataIndex={dataIndex} - resizeProxyDomRef={resizeProxyDomRef} + dataIndex={dataIndex!} + resizeProxyDomRef={resizeProxyDomRef!} cellDomRef={cellRef} onChange={onResizeChange} /> @@ -213,7 +221,7 @@ export default class Header extends React.Component { if (filters) { filterElement = filters.length ? ( { static propTypes = { prefix: T.string, rtl: T.bool, @@ -15,8 +16,22 @@ class Resize extends React.Component { hasLock: T.bool, asyncResizable: T.bool, }; - constructor() { - super(); + + static defaultProps = { + onChange: () => {}, + }; + + cellMinWidth: number; + lastPageX: number; + tRight: number; + tLeft: number; + cellLeft: number; + startLeft: number; + changedPageX: number; + asyncResizeFlag: boolean; + + constructor(props: ResizeProps) { + super(props); this.cellMinWidth = 40; @@ -29,18 +44,16 @@ class Resize extends React.Component { this.asyncResizeFlag = false; } - static defaultProps = { - onChange: () => {}, - }; + componentWillUnmount() { this.destory(); } showResizeProxy = () => { - this.props.resizeProxyDomRef.style.cssText = `display:block;left:${this.startLeft}px;`; + this.props.resizeProxyDomRef!.style.cssText = `display:block;left:${this.startLeft}px;`; }; moveResizeProxy = () => { const moveLeft = this.startLeft + this.changedPageX; - this.props.resizeProxyDomRef.style.cssText = `left:${moveLeft}px;display:block;`; + this.props.resizeProxyDomRef!.style.cssText = `left:${moveLeft}px;display:block;`; }; resetResizeProxy = () => { // when the mouse was not moved,don't change cell width @@ -50,7 +63,7 @@ class Resize extends React.Component { this.changedPageX = 0; this.tRight = 0; this.asyncResizeFlag = false; - this.props.resizeProxyDomRef.style.cssText = `display:none;`; + this.props.resizeProxyDomRef!.style.cssText = `display:none;`; }; movingLimit = () => { // table right limit @@ -70,12 +83,14 @@ class Resize extends React.Component { this.changedPageX = 0 - this.startLeft; } + // @ts-expect-error width 在这里不能是 string,否则计算会有问题 if (this.props.col.width + this.changedPageX < this.cellMinWidth) { + // @ts-expect-error width 在这里不能是 string,否则计算会有问题 this.changedPageX = this.cellMinWidth - this.props.col.width; } }; - onMouseDown = e => { - const { left: tableLeft, width: tableWidth } = this.props.tableEl.getBoundingClientRect(); + onMouseDown = (e: MouseEvent) => { + const { left: tableLeft, width: tableWidth } = this.props.tableEl!.getBoundingClientRect(); if (!this.props.cellDomRef || !this.props.cellDomRef.current) { return; } @@ -91,7 +106,7 @@ class Resize extends React.Component { events.on(document, 'mouseup', this.onMouseUp); this.unSelect(); }; - onMouseMove = e => { + onMouseMove = (e: MouseEvent) => { const pageX = e.pageX; this.changedPageX = pageX - this.lastPageX; @@ -102,7 +117,8 @@ class Resize extends React.Component { if (this.props.hasLock) { if (!this.props.asyncResizable) { // when hasn't lock attribute, cellLeft will change - this.cellLeft = this.props.cellDomRef.current.getBoundingClientRect().left - this.tLeft; + this.cellLeft = + this.props.cellDomRef.current!.getBoundingClientRect().left - this.tLeft; } } this.movingLimit(); diff --git a/components/table/base/row.jsx b/components/table/base/row.tsx similarity index 71% rename from components/table/base/row.jsx rename to components/table/base/row.tsx index 4f9ee034e9..ac69b7e1ba 100644 --- a/components/table/base/row.jsx +++ b/components/table/base/row.tsx @@ -1,13 +1,16 @@ -import React from 'react'; +import React, { type ReactNode, type MouseEvent } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { obj, dom } from '../../util'; +import { obj, dom, type ClassPropsWithDefault } from '../../util'; import { fetchDataByPath } from '../util'; +import type { CellLike, RecordItem, RowProps } from '../types'; const noop = () => {}; -export default class Row extends React.Component { +type InnerRowProps = ClassPropsWithDefault; + +export default class Row extends React.Component { static propTypes = { prefix: PropTypes.string, pure: PropTypes.bool, @@ -38,8 +41,7 @@ export default class Row extends React.Component { onMouseEnter: noop, onMouseLeave: noop, cellRef: noop, - colGroup: {}, - wrapper: row => row, + wrapper: (row: ReactNode) => row, }; static contextTypes = { @@ -47,7 +49,13 @@ export default class Row extends React.Component { lockType: PropTypes.oneOf(['left', 'right']), }; - shouldComponentUpdate(nextProps) { + readonly props: InnerRowProps; + readonly context: { + notRenderCellIndex: number[]; + lockType: 'left' | 'right'; + }; + + shouldComponentUpdate(nextProps: InnerRowProps) { if (nextProps.pure) { const isEqual = obj.shallowEqual(this.props, nextProps); return !isEqual; @@ -56,26 +64,26 @@ export default class Row extends React.Component { return true; } - onClick = e => { + onClick = (e: MouseEvent) => { const { record, rowIndex } = this.props; this.props.onClick(record, rowIndex, e); }; - onMouseEnter = e => { + onMouseEnter = (e: MouseEvent) => { const { record, rowIndex, __rowIndex } = this.props; const row = __rowIndex || rowIndex; this.onRowHover(record, row, true, e); }; - onMouseLeave = e => { + onMouseLeave = (e: MouseEvent) => { const { record, rowIndex, __rowIndex } = this.props; const row = __rowIndex || rowIndex; this.onRowHover(record, row, false, e); }; - onRowHover(record, index, isEnter, e) { + onRowHover(record: RecordItem, index: number, isEnter: boolean, e: MouseEvent) { const { onMouseEnter, onMouseLeave } = this.props, - currentRow = findDOMNode(this); + currentRow = findDOMNode(this) as Element; if (isEnter) { onMouseEnter(record, index, e); currentRow && dom.addClass(currentRow, 'hovered'); @@ -85,7 +93,7 @@ export default class Row extends React.Component { } } - renderCells(record, rowIndex) { + renderCells(record: RecordItem, rowIndex?: number) { const { Cell, columns, @@ -93,7 +101,7 @@ export default class Row extends React.Component { cellRef, prefix, primaryKey, - // __rowIndex 是连贯的table行的索引,只有在开启expandedIndexSimulate的ExpandedTable模式下__rowIndex可能会不等于rowIndex + // __rowIndex 是连贯的 table 行的索引,只有在开启 expandedIndexSimulate 的 ExpandedTable 模式下__rowIndex 可能会不等于 rowIndex __rowIndex, pure, locale, @@ -105,14 +113,23 @@ export default class Row extends React.Component { const { lockType } = this.context; return columns.map((child, index) => { - /* eslint-disable no-unused-vars, prefer-const */ - const { dataIndex, align, alignHeader, width, colSpan, style, cellStyle, __colIndex, ...others } = child; + const { + dataIndex, + align, + alignHeader, + width, + colSpan, + style, + cellStyle, + __colIndex, + ...others + } = child; const colIndex = '__colIndex' in child ? __colIndex : index; // colSpan should show in body td by the way of // tbody's cell merge should only by the way of const value = fetchDataByPath(record, dataIndex); - const attrs = getCellProps(rowIndex, colIndex, dataIndex, record) || {}; + const attrs = getCellProps(rowIndex!, colIndex!, dataIndex!, record) || {}; if (this.context.notRenderCellIndex) { const matchCellIndex = this.context.notRenderCellIndex @@ -123,8 +140,16 @@ export default class Row extends React.Component { return null; } } - if ((attrs.colSpan && attrs.colSpan > 1) || (attrs.rowSpan && attrs.rowSpan > 1)) { - this._getNotRenderCellIndex(colIndex, rowIndex, attrs.colSpan || 1, attrs.rowSpan || 1); + if ( + (attrs.colSpan && (attrs.colSpan as number) > 1) || + (attrs.rowSpan && attrs.rowSpan > 1) + ) { + this._getNotRenderCellIndex( + colIndex!, + rowIndex!, + (attrs.colSpan as number) || 1, + attrs.rowSpan || 1 + ); } const cellClass = attrs.className; @@ -132,9 +157,10 @@ export default class Row extends React.Component { first: lockType !== 'right' && colIndex === 0, last: lockType !== 'left' && - (colIndex === columns.length - 1 || colIndex + attrs.colSpan === columns.length), // 考虑合并单元格的情况 - [child.className]: child.className, - [cellClass]: cellClass, + (colIndex === columns.length - 1 || + colIndex! + (attrs.colSpan as number) === columns.length), // 考虑合并单元格的情况 + [child.className!]: child.className, + [cellClass!]: cellClass, }); const newStyle = { ...attrs.style, ...cellStyle }; @@ -147,7 +173,9 @@ export default class Row extends React.Component { style={newStyle} data-next-table-col={colIndex} data-next-table-row={rowIndex} - ref={cell => cellRef(__rowIndex, colIndex, cell)} + ref={(cell: InstanceType | null) => + cellRef(__rowIndex, colIndex!, cell) + } prefix={prefix} pure={pure} primaryKey={primaryKey} @@ -165,7 +193,7 @@ export default class Row extends React.Component { }); } - _getNotRenderCellIndex(colIndex, rowIndex, colSpan, rowSpan) { + _getNotRenderCellIndex(colIndex: number, rowIndex: number, colSpan: number, rowSpan: number) { const maxColIndex = colSpan; const maxRowIndex = rowSpan; const notRenderCellIndex = []; @@ -178,7 +206,6 @@ export default class Row extends React.Component { } render() { - /* eslint-disable no-unused-vars*/ const { prefix, className, @@ -205,7 +232,7 @@ export default class Row extends React.Component { } = this.props; const cls = classnames({ [`${prefix}table-row`]: true, - [className]: className, + [className!]: className, }); const tr = ( diff --git a/components/table/base/sort.jsx b/components/table/base/sort.tsx similarity index 59% rename from components/table/base/sort.jsx rename to components/table/base/sort.tsx index ceee321ec4..f217c1dd4b 100644 --- a/components/table/base/sort.jsx +++ b/components/table/base/sort.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { type KeyboardEvent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Icon from '../../icon'; import { KEYCODE } from '../../util'; +import type { SortOrder, SortProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class Sort extends React.Component { +export default class Sort extends React.Component { static propTypes = { prefix: PropTypes.string, rtl: PropTypes.bool, @@ -23,31 +23,37 @@ export default class Sort extends React.Component { }; // 渲染排序 renderSort() { - const { prefix, sort, sortIcons, className, dataIndex, locale, sortDirections, rtl } = this.props, - sortStatus = sort[dataIndex], + const { prefix, sort, sortIcons, className, dataIndex, locale, sortDirections, rtl } = + this.props, + sortStatus = sort![dataIndex!], map = { desc: 'descending', asc: 'ascending', }; - const icons = sortDirections.map(sortOrder => { + const icons = sortDirections!.map(sortOrder => { return sortOrder === 'default' ? null : ( - {sortIcons ? sortIcons[sortOrder] : } + {sortIcons ? ( + sortIcons[sortOrder] + ) : ( + + )} ); }); const cls = classnames({ [`${prefix}table-sort`]: true, - [className]: className, + [className!]: className, }); return ( { const { sort, dataIndex, sortDirections } = this.props; - let nextSortType = ''; + let nextSortType: SortOrder; - sortDirections.forEach((dir, i) => { - if (sort[dataIndex] === dir) { - nextSortType = sortDirections.length - 1 > i ? sortDirections[i + 1] : sortDirections[0]; + sortDirections!.forEach((dir, i) => { + if (sort![dataIndex!] === dir) { + nextSortType = + sortDirections!.length - 1 > i ? sortDirections![i + 1] : sortDirections![0]; } }); - if (!sort[dataIndex]) { - nextSortType = sortDirections[0]; + if (!sort![dataIndex!]) { + nextSortType = sortDirections![0]; } - this.onSort(dataIndex, nextSortType); + this.onSort(dataIndex!, nextSortType!); }; - keydownHandler = e => { + keydownHandler = (e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); @@ -84,8 +91,8 @@ export default class Sort extends React.Component { } }; - onSort = (dataIndex, order) => { - const sort = {}; + onSort = (dataIndex: string, order: SortOrder) => { + const sort: SortProps['sort'] = {}; sort[dataIndex] = order; this.props.onSort(dataIndex, order, sort); diff --git a/components/table/base/wrapper.jsx b/components/table/base/wrapper.jsx deleted file mode 100644 index 383ca34498..0000000000 --- a/components/table/base/wrapper.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -/* eslint-disable react/prefer-stateless-function */ -export default class Wrapper extends Component { - static propTypes = { - tableWidth: PropTypes.number, - }; - - render() { - const { colGroup, children, tableWidth, component: Tag } = this.props; - return ( - - {colGroup} - {children} - - ); - } -} - -Wrapper.defaultProps = { - component: 'table', -}; - -Wrapper.propTypes = { - children: PropTypes.any, - prefix: PropTypes.string, - colGroup: PropTypes.any, - component: PropTypes.string, -}; diff --git a/components/table/base/wrapper.tsx b/components/table/base/wrapper.tsx new file mode 100644 index 0000000000..33181288b8 --- /dev/null +++ b/components/table/base/wrapper.tsx @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import type { WrapperProps } from '../types'; +import { type ClassPropsWithDefault } from '../../util'; + +type InnerWrapperProps = ClassPropsWithDefault; + +export default class Wrapper extends Component { + static propTypes = { + tableWidth: PropTypes.number, + children: PropTypes.any, + prefix: PropTypes.string, + colGroup: PropTypes.any, + component: PropTypes.string, + }; + + static defaultProps = { + component: 'table', + }; + + readonly props: InnerWrapperProps; + + render() { + const { colGroup, children, tableWidth, component: Tag } = this.props; + return ( + + {colGroup} + {children} + + ); + } +} diff --git a/components/table/column-group.jsx b/components/table/column-group.tsx similarity index 73% rename from components/table/column-group.jsx rename to components/table/column-group.tsx index 4572152db3..a11fb77868 100644 --- a/components/table/column-group.jsx +++ b/components/table/column-group.tsx @@ -1,11 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import type { ColumnGroupProps } from './types'; -/** - * Table.ColumnGroup - * @order 1 - **/ -export default class ColumnGroup extends React.Component { +export default class ColumnGroup extends React.Component { static propTypes = { /** * 表头显示的内容 @@ -25,7 +22,7 @@ export default class ColumnGroup extends React.Component { getChildContext() { return { - parent: this, + parent: this as InstanceType, }; } diff --git a/components/table/column.jsx b/components/table/column.jsx deleted file mode 100644 index c95079459b..0000000000 --- a/components/table/column.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -/** - * Table.Column - * @order 0 - **/ -export default class Column extends React.Component { - static propTypes = { - /** - * 指定列对应的字段,支持`a.b`形式的快速取值 - */ - dataIndex: PropTypes.string, - /** - * 行渲染的逻辑 - * value, rowIndex, record, context四个属性只可读不可被更改 - * Function(value, index, record) => Element - */ - cell: PropTypes.oneOfType([PropTypes.element, PropTypes.node, PropTypes.func]), - /** - * 表头显示的内容 - */ - title: PropTypes.oneOfType([PropTypes.element, PropTypes.node, PropTypes.func]), - /** - * 写到 header 单元格上的title属性 - */ - htmlTitle: PropTypes.string, - /** - * 是否支持排序 - */ - sortable: PropTypes.bool, - /** - * 排序的方向。 - * 设置 ['desc', 'asc'],表示降序、升序 - * 设置 ['desc', 'asc', 'default'],表示表示降序、升序、不排序 - * @version 1.23 - */ - sortDirections: PropTypes.arrayOf(PropTypes.oneOf(['desc', 'asc', 'default'])), - /** - * 列宽,注意在锁列的情况下一定需要配置宽度 - */ - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * 单元格的对齐方式 - */ - align: PropTypes.oneOf(['left', 'center', 'right']), - /** - * 单元格标题的对齐方式, 不配置默认读取align值 - */ - alignHeader: PropTypes.oneOf(['left', 'center', 'right']), - /** - * 生成标题过滤的菜单, 格式为`[{label:'xxx', value:'xxx'}]` - */ - filters: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - }) - ), - /** - * 过滤的模式是单选还是多选 - */ - filterMode: PropTypes.oneOf(['single', 'multiple']), - /** - * filter 模式下传递给 Menu 菜单的属性, 默认继承 `Menu` 组件的API - * @property {Boolean} subMenuSelectable 默认为`false` subMenu是否可选择 - * @property {Boolean} isSelectIconRight 默认为`false` 是否将选中图标居右。注意:SubMenu 上的选中图标一直居左,不受此API控制 - */ - filterMenuProps: PropTypes.object, - filterProps: PropTypes.object, - /** - * 是否支持锁列,可选值为`left`,`right`, `true` - */ - lock: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - /** - * 是否支持列宽调整, 当该值设为true,table的布局方式会修改为fixed. - */ - resizable: PropTypes.bool, - /** - * (推荐使用)是否支持异步列宽调整, 当该值设为true,table的布局方式会修改为fixed. - * @version 1.24 - */ - asyncResizable: PropTypes.bool, - /** - * header cell 横跨的格数,设置为0表示不出现此 th - */ - colSpan: PropTypes.number, - /** - * 设置该列单元格的word-break样式,对于id类、中文类适合用all,对于英文句子适合用word - * @enumdesc all, word - * @default all - * @version 1.23 - */ - wordBreak: PropTypes.oneOf(['all', 'word']), - }; - - static contextTypes = { - parent: PropTypes.any, - }; - - static defaultProps = { - cell: value => value, - filterMode: 'multiple', - filterMenuProps: { - subMenuSelectable: false, - }, - filterProps: {}, - resizable: false, - asyncResizable: false, - }; - - static _typeMark = 'column'; - - render() { - return null; - } -} diff --git a/components/table/column.tsx b/components/table/column.tsx new file mode 100644 index 0000000000..ff156f9f06 --- /dev/null +++ b/components/table/column.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import type { ColumnProps, InnerColumnProps } from './types'; + +export default class Column extends React.Component { + static propTypes = { + dataIndex: PropTypes.string, + cell: PropTypes.oneOfType([PropTypes.element, PropTypes.node, PropTypes.func]), + title: PropTypes.oneOfType([PropTypes.element, PropTypes.node, PropTypes.func]), + htmlTitle: PropTypes.string, + sortable: PropTypes.bool, + sortDirections: PropTypes.arrayOf(PropTypes.oneOf(['desc', 'asc', 'default'])), + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + align: PropTypes.oneOf(['left', 'center', 'right']), + alignHeader: PropTypes.oneOf(['left', 'center', 'right']), + filters: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), + }) + ), + filterMode: PropTypes.oneOf(['single', 'multiple']), + filterMenuProps: PropTypes.object, + filterProps: PropTypes.object, + lock: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + resizable: PropTypes.bool, + asyncResizable: PropTypes.bool, + colSpan: PropTypes.number, + wordBreak: PropTypes.oneOf(['all', 'word']), + }; + + static contextTypes = { + parent: PropTypes.any, + }; + + static defaultProps = { + cell: (value: unknown) => value, + filterMode: 'multiple', + filterMenuProps: { + subMenuSelectable: false, + }, + filterProps: {}, + resizable: false, + asyncResizable: false, + }; + + static _typeMark = 'column'; + + render() { + return null; + } +} diff --git a/components/table/expanded.jsx b/components/table/expanded.tsx similarity index 68% rename from components/table/expanded.jsx rename to components/table/expanded.tsx index 4d2b2debd0..1c8fa8809a 100644 --- a/components/table/expanded.jsx +++ b/components/table/expanded.tsx @@ -1,4 +1,4 @@ -import React, { Children } from 'react'; +import React, { Children, type ReactElement, type KeyboardEvent, type UIEvent } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -8,60 +8,16 @@ import { KEYCODE, dom, events } from '../util'; import RowComponent from './expanded/row'; import Col from './column'; import { statics } from './util'; +import type Base from './base'; +import type { ExpandedTableProps, RecordItem, RowLike } from './types'; const noop = () => {}; -export default function expanded(BaseComponent, stickyLock) { +export default function expanded(BaseComponent: typeof Base, stickyLock?: boolean) { /** Table */ - class ExpandedTable extends React.Component { + class ExpandedTable extends React.Component { static ExpandedRow = RowComponent; static propTypes = { - /** - * 额外渲染行的渲染函数 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @returns {Element} - */ - expandedRowRender: PropTypes.func, - /** - * 设置行是否可展开,设置 false 为不可展开 - * @param {Object} record 该行所对应的数据 - * @param {Number} index 该行所对应的序列 - * @returns {Boolean} 是否可展开 - * @version 1.21 - */ - rowExpandable: PropTypes.func, - /** - * 额外渲染行的缩进 - */ - expandedRowIndent: PropTypes.array, - /** - * 默认情况下展开的渲染行或者Tree, 传入此属性为受控状态 - */ - openRowKeys: PropTypes.array, - /** - * 默认情况下展开的 Expand行 或者 Tree行,非受控模式 - * @version 1.23.22 - */ - defaultOpenRowKeys: PropTypes.array, - /** - * 是否显示点击展开额外渲染行的+号按钮 - */ - hasExpandedRowCtrl: PropTypes.bool, - /** - * 设置额外渲染行的属性 - */ - getExpandedColProps: PropTypes.func, - /** - * 在额外渲染行或者Tree展开或者收起的时候触发的事件 - * @param {Array} openRowKeys 展开的渲染行的key - * @param {String} currentRowKey 当前点击的渲染行的key - * @param {Boolean} expanded 当前点击是展开还是收起 - * @param {Object} currentRecord 当前点击额外渲染行的记录 - */ - onRowOpen: PropTypes.func, - onExpandedRowClick: PropTypes.func, - locale: PropTypes.object, ...BaseComponent.propTypes, }; @@ -88,6 +44,8 @@ export default function expanded(BaseComponent, stickyLock) { state = { openRowKeys: this.props.openRowKeys || this.props.defaultOpenRowKeys || [], }; + expandedRowRefs: Record; + tableInc: InstanceType | null; getChildContext() { return { @@ -101,7 +59,7 @@ export default function expanded(BaseComponent, stickyLock) { }; } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: ExpandedTableProps) { if ('openRowKeys' in nextProps) { return { openRowKeys: nextProps.openRowKeys || [], @@ -124,7 +82,7 @@ export default function expanded(BaseComponent, stickyLock) { events.off(window, 'resize', this.setExpandedWidth); } - saveExpandedRowRef = (key, rowRef) => { + saveExpandedRowRef = (key: string, rowRef: HTMLElement) => { if (!this.expandedRowRefs) { this.expandedRowRefs = {}; } @@ -134,15 +92,17 @@ export default function expanded(BaseComponent, stickyLock) { setExpandedWidth = () => { const { prefix } = this.props; const tableEl = this.getTableNode(); - const totalWidth = +(tableEl && tableEl.clientWidth) - 1 || '100%'; + const totalWidth = +(tableEl! && tableEl.clientWidth) - 1 || '100%'; const bodyNode = tableEl && tableEl.querySelector(`.${prefix}table-body`); Object.keys(this.expandedRowRefs || {}).forEach(key => { - dom.setStyle(this.expandedRowRefs[key], { width: (bodyNode && bodyNode.clientWidth) || totalWidth }); + dom.setStyle(this.expandedRowRefs[key], { + width: (bodyNode && bodyNode.clientWidth) || totalWidth, + }); }); }; - getTableInstance = instance => { + getTableInstance = (instance: InstanceType | null) => { this.tableInc = instance; }; @@ -152,13 +112,13 @@ export default function expanded(BaseComponent, stickyLock) { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.tableEl); + return findDOMNode(table!.tableEl) as HTMLElement; } catch (error) { return null; } } - expandedKeydown = (value, record, index, e) => { + expandedKeydown = (value: unknown, record: RecordItem, index: number, e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); @@ -167,7 +127,7 @@ export default function expanded(BaseComponent, stickyLock) { } }; - renderExpandedCell = (value, index, record) => { + renderExpandedCell = (value: unknown, index: number, record: RecordItem) => { const { getExpandedColProps, prefix, locale, rowExpandable } = this.props; if (typeof rowExpandable === 'function' && !rowExpandable(record, index)) { @@ -176,17 +136,17 @@ export default function expanded(BaseComponent, stickyLock) { const { openRowKeys } = this.state, { primaryKey } = this.props, - hasExpanded = openRowKeys.indexOf(record[primaryKey]) > -1, + hasExpanded = openRowKeys.indexOf(record[primaryKey!] as string | number) > -1, switchNode = hasExpanded ? ( ) : ( ), - attrs = getExpandedColProps(record, index) || {}; + attrs = getExpandedColProps!(record, index) || {}; const cls = classnames({ [`${prefix}table-expanded-ctrl`]: true, disabled: attrs.disabled, - [attrs.className]: attrs.className, + [attrs.className!]: attrs.className, }); if (!attrs.disabled) { @@ -196,9 +156,9 @@ export default function expanded(BaseComponent, stickyLock) { @@ -207,10 +167,10 @@ export default function expanded(BaseComponent, stickyLock) { ); }; - onExpandedClick(value, record, i, e) { + onExpandedClick(value: unknown, record: RecordItem, i: number, e: UIEvent) { const openRowKeys = [...this.state.openRowKeys], { primaryKey } = this.props, - id = record[primaryKey], + id = record[primaryKey!] as string | number, index = openRowKeys.indexOf(id); if (index > -1) { openRowKeys.splice(index, 1); @@ -222,11 +182,11 @@ export default function expanded(BaseComponent, stickyLock) { openRowKeys: openRowKeys, }); } - this.props.onRowOpen(openRowKeys, id, index === -1, record); + this.props.onRowOpen!(openRowKeys, id, index === -1, record); e.stopPropagation(); } - addExpandCtrl = columns => { + addExpandCtrl = (columns: NonNullable) => { const { prefix, size } = this.props; if (!columns.find(record => record.key === 'expanded')) { @@ -241,14 +201,14 @@ export default function expanded(BaseComponent, stickyLock) { } }; - normalizeChildren(children) { + normalizeChildren(children: ExpandedTableProps['children']) { const { prefix, size } = this.props; const toArrayChildren = Children.map(children, (child, index) => - React.cloneElement(child, { + React.cloneElement(child as ReactElement, { key: index, }) ); - toArrayChildren.unshift( + toArrayChildren!.unshift( ) { + const ret: NonNullable = []; ds.forEach(item => { const itemCopy = { ...item }; itemCopy.__expanded = true; @@ -272,17 +232,12 @@ export default function expanded(BaseComponent, stickyLock) { } render() { - /* eslint-disable no-unused-vars, prefer-const */ - let { - components, + const { openRowKeys, expandedRowRender, rowExpandable, hasExpandedRowCtrl, - children, columns, - dataSource, - entireDataSource, getExpandedColProps, expandedRowIndent, onRowOpen, @@ -290,14 +245,16 @@ export default function expanded(BaseComponent, stickyLock) { ...others } = this.props; - if (expandedRowRender && !components.Row) { + let { components, dataSource, entireDataSource, children } = this.props; + + if (expandedRowRender && !components!.Row) { components = { ...components }; - components.Row = RowComponent; - dataSource = this.normalizeDataSource(dataSource); + components.Row = RowComponent as RowLike; + dataSource = this.normalizeDataSource(dataSource!); entireDataSource = this.normalizeDataSource(entireDataSource); } if (expandedRowRender && hasExpandedRowCtrl) { - let useColumns = columns && !children; + const useColumns = columns && !children; if (useColumns) { this.addExpandCtrl(columns); @@ -319,6 +276,5 @@ export default function expanded(BaseComponent, stickyLock) { ); } } - statics(ExpandedTable, BaseComponent); - return polyfill(ExpandedTable); + return polyfill(statics(ExpandedTable, BaseComponent)); } diff --git a/components/table/expanded/row.jsx b/components/table/expanded/row.tsx similarity index 68% rename from components/table/expanded/row.jsx rename to components/table/expanded/row.tsx index 718e067da7..3158537748 100644 --- a/components/table/expanded/row.jsx +++ b/components/table/expanded/row.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { type CSSProperties, type ReactNode } from 'react'; import PropTypes from 'prop-types'; import { log } from '../../util'; import Row from '../lock/row'; +import type { ExpandedRowProps, RecordItem } from '../types'; -export default class ExpandedRow extends React.Component { +export default class ExpandedRow extends React.Component { static propTypes = { ...Row.propTypes, }; @@ -22,12 +23,22 @@ export default class ExpandedRow extends React.Component { getExpandedRowRef: PropTypes.func, }; - getExpandedRow = (parentKey, ref) => { + readonly context: { + openRowKeys: string[]; + expandedRowRender: (record: RecordItem, index: number) => ReactNode; + expandedRowIndent: [number, number]; + expandedIndexSimulate: boolean; + expandedRowWidthEquals2Table: boolean; + lockType: 'left' | 'right'; + getExpandedRowRef: (parentKey: unknown, ref: HTMLDivElement | null) => void; + }; + + getExpandedRow = (parentKey: unknown, ref: HTMLDivElement | null) => { const { getExpandedRowRef } = this.context; getExpandedRowRef && getExpandedRowRef(parentKey, ref); }; - renderExpandedRow(record, rowIndex) { + renderExpandedRow(record: RecordItem, rowIndex: number) { const { expandedRowRender, expandedRowIndent, @@ -47,7 +58,7 @@ export default class ExpandedRow extends React.Component { leftIndent = expandedRowIndent[0], rightIndent = expandedRowIndent[1], totalIndent = leftIndent + rightIndent, - renderCols = (number, start = 0) => { + renderCols = (number: number, start = 0) => { const ret = []; for (let i = 0; i < number; i++) { ret.push( @@ -61,16 +72,20 @@ export default class ExpandedRow extends React.Component { let content; if (totalIndent > colSpan && !lockType) { - log.warning("It's not allowed expandedRowIndent is more than the number of columns."); + log.warning( + "It's not allowed expandedRowIndent is more than the number of columns." + ); } if (leftIndent < columns.length && lockType === 'left') { log.warning('expandedRowIndent left is less than the number of left lock columns.'); } if (rightIndent < columns.length && lockType === 'right') { - log.warning('expandedRowIndent right is less than the number of right lock columns.'); + log.warning( + 'expandedRowIndent right is less than the number of right lock columns.' + ); } if (lockType) { - return openRowKeys.indexOf(record[primaryKey]) > -1 ? ( + return openRowKeys.indexOf(record[primaryKey!] as string) > -1 ? ( + return openRowKeys.indexOf(record[primaryKey!] as string) > -1 ? ( + {renderCols(leftIndent)} {renderCols(rightIndent, rightStart)} @@ -126,15 +145,22 @@ export default class ExpandedRow extends React.Component { } render() { - /* eslint-disable no-unused-vars*/ const { record, rowIndex, columns, ...others } = this.props; const { expandedIndexSimulate } = this.context; if (record.__expanded) { - return this.renderExpandedRow(record, rowIndex, columns); + return this.renderExpandedRow(record, rowIndex); } const newRowIndex = expandedIndexSimulate ? rowIndex / 2 : rowIndex; - return ; + return ( + + ); } } diff --git a/components/table/fixed.jsx b/components/table/fixed.tsx similarity index 76% rename from components/table/fixed.jsx rename to components/table/fixed.tsx index 2f760f3055..b00ac3c1ba 100644 --- a/components/table/fixed.jsx +++ b/components/table/fixed.tsx @@ -7,26 +7,16 @@ import HeaderComponent from './fixed/header'; import BodyComponent from './fixed/body'; import WrapperComponent from './fixed/wrapper'; import { statics } from './util'; +import type Base from './base'; +import type { FixedTableProps, WrapperLike } from './types'; -export default function fixed(BaseComponent, stickyLock) { +export default function fixed(BaseComponent: typeof Base) { /** Table */ - class FixedTable extends React.Component { + class FixedTable extends React.Component { static FixedHeader = HeaderComponent; static FixedBody = BodyComponent; static FixedWrapper = WrapperComponent; static propTypes = { - /** - * 是否具有表头 - */ - hasHeader: PropTypes.bool, - /** - * 表头是否固定,该属性配合maxBodyHeight使用,当内容区域的高度超过maxBodyHeight的时候,在内容区域会出现滚动条 - */ - fixedHeader: PropTypes.bool, - /** - * 最大内容区域的高度,在`fixedHeader`为`true`的时候,超过这个高度会出现滚动条 - */ - maxBodyHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), ...BaseComponent.propTypes, }; @@ -49,6 +39,9 @@ export default function fixed(BaseComponent, stickyLock) { }; state = {}; + scrollToRightEnd?: boolean; + scrollTarget: HTMLElement | null; + timeoutId?: number; getChildContext() { return { @@ -71,14 +64,17 @@ export default function fixed(BaseComponent, stickyLock) { this.onFixedScrollSync({ currentTarget: this.bodyNode, target: this.bodyNode }); } - getNode = (type, node, lockType) => { + [nodeKey: `${string}${'header' | 'body'}${'Left' | 'Right' | ''}Node`]: HTMLElement; + [tableKey: `table${string}Inc`]: InstanceType; + + getNode = (type: 'header' | 'body', node: HTMLElement, lockType?: string) => { lockType = lockType ? lockType.charAt(0).toUpperCase() + lockType.substr(1) : ''; - this[`${type}${lockType}Node`] = node; + this[`${type}${lockType as 'Left' | 'Right'}Node` as const] = node; }; - getTableInstance = (type, instance) => { + getTableInstance = (type: string, instance: InstanceType) => { type = ''; - this[`table${type}Inc`] = instance; + this[`table${type}Inc` as const] = instance; }; getTableNode() { @@ -87,20 +83,22 @@ export default function fixed(BaseComponent, stickyLock) { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.tableEl); + return findDOMNode(table.tableEl) as HTMLElement; } catch (error) { return null; } } // for fixed header scroll left - onFixedScrollSync = (current = { currentTarget: {} }) => { - const currentTarget = current.currentTarget || {}, + onFixedScrollSync = ( + current: { currentTarget?: HTMLElement; target?: HTMLElement } = {} + ) => { + const currentTarget = current.currentTarget || ({} as HTMLElement), headerNode = this.headerNode, bodyNode = this.bodyNode; const { scrollLeft, scrollWidth, clientWidth } = currentTarget; - const scrollToRightEnd = !(scrollLeft < scrollWidth - clientWidth); + const scrollToRightEnd = !(scrollLeft! < scrollWidth! - clientWidth!); const { prefix, loading } = this.props; if (!loading && scrollToRightEnd !== this.scrollToRightEnd) { @@ -143,7 +141,12 @@ export default function fixed(BaseComponent, stickyLock) { if (hasHeader && !this.props.lockType && body) { const hasVerScroll = body.scrollHeight > body.clientHeight, hasHozScroll = body.scrollWidth > body.clientWidth; - const style = {}; + const style: Partial< + Record< + typeof paddingName | typeof marginName | 'marginBottom' | 'paddingBottom', + unknown + > + > = {}; if (!hasVerScroll) { style[paddingName] = 0; style[marginName] = 0; @@ -165,9 +168,11 @@ export default function fixed(BaseComponent, stickyLock) { } if (hasHeader && !this.props.lockType && this.headerNode) { - const fixer = this.headerNode.querySelector(`.${prefix}table-header-fixer`); - const height = dom.getStyle(this.headerNode, 'height'); - const paddingBottom = dom.getStyle(this.headerNode, 'paddingBottom'); + const fixer = this.headerNode.querySelector( + `.${prefix}table-header-fixer` + ) as HTMLElement; + const height = dom.getStyle(this.headerNode, 'height') as number; + const paddingBottom = dom.getStyle(this.headerNode, 'paddingBottom') as number; dom.setStyle(fixer, { width: scrollBarSize, @@ -177,17 +182,9 @@ export default function fixed(BaseComponent, stickyLock) { } render() { - /* eslint-disable no-unused-vars, prefer-const */ - let { - components, - className, - prefix, - fixedHeader, - lockType, - dataSource, - maxBodyHeight, - ...others - } = this.props; + const { prefix, fixedHeader, lockType, dataSource, maxBodyHeight, ...others } = + this.props; + let { className, components } = this.props; if (fixedHeader) { components = { ...components }; if (!components.Header) { @@ -197,12 +194,12 @@ export default function fixed(BaseComponent, stickyLock) { components.Body = BodyComponent; } if (!components.Wrapper) { - components.Wrapper = WrapperComponent; + components.Wrapper = WrapperComponent as WrapperLike; } className = classnames({ [`${prefix}table-fixed`]: true, - [`${prefix}table-wrap-empty`]: !dataSource.length, - [className]: className, + [`${prefix}table-wrap-empty`]: !dataSource!.length, + [className!]: className, }); } @@ -218,6 +215,5 @@ export default function fixed(BaseComponent, stickyLock) { ); } } - statics(FixedTable, BaseComponent); - return FixedTable; + return statics(FixedTable, BaseComponent); } diff --git a/components/table/fixed/body.jsx b/components/table/fixed/body.tsx similarity index 80% rename from components/table/fixed/body.jsx rename to components/table/fixed/body.tsx index 585b77a75a..7ced2f96aa 100644 --- a/components/table/fixed/body.jsx +++ b/components/table/fixed/body.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { type CSSProperties, type UIEvent } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import BodyComponent from '../base/body'; +import type { FixedBodyContext, FixedBodyProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class FixedBody extends React.Component { +export default class FixedBody extends React.Component { static propTypes = { children: PropTypes.any, prefix: PropTypes.string, @@ -21,12 +21,14 @@ export default class FixedBody extends React.Component { getNode: PropTypes.func, }; + context: FixedBodyContext; + componentDidMount() { const { getNode } = this.context; - getNode && getNode('body', findDOMNode(this)); + getNode && getNode('body', findDOMNode(this) as HTMLElement); } - onBodyScroll = event => { + onBodyScroll = (event: UIEvent) => { const { onFixedScrollSync } = this.context; // sync scroll left to header onFixedScrollSync && onFixedScrollSync(event); @@ -38,10 +40,9 @@ export default class FixedBody extends React.Component { }; render() { - /*eslint-disable no-unused-vars */ const { className, colGroup, onLockScroll, tableWidth, ...others } = this.props; const { maxBodyHeight, fixedHeader } = this.context; - const style = {}; + const style: CSSProperties = {}; if (fixedHeader) { style.maxHeight = maxBodyHeight; style.position = 'relative'; diff --git a/components/table/fixed/header.jsx b/components/table/fixed/header.tsx similarity index 70% rename from components/table/fixed/header.jsx rename to components/table/fixed/header.tsx index 14d6875d47..f2b1559121 100644 --- a/components/table/fixed/header.jsx +++ b/components/table/fixed/header.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import HeaderComponent from '../base/header'; +import type { FixedHeaderContext, FixedHeaderProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class FixedHeader extends React.Component { +export default class FixedHeader extends React.Component { static propTypes = { children: PropTypes.any, prefix: PropTypes.string, @@ -19,12 +19,14 @@ export default class FixedHeader extends React.Component { lockType: PropTypes.oneOf(['left', 'right']), }; + context: FixedHeaderContext; + componentDidMount() { - this.context.getNode('header', findDOMNode(this)); + this.context.getNode('header', findDOMNode(this) as HTMLElement); } - // 这里的 style={{overflow: 'unset'}} 可以删掉,只是为了解决用户js升级但是样式没升级的情况 - // 这里的 style={{position: 'absolute', right: 0}} 也可以删掉,是为了兼容用户js升级但是样式没升级的情况 + // 这里的 style={{overflow: 'unset'}} 可以删掉,只是为了解决用户 js 升级但是样式没升级的情况 + // 这里的 style={{position: 'absolute', right: 0}} 也可以删掉,是为了兼容用户 js 升级但是样式没升级的情况 render() { const { prefix, className, colGroup, tableWidth, ...others } = this.props; const { onFixedScrollSync, lockType } = this.context; @@ -38,7 +40,10 @@ export default class FixedHeader extends React.Component {
cellRef(rowIndex, expandedCols, cell)}>   @@ -79,17 +94,18 @@ export default class ExpandedRow extends React.Component { ) : null; } - const expandedRowStyle = { + const expandedRowStyle: CSSProperties = { position: 'sticky', left: 0, }; - // 暴露给用户的index + // 暴露给用户的 index content = expandedRowRender(record, expandedIndex); if (!React.isValidElement(content)) { content = (
{content} @@ -99,7 +115,7 @@ export default class ExpandedRow extends React.Component { content = expandedRowWidthEquals2Table ? (
{content} @@ -113,8 +129,11 @@ export default class ExpandedRow extends React.Component { columns.forEach(col => { col.lock === 'right' && rightStart--; }); - return openRowKeys.indexOf(record[primaryKey]) > -1 ? ( -
{content}
{!lockType && ( -
+
)}
); diff --git a/components/table/fixed/wrapper.jsx b/components/table/fixed/wrapper.tsx similarity index 78% rename from components/table/fixed/wrapper.jsx rename to components/table/fixed/wrapper.tsx index 6974f2df39..e3e154ee5e 100644 --- a/components/table/fixed/wrapper.jsx +++ b/components/table/fixed/wrapper.tsx @@ -1,8 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import type { FixedWrapperProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class FixedWrapper extends React.Component { +export default class FixedWrapper extends React.Component { static propTypes = { children: PropTypes.any, prefix: PropTypes.string, diff --git a/components/table/index.d.ts b/components/table/index.d.ts deleted file mode 100644 index 8f52080322..0000000000 --- a/components/table/index.d.ts +++ /dev/null @@ -1,427 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; -import { LoadingProps } from '../loading'; -import { AffixProps } from '../affix'; - -interface HTMLAttributesWeak extends React.HTMLAttributes { - title?: any; -} - -export interface ColumnProps extends HTMLAttributesWeak, CommonProps { - /** - * 指定列对应的字段,支持`a.b`形式的快速取值 - */ - dataIndex?: string; - - /** - * 行渲染的逻辑 - * value, rowIndex, record, context四个属性只可读不可被更改 - * Function(value, index, record) => Element - */ - cell?: - | React.ReactElement - | React.ReactNode - | ((value: any, index: number, record: any) => any); - - /** - * 表头显示的内容 - * value, rowIndex, record, context四个属性只可读不可被更改 - */ - title?: React.ReactElement | React.ReactNode | (() => any); - - htmlTitle?: string; - /** - * 是否支持排序 - */ - sortable?: boolean; - - /** - * 列宽,注意在锁列的情况下一定需要配置宽度 - */ - width?: number | string; - - /** - * 单元格的对齐方式 - */ - align?: 'left' | 'center' | 'right'; - sortDirections?: Array<'desc' | 'asc' | 'default'>; - /** - * 单元格标题的对齐方式, 不配置默认读取align值 - */ - alignHeader?: 'left' | 'center' | 'right'; - - /** - * 生成标题过滤的菜单, 格式为`[{label:'xxx', value:'xxx'}]` - */ - filters?: Array; - - /** - * 过滤的模式是单选还是多选 - */ - filterMode?: 'single' | 'multiple'; - - /** - * 是否支持锁列,可选值为`left`,`right`, `true` - */ - lock?: boolean | string; - - /** - * 是否支持列宽调整, 当该值设为true,table的布局方式会修改为fixed. - */ - resizable?: boolean; - /** - * 是否支持异步列宽调整, 当该值设为true,table的布局方式会修改为fixed. - */ - asyncResizable?: boolean; - - /** - * header cell 横跨的格数,设置为0表示不出现此 th - */ - colSpan?: number; - - /** - * 设置该列单元格的word-break样式,对于id类、中文类适合用all,对于英文句子适合用word - */ - wordBreak?: 'all' | 'word'; -} - -export class Column extends React.Component {} - -interface HTMLAttributesWeak extends React.HTMLAttributes { - title?: any; -} - -export interface ColumnGroupProps extends HTMLAttributesWeak, CommonProps { - /** - * 表头显示的内容 - */ - title?: React.ReactElement | React.ReactNode | (() => any); -} - -export class ColumnGroup extends React.Component {} - -export interface GroupHeaderProps extends React.HTMLAttributes, CommonProps { - /** - * 行渲染的逻辑 - */ - cell?: - | React.ReactElement - | React.ReactNode - | ((value: any, index: number, record: any) => any); - - /** - * 是否在Children上面渲染selection - */ - hasChildrenSelection?: boolean; - - /** - * 是否在GroupHeader上面渲染selection - */ - hasSelection?: boolean; - - /** - * 当 dataSouce 里没有 children 时,是否使用内容作为数据 - */ - useFirstLevelDataWhenNoChildren?: boolean; -} - -export class GroupHeader extends React.Component {} - -export interface GroupFooterProps extends React.HTMLAttributes, CommonProps { - /** - * 行渲染的逻辑 - */ - cell?: - | React.ReactElement - | React.ReactNode - | ((value: any, index: number, record: any) => any); -} - -export class GroupFooter extends React.Component {} - -export interface BaseTableProps extends React.HTMLAttributes, CommonProps { - /** - * 样式类名的品牌前缀 - */ - prefix?: string; - - /** - * 尺寸 small为紧凑模式 - */ - size?: 'small' | 'medium'; - - /** - * 自定义类名 - */ - className?: string; - - /** - * 自定义内联样式 - */ - style?: React.CSSProperties; - columns?: Array; - /** - * 表格元素的 table-layout 属性,设为 fixed 表示内容不会影响列的布局 - */ - tableLayout?: 'fixed' | 'auto'; - /** - * 表格的总长度,可以这么用:设置表格总长度 、设置部分列的宽度,这样表格会按照剩余空间大小,自动其他列分配宽度 - */ - tableWidth?: number; - /** - * 表格展示的数据源 - */ - dataSource?: Array; - - /** - * 表格是否具有边框 - */ - hasBorder?: boolean; - - /** - * 表格是否具有头部 - */ - hasHeader?: boolean; - - /** - * 表格是否是斑马线 - */ - isZebra?: boolean; - - /** - * 表格是否在加载中 - */ - loading?: boolean; - - /** - * 自定义国际化文案对象 - */ - locale?: { - ok: string; - reset: string; - empty: string; - asc: string; - desc: string; - expanded: string; - folded: string; - filter: string; - selectAll: string; - }; - - /** - * 设置数据为空的时候的表格内容展现 - */ - emptyContent?: React.ReactNode; - - /** - * dataSource当中数据的主键,如果给定的数据源中的属性不包含该主键,会造成选择状态全部选中 - */ - primaryKey?: string; -} -export interface TableProps extends React.HTMLAttributes, BaseTableProps { - /** - * 点击表格每一行触发的事件 - */ - onRowClick?: (record: any, index: number, e: React.MouseEvent) => void; - - /** - * 悬浮在表格每一行的时候触发的事件 - */ - onRowMouseEnter?: (record: any, index: number, e: React.MouseEvent) => void; - - /** - * 离开表格每一行的时候触发的事件 - */ - onRowMouseLeave?: (record: any, index: number, e: React.MouseEvent) => void; - - /** - * 点击列排序触发的事件 - */ - onSort?: (dataIndex: string, order: string) => void; - - /** - * 点击过滤确认按钮触发的事件 - */ - onFilter?: (filterParams: any) => void; - - /** - * 重设列尺寸的时候触发的事件 - */ - onResizeChange?: (dataIndex: string, value: number) => void; - - /** - * 设置每一行的属性,如果返回值和其他针对行操作的属性冲突则无效。 - */ - getRowProps?: (record: any, index: number) => any; - rowProps?: (record: any, index: number) => any; - - /** - * 设置单元格的属性,通过该属性可以进行合并单元格 - */ - getCellProps?: (rowIndex: number, colIndex: number, dataIndex: string, record: any) => any; - cellProps?: (rowIndex: number, colIndex: number, dataIndex: string, record: any) => any; - - /** - * 自定义 Loading 组件 - * 请务必传递 props, 使用方式: loadingComponent={props => } - */ - loadingComponent?: (props: LoadingProps) => React.ReactNode; - - /** - * 当前过滤的的keys,使用此属性可以控制表格的头部的过滤选项中哪个菜单被选中,格式为 {dataIndex: {selectedKeys:[]}} - * 示例: - * 假设要控制dataIndex为id的列的过滤菜单中key为one的菜单项选中 - * `` - */ - filterParams?: { [propName: string]: any }; - - /** - * 当前排序的字段,使用此属性可以控制表格的字段的排序,格式为{dataIndex: 'asc'} - */ - sort?: { [propName: string]: any }; - - /** - * 自定义排序按钮,例如上下排布的: `{desc: , asc: }` - */ - sortIcons?: { desc?: React.ReactNode; asc?: React.ReactNode }; - - /** - * 额外渲染行的渲染函数 - */ - expandedRowRender?: (record: any, index: number) => React.ReactElement; - rowExpandable?: (record: any) => boolean; - - /** - * 额外渲染行的缩进,包含两个数字,第一个数字为左侧缩进,第二个数字为右侧缩进 - */ - expandedRowIndent?: [number, number]; - - /** - * 默认情况下展开的渲染行或者Tree, 传入此属性为受控状态 - */ - openRowKeys?: Array; - - /** - * 是否显示点击展开额外渲染行的+号按钮 - */ - hasExpandedRowCtrl?: boolean; - - /** - * 设置额外渲染行的属性 - */ - getExpandedColProps?: ( - record: IRecord, - index: number - ) => object | Record; - - /** - * 在额外渲染行或者Tree展开或者收起的时候触发的事件 - */ - onRowOpen?: ( - openRowKeys: Array, - currentRowKey: string, - expanded: boolean, - currentRecord: any - ) => void; - - /** - * 点击额外渲染行触发的事件 - */ - onExpandedRowClick?: (record: any, index: number, e: React.MouseEvent) => void; - - /** - * 表头是否固定,该属性配合maxBodyHeight使用,当内容区域的高度超过maxBodyHeight的时候,在内容区域会出现滚动条 - */ - fixedHeader?: boolean; - - /** - * 最大内容区域的高度,在`fixedHeader`为`true`的时候,超过这个高度会出现滚动条 - */ - maxBodyHeight?: number | string; - - /** - * 是否启用选择模式 - */ - rowSelection?: { - getProps?: (record: any, index: number) => void; - onChange?: (selectedRowKeys: Array, records: Array) => void; - onSelect?: (selected: boolean, record: any, records: Array) => void; - onSelectAll?: (selected: boolean, records: Array) => void; - selectedRowKeys?: Array; - mode?: 'single' | 'multiple'; - titleProps?: () => any; - columnProps?: () => any; - titleAddons?: () => any; - }; - - /** - * 表头是否是sticky - */ - stickyHeader?: boolean; - - /** - * 距离窗口顶部达到指定偏移量后触发 - */ - offsetTop?: number; - - /** - * affix组件的的属性 - */ - affixProps?: AffixProps; - - /** - * 在tree模式下的缩进尺寸, 仅在isTree为true时候有效 - */ - indent?: number; - - /** - * 开启Table的tree模式, 接收的数据格式中包含children则渲染成tree table - */ - isTree?: boolean; - - /** - * 是否开启虚拟滚动 - */ - useVirtual?: boolean; - - /** - * 滚动到指定行 - */ - scrollToRow?: number; - - /** - * 设置行高 - */ - rowHeight?: number | (() => any); - - /** - * 在内容区域滚动的时候触发的函数 - */ - onBodyScroll?: (start: number) => void; - - /** - * 开启时,getExpandedColProps() / getRowProps() / expandedRowRender() 的第二个参数 index (该行所对应的序列) 将按照01,2,3,4...的顺序返回,否则返回真实index(0,2,4,6... / 1,3,5,7...) - */ - expandedIndexSimulate?: boolean; - /** - * 在 hover 时出现十字参考轴,适用于表头比较复杂,需要做表头分类的场景。 - */ - crossline?: boolean; - - /** - * 虚拟滚动时向前保留渲染的行数 - * @defaultValue 10 - */ - keepForwardRenderRows?: number; -} - -export default class Table extends React.Component { - static Column: typeof Column; - static ColumnGroup: typeof ColumnGroup; - static GroupHeader: typeof GroupHeader; - static GroupFooter: typeof GroupFooter; - static StickyLock: typeof Table; - static Base: typeof Table; -} diff --git a/components/table/index.jsx b/components/table/index.tsx similarity index 67% rename from components/table/index.jsx rename to components/table/index.tsx index 649b8b9481..6b0527c009 100644 --- a/components/table/index.jsx +++ b/components/table/index.tsx @@ -1,3 +1,4 @@ +import { type ComponentType } from 'react'; import ConfigProvider from '../config-provider'; import Base from './base'; import tree from './tree'; @@ -11,54 +12,81 @@ import list from './list'; import sticky from './sticky'; import ListHeader from './list-header'; import ListFooter from './list-footer'; -import { env } from '../util'; +import { env, type NonReactStatics } from '../util'; +import { assignSubComponent } from '../util/component'; +import type { TableProps } from './types'; const { ieVersion } = env; +// 动态切换类型的场景过多,无法通过类型推导,所以这里直接给出最终类型。 +type TypeFullType = ComponentType & + NonReactStatics & + NonReactStatics>; + const ORDER_LIST = [fixed, lock, selection, expanded, virtual, tree, list, sticky]; const Table = ORDER_LIST.reduce((ret, current) => { ret = current(ret); return ret; -}, Base); +}, Base) as unknown as TypeFullType; -lock._typeMark = 'lock'; -expanded._typeMark = 'expanded'; -fixed._typeMark = 'fixed'; +(lock as typeof lock & { _typeMark: string })._typeMark = 'lock'; +(expanded as typeof expanded & { _typeMark: string })._typeMark = 'expanded'; +(fixed as typeof fixed & { _typeMark: string })._typeMark = 'fixed'; -const StickyLockTable = ORDER_LIST.reduce((ret, current) => { +type CurrentType = ((base: typeof Base) => typeof Base) & { _typeMark?: string }; +const StickyLockTable = ORDER_LIST.reduce((ret, current: CurrentType) => { const newLock = !ieVersion; if (current._typeMark === 'lock') { ret = newLock ? stickyLock(ret) : lock(ret); } else if (current._typeMark === 'expanded') { ret = newLock ? expanded(ret, true) : expanded(ret); } else if (current._typeMark === 'fixed') { + // @ts-expect-error fixed 现在没有第二个参数 ret = newLock ? fixed(ret, true) : fixed(ret); } else { ret = current(ret); } return ret; -}, Base); - -Table.Base = Base; -Table.fixed = fixed; -Table.lock = lock; -Table.selection = selection; -Table.expanded = expanded; -Table.tree = tree; -Table.virtual = virtual; -Table.list = list; -Table.sticky = sticky; - -Table.GroupHeader = ListHeader; -Table.GroupFooter = ListFooter; - -Table.StickyLock = ConfigProvider.config(StickyLockTable, { - componentName: 'Table', +}, Base) as unknown as TypeFullType; + +const TableWithSub = assignSubComponent(Table, { + Base, + fixed, + lock, + selection, + expanded, + tree, + virtual, + list, + sticky, + GroupHeader: ListHeader, + GroupFooter: ListFooter, + StickyLock: ConfigProvider.config(StickyLockTable, { + componentName: 'Table', + }), }); -export default ConfigProvider.config(Table, { +export type { + TableProps, + BaseTableProps, + GroupHeaderProps, + GroupFooterProps, + ColumnProps, + ColumnGroupProps, + HeaderProps, + BodyProps, + WrapperProps, + RowProps, + CellProps, + FilterProps, + SortProps, + RecordItem, + SelectionRowProps, +} from './types'; + +export default ConfigProvider.config(TableWithSub, { componentName: 'Table', - transform: /* istanbul ignore next */ (props, deprecated) => { + transform: (props: TableProps, deprecated) => { // fix https://github.com/alibaba-fusion/next/issues/4062 if ('columns' in props && typeof props.columns !== 'undefined') { const { columns, ...others } = props; @@ -106,7 +134,7 @@ export default ConfigProvider.config(Table, { const { getRowClassName, getRowProps, ...others } = props; if (getRowClassName) { - const newGetRowProps = (...args) => { + const newGetRowProps: TableProps['getRowProps'] = (...args) => { return { className: getRowClassName(...args), ...(getRowProps ? getRowProps(...args) : {}), diff --git a/components/table/list-footer.jsx b/components/table/list-footer.tsx similarity index 72% rename from components/table/list-footer.jsx rename to components/table/list-footer.tsx index 97f0dec03a..255d366e69 100644 --- a/components/table/list-footer.jsx +++ b/components/table/list-footer.tsx @@ -1,16 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import type { GroupFooterProps } from './types'; -/* istanbul ignore file */ /** * Table.GroupFooter * @order 3 **/ -export default class ListFooter extends React.Component { +export default class ListFooter extends React.Component { static propTypes = { - /** - * 行渲染的逻辑 - */ cell: PropTypes.oneOfType([PropTypes.element, PropTypes.node, PropTypes.func]), }; diff --git a/components/table/list-header.jsx b/components/table/list-header.tsx similarity index 62% rename from components/table/list-header.jsx rename to components/table/list-header.tsx index c2bbf2b7e2..5f3f9df925 100644 --- a/components/table/list-header.jsx +++ b/components/table/list-header.tsx @@ -1,27 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; +import type { GroupHeaderProps } from './types'; /** * Table.GroupHeader * @order 2 **/ -export default class ListHeader extends React.Component { +export default class ListHeader extends React.Component { static propTypes = { - /** - * 行渲染的逻辑 - */ cell: PropTypes.oneOfType([PropTypes.element, PropTypes.node, PropTypes.func]), - /** - * 是否在Children上面渲染selection - */ hasChildrenSelection: PropTypes.bool, - /** - * 是否在GroupHeader上面渲染selection - */ hasSelection: PropTypes.bool, - /** - * 当 dataSouce 里没有 children 时,是否使用内容作为数据 - */ useFirstLevelDataWhenNoChildren: PropTypes.bool, }; diff --git a/components/table/list.jsx b/components/table/list.tsx similarity index 55% rename from components/table/list.jsx rename to components/table/list.tsx index 9387b68b15..a7244c905e 100644 --- a/components/table/list.jsx +++ b/components/table/list.tsx @@ -1,4 +1,4 @@ -import React, { Children } from 'react'; +import React, { Children, type ReactElement } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import ListHeader from './list-header'; @@ -8,9 +8,18 @@ import BodyComponent from './list/body'; import HeaderComponent from './fixed/header'; import WrapperComponent from './fixed/wrapper'; import { statics } from './util'; +import type Base from './base'; +import type { + GroupFooterProps, + GroupHeaderProps, + ListTableProps, + RecordItem, + RowLike, + WrapperLike, +} from './types'; -export default function list(BaseComponent) { - class ListTable extends React.Component { +export default function list(BaseComponent: typeof Base) { + class ListTable extends React.Component { static ListHeader = ListHeader; static ListFooter = ListFooter; static ListRow = RowComponent; @@ -29,6 +38,10 @@ export default function list(BaseComponent) { }; state = {}; + listHeader: GroupHeaderProps; + listFooter: GroupFooterProps; + rowSelection: ListTableProps['rowSelection']; + ds: RecordItem[]; getChildContext() { return { @@ -38,9 +51,9 @@ export default function list(BaseComponent) { }; } - normalizeDataSource(dataSource) { - const ret = []; - const loop = function(dataSource, level) { + normalizeDataSource(dataSource: RecordItem[]) { + const ret: RecordItem[] = []; + const loop = function (dataSource: RecordItem[], level: number) { dataSource.forEach(item => { const itemCopy = { ...item }; itemCopy.__level = level; @@ -56,36 +69,39 @@ export default function list(BaseComponent) { } render() { - /* eslint-disable prefer-const */ - let { components, children, className, prefix, ...others } = this.props; - let isList = false, - ret = []; - Children.forEach(children, child => { - if (child) { - if (['function', 'object'].indexOf(typeof child.type) > -1) { - if (child.type._typeMark === 'listHeader') { - this.listHeader = child.props; - isList = true; - } else if (child.type._typeMark === 'listFooter') { - this.listFooter = child.props; + const { children, prefix, ...others } = this.props; + let { components, className } = this.props; + let isList = false; + const ret: ReactElement[] = []; + Children.forEach( + children, + (child: ReactElement) => { + if (child) { + if (['function', 'object'].indexOf(typeof child.type) > -1) { + if (child.type._typeMark === 'listHeader') { + this.listHeader = child.props as GroupHeaderProps; + isList = true; + } else if (child.type._typeMark === 'listFooter') { + this.listFooter = child.props as GroupFooterProps; + } else { + ret.push(child); + } } else { ret.push(child); } - } else { - ret.push(child); } } - }); + ); this.rowSelection = this.props.rowSelection; if (isList) { components = { ...components }; - components.Row = components.Row || RowComponent; + components.Row = components.Row || (RowComponent as RowLike); components.Body = components.Body || BodyComponent; components.Header = components.Header || HeaderComponent; - components.Wrapper = components.Wrapper || WrapperComponent; + components.Wrapper = components.Wrapper || (WrapperComponent as WrapperLike); className = classnames({ [`${prefix}table-group`]: true, - [className]: className, + [className!]: className, }); } return ( @@ -99,6 +115,5 @@ export default function list(BaseComponent) { ); } } - statics(ListTable, BaseComponent); - return ListTable; + return statics(ListTable, BaseComponent); } diff --git a/components/table/list/body.jsx b/components/table/list/body.tsx similarity index 58% rename from components/table/list/body.jsx rename to components/table/list/body.tsx index 294a531e26..9874faf467 100644 --- a/components/table/list/body.jsx +++ b/components/table/list/body.tsx @@ -1,25 +1,29 @@ -import React from 'react'; +import React, { type UIEvent } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import BodyComponent from '../base/body'; +import type { ListBodyContext, ListBodyProps } from '../types'; -export default class ListBody extends React.Component { +export default class ListBody extends React.Component { static contextTypes = { getNode: PropTypes.func, onFixedScrollSync: PropTypes.func, }; + context: ListBodyContext; + componentDidMount() { const { getNode } = this.context; - getNode && getNode('body', findDOMNode(this)); + getNode && getNode('body', findDOMNode(this) as HTMLElement); } - onScroll = e => { + onScroll = (e: UIEvent) => { const { onFixedScrollSync } = this.context; onFixedScrollSync && onFixedScrollSync(e); }; render() { + // @ts-expect-error Base 中没有 onScroll 属性 return ; } } diff --git a/components/table/list/row.jsx b/components/table/list/row.tsx similarity index 88% rename from components/table/list/row.jsx rename to components/table/list/row.tsx index 7ad25c669d..a22d363786 100644 --- a/components/table/list/row.jsx +++ b/components/table/list/row.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { log } from '../../util'; import Row from '../base/row'; +import type { ListRowContext, RecordItem } from '../types'; export default class GroupListRow extends Row { static contextTypes = { @@ -13,8 +14,9 @@ export default class GroupListRow extends Row { lockType: PropTypes.oneOf(['left', 'right']), }; + context: ListRowContext; + render() { - /* eslint-disable no-unused-vars*/ const { prefix, className, @@ -39,7 +41,7 @@ export default class GroupListRow extends Row { } = this.props; const cls = classnames({ [`${prefix}table-row`]: true, - [className]: className, + [className!]: className, }); // clear notRenderCellIndex, incase of cached data @@ -98,7 +100,7 @@ export default class GroupListRow extends Row { 'record.children/recored should contains primaryKey when childrenSelection is true.' ); } - return {cells}; + return {cells}; } if (this.context.rowSelection) { cells.shift(); @@ -114,17 +116,20 @@ export default class GroupListRow extends Row { } return null; } - renderContent(type) { + renderContent(type: string) { const { columns, prefix, record, rowIndex } = this.props; const cameType = type.charAt(0).toUpperCase() + type.substr(1); - const list = this.context[`list${cameType}`]; + const list = this.context[`list${cameType as 'Header' | 'Footer'}` as const]; let listNode; if (list) { if (React.isValidElement(list.cell)) { - listNode = React.cloneElement(list.cell, { - record, - index: rowIndex, - }); + listNode = React.cloneElement( + list.cell as React.ReactElement<{ record?: RecordItem; index?: number }>, + { + record, + index: rowIndex, + } + ); } else if (typeof list.cell === 'function') { listNode = list.cell(record, rowIndex); } diff --git a/components/table/lock.jsx b/components/table/lock.tsx similarity index 70% rename from components/table/lock.jsx rename to components/table/lock.tsx index 1f40ddfd86..4b52d15ebc 100644 --- a/components/table/lock.jsx +++ b/components/table/lock.tsx @@ -1,4 +1,4 @@ -import React, { Children } from 'react'; +import React, { Children, type ReactElement } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -9,20 +9,38 @@ import LockBody from './lock/body'; import LockHeader from './lock/header'; import LockWrapper from './fixed/wrapper'; import { statics } from './util'; +import type Base from './base'; +import type { + LockTableProps, + NormalizedColumnProps, + RecordItem, + RowLike, + WrapperLike, +} from './types'; + +// Fixme: 实现很简陋的 deepCopy,应用 lodash 替换 +function deepCopy(arr: NormalizedColumnProps[]) { + const copy = (arr: NormalizedColumnProps[]) => { + return arr.map(item => { + const newItem = { ...item }; + if (item.children) { + item.children = copy(item.children); + } + return newItem; + }); + }; + return copy(arr) as NormalizedColumnProps[]; +} const { ieVersion } = env; -export default function lock(BaseComponent) { +export default function lock(BaseComponent: typeof Base) { /** Table */ - class LockTable extends React.Component { + class LockTable extends React.Component { static LockRow = LockRow; static LockBody = LockBody; static LockHeader = LockHeader; static propTypes = { scrollToCol: PropTypes.number, - /** - * 指定滚动到某一行,仅在`useVirtual`的时候生效 - */ - scrollToRow: PropTypes.number, ...BaseComponent.propTypes, }; @@ -37,9 +55,18 @@ export default function lock(BaseComponent) { onRowMouseEnter: PropTypes.func, onRowMouseLeave: PropTypes.func, }; - - constructor(props, context) { - super(props, context); + _isLock: boolean; + innerHeaderNode: HTMLDivElement | null; + lockLeftChildren: NormalizedColumnProps[]; + lockRightChildren: NormalizedColumnProps[]; + lastScrollTop: number; + lockLeftEl: InstanceType | null; + lockRightEl: InstanceType | null; + _notNeedAdjustLockLeft: boolean; + _notNeedAdjustLockRight: boolean; + + constructor(props: LockTableProps) { + super(props); this.lockLeftChildren = []; this.lockRightChildren = []; } @@ -64,7 +91,7 @@ export default function lock(BaseComponent) { this.forceUpdate(); } - shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdate(nextProps: LockTableProps, nextState: unknown, nextContext: unknown) { if (nextProps.pure) { const isEqual = shallowElementEquals(nextProps, this.props); return !(isEqual && obj.shallowEqual(nextContext, this.context)); @@ -82,7 +109,10 @@ export default function lock(BaseComponent) { events.off(window, 'resize', this.adjustSize); } - normalizeChildrenState(props) { + [tableKey: `table${string}Inc`]: InstanceType | null; + [nodeKey: `${string}${'header' | 'body'}${'Left' | 'Right' | ''}Node`]: HTMLElement | null; + + normalizeChildrenState(props: LockTableProps) { const columns = this.normalizeChildren(props); const splitChildren = this.splitFromNormalizeChildren(columns); const { lockLeftChildren, lockRightChildren } = splitChildren; @@ -93,15 +123,17 @@ export default function lock(BaseComponent) { }; } - // 将React结构化数据提取props转换成数组 - normalizeChildren(props) { + // 将 React 结构化数据提取 props 转换成数组 + normalizeChildren(props: LockTableProps) { const { children, columns } = props; let isLock = false, ret; - const checkLock = col => { - if ([true, 'left', 'right'].indexOf(col.lock) > -1) { + const checkLock = (col: NormalizedColumnProps) => { + if ([true, 'left', 'right'].indexOf(col.lock!) > -1) { if (!('width' in col)) { - log.warning(`Should config width for lock column named [ ${col.dataIndex} ].`); + log.warning( + `Should config width for lock column named [ ${col.dataIndex} ].` + ); } isLock = true; } @@ -109,7 +141,7 @@ export default function lock(BaseComponent) { if (columns && !children) { ret = columns; - const getColumns = cols => { + const getColumns = (cols: NonNullable) => { cols.forEach((col = {}) => { checkLock(col); @@ -121,9 +153,9 @@ export default function lock(BaseComponent) { getColumns(columns); } else { - const getChildren = children => { - const ret = []; - Children.forEach(children, child => { + const getChildren = (children: LockTableProps['children']) => { + const ret: NormalizedColumnProps[] = []; + Children.forEach(children, (child: ReactElement) => { if (child) { const props = { ...child.props }; checkLock(props); @@ -151,13 +183,16 @@ export default function lock(BaseComponent) { return ret; } - //从数组中分离出lock的列和正常的列 - splitFromNormalizeChildren(children) { + //从数组中分离出 lock 的列和正常的列 + splitFromNormalizeChildren(children: NormalizedColumnProps[]) { const originChildren = deepCopy(children); const lockLeftChildren = deepCopy(children); const lockRightChildren = deepCopy(children); - const loop = (lockChildren, condition) => { - const ret = []; + const loop = ( + lockChildren: NormalizedColumnProps[], + condition: (child: NormalizedColumnProps) => string | boolean | undefined + ) => { + const ret: NormalizedColumnProps[] = []; lockChildren.forEach(child => { if (child.children) { const res = loop(child.children, condition); @@ -198,7 +233,11 @@ export default function lock(BaseComponent) { } //将左侧的锁列树和中间的普通树及右侧的锁列树进行合并 - mergeFromSplitLockChildren(splitChildren) { + mergeFromSplitLockChildren(splitChildren: { + lockLeftChildren: NormalizedColumnProps[]; + lockRightChildren: NormalizedColumnProps[]; + originChildren: NormalizedColumnProps[]; + }) { const { lockLeftChildren, lockRightChildren } = splitChildren; let { originChildren } = splitChildren; Array.prototype.unshift.apply(originChildren, lockLeftChildren); @@ -206,20 +245,20 @@ export default function lock(BaseComponent) { return originChildren; } - getTableInstance = (type, instance) => { + getTableInstance = (type: string, instance: InstanceType | null) => { type = type ? type.charAt(0).toUpperCase() + type.substr(1) : ''; - this[`table${type}Inc`] = instance; + this[`table${type}Inc` as const] = instance; }; - getNode = (type, node, lockType) => { + getNode = (type: 'header' | 'body', node: HTMLElement, lockType: string) => { lockType = lockType ? lockType.charAt(0).toUpperCase() + lockType.substr(1) : ''; - this[`${type}${lockType}Node`] = node; + this[`${type}${lockType as 'Left' | 'Right' | ''}Node`] = node; if (type === 'header' && !this.innerHeaderNode && !lockType) { - this.innerHeaderNode = this.headerNode.querySelector('div'); + this.innerHeaderNode = this.headerNode!.querySelector('div'); } }; - onRowMouseEnter = (record, index) => { + onRowMouseEnter = (record: RecordItem, index: number) => { if (this.isLock()) { const row = this.getRowNode(index); const leftRow = this.getRowNode(index, 'left'); @@ -230,7 +269,7 @@ export default function lock(BaseComponent) { } }; - onRowMouseLeave = (record, index) => { + onRowMouseLeave = (record: RecordItem, index: number) => { if (this.isLock()) { const row = this.getRowNode(index); const leftRow = this.getRowNode(index, 'left'); @@ -261,13 +300,17 @@ export default function lock(BaseComponent) { } } - onLockBodyScrollTop = event => { + onLockBodyScrollTop = (event: { + currentTarget?: HTMLElement; + target?: HTMLElement; + [key: string]: unknown; + }) => { // set scroll top for lock columns & main body const target = event.target; if (event.currentTarget !== target) { return; } - const distScrollTop = target.scrollTop; + const distScrollTop = target!.scrollTop; if (this.isLock() && distScrollTop !== this.lastScrollTop) { const lockRightBody = this.bodyRightNode, @@ -290,16 +333,20 @@ export default function lock(BaseComponent) { // add shadow class for lock columns if (this.isLock()) { const { rtl } = this.props; - const lockRightTable = rtl ? this.getWrapperNode('left') : this.getWrapperNode('right'), - lockLeftTable = rtl ? this.getWrapperNode('right') : this.getWrapperNode('left'), + const lockRightTable = rtl + ? this.getWrapperNode('left') + : this.getWrapperNode('right'), + lockLeftTable = rtl + ? this.getWrapperNode('right') + : this.getWrapperNode('left'), shadowClassName = 'shadow'; - const x = this.bodyNode.scrollLeft; + const x = this.bodyNode!.scrollLeft; if (x === 0) { lockLeftTable && dom.removeClass(lockLeftTable, shadowClassName); lockRightTable && dom.addClass(lockRightTable, shadowClassName); - } else if (x === this.bodyNode.scrollWidth - this.bodyNode.clientWidth) { + } else if (x === this.bodyNode!.scrollWidth - this.bodyNode!.clientWidth) { lockLeftTable && dom.addClass(lockLeftTable, shadowClassName); lockRightTable && dom.removeClass(lockRightTable, shadowClassName); } else { @@ -309,17 +356,17 @@ export default function lock(BaseComponent) { } }; - onLockBodyScroll = event => { + onLockBodyScroll = (event: { currentTarget?: HTMLElement; [key: string]: unknown }) => { this.onLockBodyScrollTop(event); this.onLockBodyScrollLeft(); }; - // Table处理过后真实的lock状态 + // Table 处理过后真实的 lock 状态 isLock() { return this.lockLeftChildren.length || this.lockRightChildren.length; } - // 用户设置的lock状态 + // 用户设置的 lock 状态 isOriginLock() { return this._isLock; } @@ -351,43 +398,41 @@ export default function lock(BaseComponent) { adjustIfTableNotNeedLock() { if (this.isOriginLock()) { - const widthObj = this.tableInc.flatChildren - .map((item, index) => { - const cell = this.getCellNode(0, index) || {}; - const headerCell = this.getHeaderCellNode(0, index) || {}; - - // fix https://codesandbox.io/s/fusion-next-template-d9bu8 - // close #1832 - try { - return { - cellWidths: parseFloat(getComputedStyle(cell).width) || 0, - headerWidths: parseFloat(getComputedStyle(headerCell).width) || 0, - }; - } catch (error) { - return { - cellWidths: cell.clientWidth || 0, - headerWidths: headerCell.clientWidth || 0, - }; - } - }) - .reduce( - (a, b) => { - return { - cellWidths: a.cellWidths + b.cellWidths, - headerWidths: a.headerWidths + b.headerWidths, - }; - }, - { - cellWidths: 0, - headerWidths: 0, - } - ); + const widthObj = this.tableInc!.flatChildren.map((item, index) => { + const cell = this.getCellNode(0, index) || ({} as HTMLElement); + const headerCell = this.getHeaderCellNode(0, index) || ({} as HTMLElement); + + // fix https://codesandbox.io/s/fusion-next-template-d9bu8 + // close #1832 + try { + return { + cellWidths: parseFloat(getComputedStyle(cell).width) || 0, + headerWidths: parseFloat(getComputedStyle(headerCell).width) || 0, + }; + } catch (error) { + return { + cellWidths: cell.clientWidth || 0, + headerWidths: headerCell.clientWidth || 0, + }; + } + }).reduce( + (a, b) => { + return { + cellWidths: a.cellWidths + b.cellWidths, + headerWidths: a.headerWidths + b.headerWidths, + }; + }, + { + cellWidths: 0, + headerWidths: 0, + } + ); let node, width; try { - node = findDOMNode(this); - width = node.clientWidth; + node = findDOMNode(this) as HTMLElement; + width = node!.clientWidth; } catch (err) { node = null; width = 0; @@ -398,7 +443,9 @@ export default function lock(BaseComponent) { return true; } - const configWidths = parseInt(widthObj.cellWidths) || parseInt(widthObj.headerWidths); + const configWidths = + // @ts-expect-error 这里用 parseInt 只是想取整,后续应该换成 math 相关 + parseInt(widthObj.cellWidths) || parseInt(widthObj.headerWidths); if (configWidths <= width && configWidths > 0) { this.removeLockTable(); @@ -420,7 +467,12 @@ export default function lock(BaseComponent) { const paddingName = rtl ? 'paddingLeft' : 'paddingRight'; const marginName = rtl ? 'marginLeft' : 'marginRight'; const scrollBarSize = +dom.scrollbar().width || 0; - const style = { + const style: Partial< + Record< + typeof paddingName | typeof marginName | 'marginBottom' | 'paddingBottom', + unknown + > + > = { [paddingName]: scrollBarSize, [marginName]: scrollBarSize, }; @@ -431,7 +483,7 @@ export default function lock(BaseComponent) { const lockLeftBody = this.bodyLeftNode, lockRightBody = this.bodyRightNode, lockRightBodyWrapper = this.getWrapperNode('right'), - bodyHeight = body.offsetHeight, + bodyHeight = body!.offsetHeight, width = hasVerScroll ? scrollBarSize : 0, lockBodyHeight = bodyHeight - scrollBarSize; @@ -447,7 +499,7 @@ export default function lock(BaseComponent) { style.paddingBottom = 20; } - const lockStyle = { + const lockStyle: Partial> = { 'max-height': lockBodyHeight, }; if (!hasHeader && !+scrollBarSize) { @@ -475,8 +527,8 @@ export default function lock(BaseComponent) { adjustHeaderSize() { if (this.isLock()) { - this.tableInc.groupChildren.forEach((child, index) => { - const lastIndex = this.tableInc.groupChildren[index].length - 1; + this.tableInc!.groupChildren.forEach((child, index) => { + const lastIndex = this.tableInc!.groupChildren[index].length - 1; const headerRightRow = this.getHeaderCellNode(index, lastIndex), headerLeftRow = this.getHeaderCellNode(index, 0), headerRightLockRow = this.getHeaderCellNode(index, 0, 'right'), @@ -488,9 +540,13 @@ export default function lock(BaseComponent) { dom.setStyle(headerRightLockRow, 'height', maxRightRowHeight); setTimeout(() => { - const affixRef = this.tableRightInc.affixRef; - // if rendered then update postion of affix - return affixRef && affixRef.getInstance() && affixRef.getInstance().updatePosition(); + const affixRef = this.tableRightInc!.affixRef; + // if rendered then update position of affix + return ( + affixRef && + affixRef.getInstance() && + affixRef.getInstance().updatePosition() + ); }); } @@ -500,9 +556,13 @@ export default function lock(BaseComponent) { dom.setStyle(headerLeftLockRow, 'height', maxLeftRowHeight); setTimeout(() => { - const affixRef = this.tableLeftInc.affixRef; + const affixRef = this.tableLeftInc!.affixRef; // if rendered then update postion of affix - return affixRef && affixRef.getInstance() && affixRef.getInstance().updatePosition(); + return ( + affixRef && + affixRef.getInstance() && + affixRef.getInstance().updatePosition() + ); }); } }); @@ -511,11 +571,11 @@ export default function lock(BaseComponent) { adjustRowHeight() { if (this.isLock()) { - this.tableInc.props.dataSource.forEach((item, index) => { + this.tableInc!.props.dataSource.forEach((item, index) => { // record may be a string - const rowIndex = `${typeof item === 'object' && '__rowIndex' in item ? item.__rowIndex : index}${ - item.__expanded ? '_expanded' : '' - }`; + const rowIndex = `${ + typeof item === 'object' && '__rowIndex' in item ? item.__rowIndex : index + }${item.__expanded ? '_expanded' : ''}`; // 同步左侧的锁列 this.setRowHeight(rowIndex, 'left'); @@ -525,11 +585,13 @@ export default function lock(BaseComponent) { } } - setRowHeight(rowIndex, dir) { + setRowHeight(rowIndex: number | string, dir: string) { const lockRow = this.getRowNode(rowIndex, dir), row = this.getRowNode(rowIndex), rowHeight = - (ieVersion ? row && row.offsetHeight : row && parseFloat(getComputedStyle(row).height)) || 'auto', + (ieVersion + ? row && row.offsetHeight + : row && parseFloat(getComputedStyle(row).height)) || 'auto', lockHeight = (ieVersion ? lockRow && lockRow.offsetHeight @@ -540,13 +602,13 @@ export default function lock(BaseComponent) { } } - getWrapperNode(type) { + getWrapperNode(type: string) { type = type ? type.charAt(0).toUpperCase() + type.substr(1) : ''; try { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(this[`lock${type}El`]); + return findDOMNode(this[`lock${type as 'Left' | 'Right'}El`]) as HTMLElement; } catch (error) { return null; } @@ -567,7 +629,7 @@ export default function lock(BaseComponent) { // return row; // } - getRowNode(index, type) { + getRowNode(index: number | string, type?: string) { type = type ? type.charAt(0).toUpperCase() + type.substr(1) : ''; const table = this[`table${type}Inc`]; @@ -575,13 +637,13 @@ export default function lock(BaseComponent) { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.getRowRef(index)); + return findDOMNode(table!.getRowRef(index)) as HTMLElement; } catch (error) { return null; } } - getHeaderCellNode(index, i, type) { + getHeaderCellNode(index: number, i: number, type?: string) { type = type ? type.charAt(0).toUpperCase() + type.substr(1) : ''; const table = this[`table${type}Inc`]; @@ -589,13 +651,13 @@ export default function lock(BaseComponent) { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.getHeaderCellRef(index, i)); + return findDOMNode(table!.getHeaderCellRef(index, i)) as HTMLElement; } catch (error) { return null; } } - getCellNode(index, i, type) { + getCellNode(index: number, i: number, type?: string) { type = type ? type.charAt(0).toUpperCase() + type.substr(1) : ''; const table = this[`table${type}Inc`]; @@ -603,15 +665,15 @@ export default function lock(BaseComponent) { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.getCellRef(index, i)); + return findDOMNode(table!.getCellRef(index, i)) as HTMLElement; } catch (error) { return null; } } - getFlatenChildrenLength = (children = []) => { - const loop = arr => { - const newArray = []; + getFlatenChildrenLength = (children: NormalizedColumnProps[] = []) => { + const loop = (arr: NormalizedColumnProps[]) => { + const newArray: NormalizedColumnProps[] = []; arr.forEach(child => { if (child && child.children) { newArray.push(...loop(child.children)); @@ -625,20 +687,31 @@ export default function lock(BaseComponent) { return loop(children).length; }; - saveLockLeftRef = ref => { + saveLockLeftRef = (ref: InstanceType | null) => { this.lockLeftEl = ref; }; - saveLockRightRef = ref => { + saveLockRightRef = (ref: InstanceType | null) => { this.lockRightEl = ref; }; render() { /* eslint-disable no-unused-vars, prefer-const */ - let { children, columns, prefix, components, className, dataSource, tableWidth, ...others } = this.props; - let { lockLeftChildren, lockRightChildren, children: normalizedChildren } = this.normalizeChildrenState( - this.props - ); + let { + children, + columns, + prefix, + components, + className, + dataSource, + tableWidth, + ...others + } = this.props; + let { + lockLeftChildren, + lockRightChildren, + children: normalizedChildren, + } = this.normalizeChildrenState(this.props); const leftLen = this.getFlatenChildrenLength(lockLeftChildren); const rightLen = this.getFlatenChildrenLength(lockRightChildren); @@ -662,12 +735,12 @@ export default function lock(BaseComponent) { components = { ...components }; components.Body = components.Body || LockBody; components.Header = components.Header || LockHeader; - components.Wrapper = components.Wrapper || LockWrapper; - components.Row = components.Row || LockRow; + components.Wrapper = components.Wrapper || (LockWrapper as WrapperLike); + components.Row = components.Row || (LockRow as RowLike); className = classnames({ [`${prefix}table-lock`]: true, - [`${prefix}table-wrap-empty`]: !dataSource.length, - [className]: className, + [`${prefix}table-wrap-empty`]: !dataSource!.length, + [className!]: className, }); const content = [ ; } } - statics(LockTable, BaseComponent); - return LockTable; -} - -function deepCopy(arr) { - let copy = arr => { - return arr.map(item => { - const newItem = { ...item }; - if (item.children) { - item.children = copy(item.children); - } - return newItem; - }); - }; - return copy(arr); + return statics(LockTable, BaseComponent); } diff --git a/components/table/lock/body.jsx b/components/table/lock/body.tsx similarity index 63% rename from components/table/lock/body.jsx rename to components/table/lock/body.tsx index aafa9de723..7aa3dbc94f 100644 --- a/components/table/lock/body.jsx +++ b/components/table/lock/body.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { type UIEvent } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import FixedBody from '../fixed/body'; +import type { LockBodyContext, LockBodyProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class LockBody extends React.Component { +export default class LockBody extends React.Component { static propTypes = { ...FixedBody.propTypes, }; @@ -16,11 +16,13 @@ export default class LockBody extends React.Component { lockType: PropTypes.oneOf(['left', 'right']), }; + readonly context: LockBodyContext; + componentDidMount() { - this.context.getLockNode('body', findDOMNode(this), this.context.lockType); + this.context.getLockNode('body', findDOMNode(this) as HTMLElement, this.context.lockType); } - onBodyScroll = event => { + onBodyScroll = (event: UIEvent) => { this.context.onLockBodyScroll(event); }; diff --git a/components/table/lock/header.jsx b/components/table/lock/header.tsx similarity index 62% rename from components/table/lock/header.jsx rename to components/table/lock/header.tsx index 1297627980..26aad4ac22 100644 --- a/components/table/lock/header.jsx +++ b/components/table/lock/header.tsx @@ -1,6 +1,7 @@ import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import FixedHeader from '../fixed/header'; +import type { LockHeaderContext } from '../types'; export default class LockHeader extends FixedHeader { static propTypes = { @@ -13,9 +14,12 @@ export default class LockHeader extends FixedHeader { lockType: PropTypes.oneOf(['left', 'right']), }; + context: LockHeaderContext; + componentDidMount() { const { getNode, getLockNode } = this.context; - getNode && getNode('header', findDOMNode(this), this.context.lockType); - getLockNode && getLockNode('header', findDOMNode(this), this.context.lockType); + getNode && getNode('header', findDOMNode(this) as HTMLElement, this.context.lockType); + getLockNode && + getLockNode('header', findDOMNode(this) as HTMLElement, this.context.lockType); } } diff --git a/components/table/lock/row.jsx b/components/table/lock/row.tsx similarity index 62% rename from components/table/lock/row.jsx rename to components/table/lock/row.tsx index b27edd5b49..e55df0441a 100644 --- a/components/table/lock/row.jsx +++ b/components/table/lock/row.tsx @@ -1,8 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import Row from '../base/row'; +import type { LockRowProps, RowProps } from '../types'; -export default class LockRow extends React.Component { +export default class LockRow extends React.Component { static propTypes = { ...Row.propTypes, }; @@ -16,14 +17,14 @@ export default class LockRow extends React.Component { ...Row.defaultProps, }; - onMouseEnter = (record, index, e) => { + onMouseEnter: RowProps['onMouseEnter'] = (record, index, e) => { const { onRowMouseEnter } = this.context; const { onMouseEnter } = this.props; onRowMouseEnter && onRowMouseEnter(record, index, e); onMouseEnter(record, index, e); }; - onMouseLeave = (record, index, e) => { + onMouseLeave: RowProps['onMouseLeave'] = (record, index, e) => { const { onRowMouseLeave } = this.context; const { onMouseLeave } = this.props; onRowMouseLeave && onRowMouseLeave(record, index, e); @@ -31,7 +32,12 @@ export default class LockRow extends React.Component { }; render() { - /* eslint-disable no-unused-vars*/ - return ; + return ( + + ); } } diff --git a/components/table/mobile/index.jsx b/components/table/mobile/index.tsx similarity index 77% rename from components/table/mobile/index.jsx rename to components/table/mobile/index.tsx index 3e0e5f7ae0..56e0b1faec 100644 --- a/components/table/mobile/index.jsx +++ b/components/table/mobile/index.tsx @@ -1,3 +1,4 @@ +// @ts-expect-error meet 中没有导出 Table import { Table as MeetTable } from '@alifd/meet-react'; import NextTable from '../index'; diff --git a/components/table/new-lock.jsx b/components/table/new-lock.tsx similarity index 69% rename from components/table/new-lock.jsx rename to components/table/new-lock.tsx index 30f1a53638..eed6711c7e 100644 --- a/components/table/new-lock.jsx +++ b/components/table/new-lock.tsx @@ -1,4 +1,4 @@ -import React, { Children } from 'react'; +import React, { Children, type ReactElement } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -9,19 +9,37 @@ import LockBody from './lock/body'; import LockHeader from './lock/header'; import LockWrapper from './fixed/wrapper'; import { statics, setStickyStyle } from './util'; +import type Base from './base'; +import type { + NormalizedColumnProps, + RowLike, + StickyLockTableProps, + StickyLockTableState, + WrapperLike, +} from './types'; + +// Fixme: 实现很简陋的 deepCopy,应用 lodash 替换 +function deepCopy(arr: NormalizedColumnProps[]) { + const copy = (arr: NormalizedColumnProps[]) => { + return arr.map(item => { + const newItem = { ...item }; + if (item.children) { + item.children = copy(item.children); + } + return newItem; + }); + }; + return copy(arr) as NormalizedColumnProps[]; +} -export default function stickyLock(BaseComponent) { +export default function stickyLock(BaseComponent: typeof Base) { /** Table */ - class LockTable extends React.Component { + class LockTable extends React.Component { static LockRow = LockRow; static LockBody = LockBody; static LockHeader = LockHeader; static propTypes = { scrollToCol: PropTypes.number, - /** - * 指定滚动到某一行,仅在`useVirtual`的时候生效 - */ - scrollToRow: PropTypes.number, ...BaseComponent.propTypes, }; @@ -35,9 +53,16 @@ export default function stickyLock(BaseComponent) { onLockBodyScroll: PropTypes.func, }; - state = {}; + state = {} as StickyLockTableState; + pingLeft?: boolean; + pingRight?: boolean; + splitChildren: { + lockLeftChildren: NormalizedColumnProps[]; + lockRightChildren: NormalizedColumnProps[]; + originChildren: NormalizedColumnProps[]; + }; - constructor(props, context) { + constructor(props: StickyLockTableProps) { super(props); this.state = { @@ -62,13 +87,19 @@ export default function stickyLock(BaseComponent) { const isEmpty = !(dataSource && dataSource.length > 0); this.updateOffsetArr(); - this.onLockBodyScroll(isEmpty ? { currentTarget: this.headerNode } : { currentTarget: this.bodyNode }); + this.onLockBodyScroll( + isEmpty ? { currentTarget: this.headerNode } : { currentTarget: this.bodyNode } + ); this.forceUpdate(); events.on(window, 'resize', this.updateOffsetArr); } - shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdate( + nextProps: StickyLockTableProps, + nextState: StickyLockTableState, + nextContext: unknown + ) { if (nextProps.pure) { const isEqual = shallowElementEquals(nextProps, this.props); return !(isEqual && obj.shallowEqual(nextContext, this.context)); @@ -80,7 +111,9 @@ export default function stickyLock(BaseComponent) { componentDidUpdate() { this.updateOffsetArr(); this.onLockBodyScroll( - this.bodyNode ? { currentTarget: this.bodyNode } : { currentTarget: this.headerNode }, + this.bodyNode + ? { currentTarget: this.bodyNode } + : { currentTarget: this.headerNode }, true ); } @@ -91,8 +124,12 @@ export default function stickyLock(BaseComponent) { events.off(window, 'resize', this.updateOffsetArr); } + [tableKey: `table${string}Inc`]: InstanceType | null; + [nodeKey: `${string}${'body' | 'header'}Node`]: HTMLElement | null; + updateOffsetArr = () => { - const { lockLeftChildren, lockRightChildren, originChildren } = this.splitChildren || {}; + const { lockLeftChildren, lockRightChildren, originChildren } = + this.splitChildren || {}; const leftLen = this.getFlatenChildren(lockLeftChildren).length; const rightLen = this.getFlatenChildren(lockRightChildren).length; @@ -104,7 +141,7 @@ export default function stickyLock(BaseComponent) { const leftOffsetArr = this.getStickyWidth(lockLeftChildren, 'left', totalLen); const rightOffsetArr = this.getStickyWidth(lockRightChildren, 'right', totalLen); - const state = {}; + const state: Partial = {}; if (`${leftOffsetArr}` !== `${this.state.leftOffsetArr}`) { state.leftOffsetArr = leftOffsetArr; @@ -127,28 +164,30 @@ export default function stickyLock(BaseComponent) { } }; - normalizeChildrenState(props) { + normalizeChildrenState(props: StickyLockTableProps) { const columns = this.normalizeChildren(props); this.splitChildren = this.splitFromNormalizeChildren(columns); - return this.mergeFromSplitLockChildren(this.splitChildren, props.prefix); + return this.mergeFromSplitLockChildren(this.splitChildren, props.prefix!); } - // 将React结构化数据提取props转换成数组 - normalizeChildren(props) { + // 将 React 结构化数据提取 props 转换成数组 + normalizeChildren(props: StickyLockTableProps) { const { children, columns } = props; - let isLock = false, + let isLock: boolean | NormalizedColumnProps | undefined = false, ret; - const getChildren = children => { - const ret = []; - Children.forEach(children, child => { + const getChildren = (children: StickyLockTableProps['children']) => { + const ret: NormalizedColumnProps[] = []; + Children.forEach(children, (child: ReactElement) => { if (child) { const props = { ...child.props }; if ([true, 'left', 'right'].indexOf(props.lock) > -1) { isLock = true; if (!('width' in props)) { - log.warning(`Should config width for lock column named [ ${props.dataIndex} ].`); + log.warning( + `Should config width for lock column named [ ${props.dataIndex} ].` + ); } } ret.push(props); @@ -162,7 +201,7 @@ export default function stickyLock(BaseComponent) { if (columns && !children) { ret = columns; - isLock = columns.find(record => [true, 'left', 'right'].indexOf(record.lock) > -1); + isLock = columns.find(record => [true, 'left', 'right'].indexOf(record.lock!) > -1); } else { ret = getChildren(children); } @@ -178,16 +217,18 @@ export default function stickyLock(BaseComponent) { } /** - * 从数组中分离出lock的列和正常的列 - * @param {Array} children - * @return {Object} { lockLeftChildren, lockRightChildren, originChildren } 锁左列, 锁左列, 剩余列 + * 从数组中分离出 lock 的列和正常的列 + * @returns 锁左列,锁左列,剩余列 */ - splitFromNormalizeChildren(children) { + splitFromNormalizeChildren(children: NormalizedColumnProps[]) { const originChildren = deepCopy(children); const lockLeftChildren = deepCopy(children); const lockRightChildren = deepCopy(children); - const loop = (lockChildren, condition) => { - const ret = []; + const loop = ( + lockChildren: NormalizedColumnProps[], + condition: (child: NormalizedColumnProps) => string | boolean | undefined + ) => { + const ret: NormalizedColumnProps[] = []; lockChildren.forEach(child => { if (child.children) { const res = loop(child.children, condition); @@ -230,37 +271,49 @@ export default function stickyLock(BaseComponent) { /** * 将左侧的锁列树和中间的普通树及右侧的锁列树进行合并 * 会在原始 originChildren 上做改动 - * @param {Object} splitChildren { lockLeftChildren, lockRightChildren, originChildren } - * @return {Array} originChildren + * @param splitChildren - \{ lockLeftChildren, lockRightChildren, originChildren \} + * @returns originChildren */ - mergeFromSplitLockChildren(splitChildren, prefix) { + mergeFromSplitLockChildren(splitChildren: typeof this.splitChildren, prefix: string) { const { lockLeftChildren, lockRightChildren } = splitChildren; const { originChildren } = splitChildren; const flatenLeftChildren = this.getFlatenChildren(lockLeftChildren); const flatenRightChildren = this.getFlatenChildren(lockRightChildren); - setStickyStyle(lockLeftChildren, flatenLeftChildren, 'left', this.state.leftOffsetArr, prefix); - setStickyStyle(lockRightChildren, flatenRightChildren, 'right', this.state.rightOffsetArr, prefix); + setStickyStyle( + lockLeftChildren, + flatenLeftChildren, + 'left', + this.state.leftOffsetArr, + prefix + ); + setStickyStyle( + lockRightChildren, + flatenRightChildren, + 'right', + this.state.rightOffsetArr, + prefix + ); return [...lockLeftChildren, ...originChildren, ...lockRightChildren]; } - getCellNode(index, i) { + getCellNode(index: number, i: number) { const table = this.tableInc; try { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.getCellRef(index, i)); + return findDOMNode(table!.getCellRef(index, i)) as HTMLElement; } catch (error) { return null; } } - onLockBodyScroll = (e, forceSet) => { - const { scrollLeft, scrollWidth, clientWidth } = e.currentTarget || {}; + onLockBodyScroll = (e: { currentTarget?: HTMLElement | null }, forceSet?: boolean) => { + const { scrollLeft, scrollWidth, clientWidth } = e.currentTarget! || {}; const { pingRight, pingLeft } = this; const pingLeftNext = scrollLeft > 0 && this.state.hasLockLeft; @@ -280,16 +333,18 @@ export default function stickyLock(BaseComponent) { } }; - getStickyWidth = (lockChildren, dir, totalLen) => { - const { dataSource, scrollToRow } = this.props; - const offsetArr = []; + getStickyWidth = ( + lockChildren: NormalizedColumnProps[], + dir: 'left' | 'right', + totalLen: number + ) => { + const offsetArr: number[] = []; const flatenChildren = this.getFlatenChildren(lockChildren); const len = flatenChildren.length; flatenChildren.reduce((ret, col, index) => { const tag = dir === 'left' ? index : len - 1 - index; const tagNext = dir === 'left' ? tag - 1 : tag + 1; - const nodeToGetWidth = dir === 'left' ? tag - 1 : totalLen - index; if (dir === 'left' && tag === 0) { ret[0] = 0; @@ -301,8 +356,8 @@ export default function stickyLock(BaseComponent) { const { headerCellRowIndex, headerCellColIndex } = flatenChildren[tagNext]; - // 根据tableHeader计算,避免单元格合并出现问题 - const node = this.getHeaderCellNode(headerCellRowIndex, headerCellColIndex); + // 根据 tableHeader 计算,避免单元格合并出现问题 + const node = this.getHeaderCellNode(headerCellRowIndex!, headerCellColIndex!); let colWidth = 0; if (node) { colWidth = parseFloat(getComputedStyle(node).width) || 0; @@ -315,13 +370,13 @@ export default function stickyLock(BaseComponent) { return offsetArr; }; - getTableInstance = (type, instance) => { + getTableInstance = (type: string, instance: InstanceType | null) => { type = ''; this[`table${type}Inc`] = instance; }; - getNode = (type, node) => { - this[`${type}Node`] = node; + getNode = (type: 'body' | 'header', node: HTMLElement) => { + this[`${type}Node` as const] = node; }; getTableNode() { @@ -330,28 +385,28 @@ export default function stickyLock(BaseComponent) { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.tableEl); + return findDOMNode(table!.tableEl) as HTMLElement; } catch (error) { return null; } } - getHeaderCellNode(index, i) { + getHeaderCellNode(index: number, i: number) { const table = this.tableInc; try { // in case of finding an unmounted component due to cached data // need to clear refs of table when dataSource Changed // use try catch for temporary - return findDOMNode(table.getHeaderCellRef(index, i)); + return findDOMNode(table!.getHeaderCellRef(index, i)) as HTMLElement; } catch (error) { return null; } } - getFlatenChildren = (children = []) => { - const loop = arr => { - const newArray = []; + getFlatenChildren = (children: NormalizedColumnProps[] = []) => { + const loop = (arr: NormalizedColumnProps[]) => { + const newArray: NormalizedColumnProps[] = []; arr.forEach(child => { if (child.children) { newArray.push(...loop(child.children)); @@ -367,20 +422,29 @@ export default function stickyLock(BaseComponent) { render() { /* eslint-disable no-unused-vars, prefer-const */ - let { children, columns, prefix, components, scrollToRow, className, dataSource, ...others } = this.props; + let { + children, + columns, + prefix, + components, + scrollToRow, + className, + dataSource, + ...others + } = this.props; const normalizedChildren = this.normalizeChildrenState(this.props); components = { ...components }; components.Body = components.Body || LockBody; components.Header = components.Header || LockHeader; - components.Wrapper = components.Wrapper || LockWrapper; - components.Row = components.Row || LockRow; + components.Wrapper = components.Wrapper || (LockWrapper as WrapperLike); + components.Row = components.Row || (LockRow as RowLike); className = classnames({ [`${prefix}table-lock`]: true, [`${prefix}table-stickylock`]: true, - [`${prefix}table-wrap-empty`]: !dataSource.length, - [className]: className, + [`${prefix}table-wrap-empty`]: !dataSource!.length, + [className!]: className, }); return ( @@ -395,19 +459,5 @@ export default function stickyLock(BaseComponent) { ); } } - statics(LockTable, BaseComponent); - return LockTable; -} - -function deepCopy(arr) { - let copy = arr => { - return arr.map(item => { - const newItem = { ...item }; - if (item.children) { - item.children = copy(item.children); - } - return newItem; - }); - }; - return copy(arr); + return statics(LockTable, BaseComponent); } diff --git a/components/table/selection.jsx b/components/table/selection.tsx similarity index 62% rename from components/table/selection.jsx rename to components/table/selection.tsx index 21172e4a16..390278a81a 100644 --- a/components/table/selection.jsx +++ b/components/table/selection.tsx @@ -1,54 +1,41 @@ -import React, { Children } from 'react'; +import React, { Children, type ReactElement, type UIEvent } from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; -import Checkbox from '../checkbox'; +import Checkbox, { type CheckboxProps } from '../checkbox'; import Radio from '../radio'; import { func, log } from '../util'; import zhCN from '../locale/zh-cn'; import SelectionRow from './selection/row'; import Col from './column'; import { statics } from './util'; +import type Base from './base'; +import type { RecordItem, RowLike, SelectionTableProps, SelectionTableState } from './types'; const { makeChain } = func; -const unique = (arr, key = 'this') => { - const temp = {}, - ret = []; +const unique = (arr: T[], key = 'this'): T[] => { + const temp: Record = {}, + ret: T[] = []; arr.forEach(item => { let value; if (key === 'this') { value = item; } else { - value = item[key]; + value = (item as Record)[key]; } - if (!temp[value]) { + if (!temp[value as string]) { ret.push(item); - temp[value] = true; + temp[value as string] = true; } }); return ret; }; -export default function selection(BaseComponent) { +export default function selection(BaseComponent: typeof Base) { /** Table */ - class SelectionTable extends React.Component { + class SelectionTable extends React.Component { static SelectionRow = SelectionRow; static propTypes = { - /** - * 是否启用选择模式 - * @property {Function} getProps `Function(record, index)=>Object` 获取selection的默认属性 - * @property {Function} onChange `Function(selectedRowKeys:Array, records:Array)` 选择改变的时候触发的事件,**注意:** 其中records只会包含当前dataSource的数据,很可能会小于selectedRowKeys的长度。 - * @property {Function} onSelect `Function(selected:Boolean, record:Object, records:Array)` 用户手动选择/取消选择某行的回调 - * @property {Function} onSelectAll `Function(selected:Boolean, records:Array)` 用户手动选择/取消选择所有行的回调 - * @property {Array} selectedRowKeys 设置了此属性,将rowSelection变为受控状态,接收值为该行数据的primaryKey的值 - * @property {String} mode 选择selection的模式, 可选值为`single`, `multiple`,默认为`multiple` - * @property {Function} columnProps `Function()=>Object` 选择列 的props,例如锁列、对齐等,可使用`Table.Column` 的所有参数 - * @property {Function} titleProps `Function()=>Object` 选择列 表头的props,仅在 `multiple` 模式下生效 - */ - rowSelection: PropTypes.object, - primaryKey: PropTypes.oneOfType([PropTypes.symbol, PropTypes.string]), - dataSource: PropTypes.array, - entireDataSource: PropTypes.array, ...BaseComponent.propTypes, }; @@ -68,8 +55,8 @@ export default function selection(BaseComponent) { selectedRowKeys: PropTypes.array, }; - constructor(props, context) { - super(props, context); + constructor(props: SelectionTableProps) { + super(props); this.state = { selectedRowKeys: props.rowSelection && 'selectedRowKeys' in props.rowSelection @@ -85,7 +72,7 @@ export default function selection(BaseComponent) { }; } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: SelectionTableProps) { if (nextProps.rowSelection && 'selectedRowKeys' in nextProps.rowSelection) { const selectedRowKeys = nextProps.rowSelection.selectedRowKeys || []; return { @@ -96,10 +83,10 @@ export default function selection(BaseComponent) { return null; } - normalizeChildren(children) { + normalizeChildren(children: SelectionTableProps['children']) { const { prefix, rowSelection, size } = this.props; if (rowSelection) { - children = Children.map(children, (child, index) => + children = Children.map(children, (child: ReactElement, index) => React.cloneElement(child, { key: index, }) @@ -107,7 +94,7 @@ export default function selection(BaseComponent) { const attrs = (rowSelection.columnProps && rowSelection.columnProps()) || {}; - children.unshift( + (children as ReactElement[]).unshift( { + addSelection = (columns: SelectionTableProps['columns']) => { const { prefix, rowSelection, size } = this.props; - const attrs = (rowSelection.columnProps && rowSelection.columnProps()) || {}; + const attrs = (rowSelection!.columnProps && rowSelection!.columnProps()) || {}; - if (!columns.find(record => record.key === 'selection')) { - columns.unshift({ + if (!columns!.find(record => record.key === 'selection')) { + columns!.unshift({ key: 'selection', title: this.renderSelectionHeader.bind(this), cell: this.renderSelectionBody.bind(this), @@ -142,10 +129,10 @@ export default function selection(BaseComponent) { renderSelectionHeader = () => { const onChange = this.selectAllRow, - attrs = {}, + attrs: CheckboxProps = {}, { rowSelection, primaryKey, dataSource, entireDataSource, locale } = this.props, { selectedRowKeys } = this.state, - mode = rowSelection.mode ? rowSelection.mode : 'multiple'; + mode = rowSelection!.mode ? rowSelection!.mode : 'multiple'; let checked = !!selectedRowKeys.length; let indeterminate = false; @@ -154,25 +141,25 @@ export default function selection(BaseComponent) { this.flatDataSource(source) .filter((record, index) => { - if (!rowSelection.getProps) { + if (!rowSelection!.getProps) { return true; } else { - return !(rowSelection.getProps(record, index) || {}).disabled; + return !(rowSelection!.getProps(record, index) || {}).disabled; } }) - .map(record => record[primaryKey]) - .forEach(id => { + .map(record => record[primaryKey!]) + .forEach((id: string | number) => { if (selectedRowKeys.indexOf(id) === -1) { checked = false; } else { indeterminate = true; } }); - attrs.onClick = makeChain(e => { + attrs.onClick = makeChain((e: UIEvent) => { e.stopPropagation(); }, attrs.onClick); - const userAttrs = (rowSelection.titleProps && rowSelection.titleProps()) || {}; + const userAttrs = (rowSelection!.titleProps && rowSelection!.titleProps()) || {}; if (checked) { indeterminate = false; @@ -182,26 +169,26 @@ export default function selection(BaseComponent) { ) : null, - rowSelection.titleAddons && rowSelection.titleAddons(), + rowSelection!.titleAddons && rowSelection!.titleAddons(), ]; }; - renderSelectionBody = (value, index, record) => { + renderSelectionBody = (value: unknown, index: number, record: RecordItem) => { const { rowSelection, primaryKey } = this.props; const { selectedRowKeys } = this.state; - const mode = rowSelection.mode ? rowSelection.mode : 'multiple'; - const checked = selectedRowKeys.indexOf(record[primaryKey]) > -1; + const mode = rowSelection!.mode ? rowSelection!.mode : 'multiple'; + const checked = selectedRowKeys.indexOf(record[primaryKey!] as string | number) > -1; const onChange = this.selectOneRow.bind(this, index, record); - const attrs = rowSelection.getProps ? rowSelection.getProps(record, index) || {} : {}; + const attrs = rowSelection!.getProps ? rowSelection!.getProps(record, index) || {} : {}; - attrs.onClick = makeChain(e => { + attrs.onClick = makeChain((e: UIEvent) => { e.stopPropagation(); }, attrs.onClick); return mode === 'multiple' ? ( @@ -211,26 +198,26 @@ export default function selection(BaseComponent) { ); }; - selectAllRow = (checked, e) => { + selectAllRow: NonNullable = (checked, e) => { const ret = [...this.state.selectedRowKeys], { rowSelection, primaryKey, dataSource, entireDataSource } = this.props, { selectedRowKeys } = this.state, - getProps = rowSelection.getProps; - let attrs = {}, - records = []; + getProps = rowSelection!.getProps; + let attrs: ReturnType> = {}, + records: RecordItem[] = []; const source = entireDataSource ? entireDataSource : dataSource; this.flatDataSource(source).forEach((record, index) => { - const id = record[primaryKey]; + const id = record[primaryKey!] as string | number; if (getProps) { attrs = getProps(record, index) || {}; } // 反选和全选的时候不要丢弃禁用项的选中状态 - if (checked && (!attrs.disabled || selectedRowKeys.indexOf(id) > -1)) { + if (checked && (!attrs!.disabled || selectedRowKeys.indexOf(id) > -1)) { ret.push(id); records.push(record); - } else if (attrs.disabled && selectedRowKeys.indexOf(id) > -1) { + } else if (attrs!.disabled && selectedRowKeys.indexOf(id) > -1) { ret.push(id); records.push(record); } else { @@ -240,19 +227,19 @@ export default function selection(BaseComponent) { }); records = unique(records, primaryKey); - if (typeof rowSelection.onSelectAll === 'function') { - rowSelection.onSelectAll(checked, records); + if (typeof rowSelection!.onSelectAll === 'function') { + rowSelection!.onSelectAll(checked, records); } - this.triggerSelection(rowSelection, unique(ret), records); + this.triggerSelection(rowSelection!, unique(ret), records); e.stopPropagation(); }; - selectOneRow(index, record, checked, e) { + selectOneRow(index: number, record: RecordItem, checked: boolean, e: UIEvent) { let selectedRowKeys = [...this.state.selectedRowKeys], i; const { primaryKey, rowSelection, dataSource, entireDataSource } = this.props, - mode = rowSelection.mode ? rowSelection.mode : 'multiple', - id = record[primaryKey]; + mode = rowSelection!.mode ? rowSelection!.mode : 'multiple', + id = record[primaryKey!] as string | number; if (id === null || id === undefined) { log.warning(`Can't get value from record using given ${primaryKey} as primaryKey.`); } @@ -267,19 +254,28 @@ export default function selection(BaseComponent) { selectedRowKeys = [id]; } let totalDS = dataSource; - if (Array.isArray(entireDataSource) && entireDataSource.length > dataSource.length) { + if (Array.isArray(entireDataSource) && entireDataSource.length > dataSource!.length) { totalDS = entireDataSource; } - const records = unique(totalDS.filter(item => selectedRowKeys.indexOf(item[primaryKey]) > -1), primaryKey); - if (typeof rowSelection.onSelect === 'function') { - rowSelection.onSelect(checked, record, records); + const records = unique( + totalDS!.filter( + item => selectedRowKeys.indexOf(item[primaryKey!] as string | number) > -1 + ), + primaryKey + ); + if (typeof rowSelection!.onSelect === 'function') { + rowSelection!.onSelect(checked, record, records); } - this.triggerSelection(rowSelection, selectedRowKeys, records); + this.triggerSelection(rowSelection!, selectedRowKeys, records); e.stopPropagation(); } - triggerSelection(rowSelection, selectedRowKeys, records) { + triggerSelection( + rowSelection: NonNullable, + selectedRowKeys: SelectionTableState['selectedRowKeys'], + records: RecordItem[] + ) { if (!('selectedRowKeys' in rowSelection)) { this.setState({ selectedRowKeys, @@ -290,16 +286,16 @@ export default function selection(BaseComponent) { } } - flatDataSource(dataSource) { - let ret = dataSource; + flatDataSource(dataSource: SelectionTableProps['dataSource']) { + let ret: RecordItem[] = dataSource!; const { listHeader } = this.context; if (listHeader) { ret = []; const { hasChildrenSelection, hasSelection } = listHeader; - dataSource.forEach(item => { + dataSource!.forEach(item => { const children = item.children; - // 如果需要渲染selection才将这条记录插入到dataSource + // 如果需要渲染 selection 才将这条记录插入到 dataSource // 或者没有孩子节点 if (hasSelection) { ret.push(item); @@ -313,9 +309,9 @@ export default function selection(BaseComponent) { } render() { - /* eslint-disable prefer-const */ - let { rowSelection, components, children, columns, ...others } = this.props; - let useColumns = columns && !children; + const { rowSelection, columns, ...others } = this.props; + let { components, children } = this.props; + const useColumns = columns && !children; if (rowSelection) { if (useColumns) { @@ -324,11 +320,17 @@ export default function selection(BaseComponent) { children = this.normalizeChildren(children || []); } components = { ...components }; - components.Row = components.Row || SelectionRow; + components.Row = components.Row || (SelectionRow as RowLike); } - return ; + return ( + + ); } } - statics(SelectionTable, BaseComponent); - return polyfill(SelectionTable); + return polyfill(statics(SelectionTable, BaseComponent)); } diff --git a/components/table/selection/row.jsx b/components/table/selection/row.tsx similarity index 68% rename from components/table/selection/row.jsx rename to components/table/selection/row.tsx index d75223ce71..52971c3ca9 100644 --- a/components/table/selection/row.jsx +++ b/components/table/selection/row.tsx @@ -2,9 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Row from '../expanded/row'; +import type { SelectionRowProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class SelectionRow extends React.Component { +export default class SelectionRow extends React.Component { static propTypes = { ...Row.propTypes, }; @@ -18,12 +18,11 @@ export default class SelectionRow extends React.Component { }; render() { - /* eslint-disable no-unused-vars*/ const { className, record, primaryKey } = this.props; const { selectedRowKeys } = this.context; const cls = classnames({ - selected: selectedRowKeys.indexOf(record[primaryKey]) > -1, - [className]: className, + selected: selectedRowKeys.indexOf(record[primaryKey!]) > -1, + [className!]: className, }); return ; } diff --git a/components/table/sticky.jsx b/components/table/sticky.tsx similarity index 67% rename from components/table/sticky.jsx rename to components/table/sticky.tsx index 15b6207a65..80f878701b 100644 --- a/components/table/sticky.jsx +++ b/components/table/sticky.tsx @@ -3,30 +3,18 @@ import PropTypes from 'prop-types'; import Header from './fixed/header'; import StickyHeader from './sticky/header'; import { statics } from './util'; +import type { StickyTableProps } from './types'; +import type Base from './base'; -export default function sticky(BaseComponent) { +export default function sticky(BaseComponent: typeof Base) { /** Table */ - class StickyTable extends React.Component { + class StickyTable extends React.Component { static StickyHeader = StickyHeader; static propTypes = { - /** - * 表头是否是sticky - */ - stickyHeader: PropTypes.bool, - /** - * 距离窗口顶部达到指定偏移量后触发 - */ - offsetTop: PropTypes.number, - /** - * affix组件的的属性 - */ - affixProps: PropTypes.object, - components: PropTypes.object, ...BaseComponent.propTypes, }; static defaultProps = { - components: {}, ...BaseComponent.defaultProps, }; @@ -40,20 +28,20 @@ export default function sticky(BaseComponent) { getChildContext() { return { - Header: this.props.components.Header || Header, + Header: this.props.components!.Header || Header, offsetTop: this.props.offsetTop, affixProps: this.props.affixProps, }; } render() { - /* eslint-disable no-unused-vars */ const { stickyHeader, offsetTop, affixProps, ...others } = this.props; let { components, maxBodyHeight, fixedHeader } = this.props; if (stickyHeader) { components = { ...components }; components.Header = StickyHeader; fixedHeader = true; + // @ts-expect-error maxBodyHeight 应先转为数字 maxBodyHeight = Math.max(maxBodyHeight, 10000); } return ( @@ -66,6 +54,5 @@ export default function sticky(BaseComponent) { ); } } - statics(StickyTable, BaseComponent); - return StickyTable; + return statics(StickyTable, BaseComponent); } diff --git a/components/table/sticky/header.jsx b/components/table/sticky/header.tsx similarity index 82% rename from components/table/sticky/header.jsx rename to components/table/sticky/header.tsx index 423ab6968a..6f9635e46c 100644 --- a/components/table/sticky/header.jsx +++ b/components/table/sticky/header.tsx @@ -2,9 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Affix from '../../affix'; +import type { StickyHeaderProps } from '../types'; -/* eslint-disable react/prefer-stateless-function*/ -export default class StickHeader extends React.Component { +export default class StickHeader extends React.Component { static propTypes = { prefix: PropTypes.string, }; @@ -14,7 +14,7 @@ export default class StickHeader extends React.Component { affixProps: PropTypes.object, }; - getAffixRef = ref => { + getAffixRef = (ref: InstanceType | null) => { this.props.affixRef && this.props.affixRef(ref); }; diff --git a/components/table/style.js b/components/table/style.ts similarity index 100% rename from components/table/style.js rename to components/table/style.ts diff --git a/components/table/tree.jsx b/components/table/tree.tsx similarity index 58% rename from components/table/tree.jsx rename to components/table/tree.tsx index 3de883c101..bbbd907a73 100644 --- a/components/table/tree.jsx +++ b/components/table/tree.tsx @@ -4,44 +4,16 @@ import { polyfill } from 'react-lifecycles-compat'; import RowComponent from './tree/row'; import CellComponent from './tree/cell'; import { statics } from './util'; +import type Base from './base'; +import type { CellLike, RecordItem, RowLike, TreeTableProps, TreeTableState } from './types'; const noop = () => {}; -export default function tree(BaseComponent) { - class TreeTable extends React.Component { +export default function tree(BaseComponent: typeof Base) { + class TreeTable extends React.Component { static TreeRow = RowComponent; static TreeCell = CellComponent; static propTypes = { - /** - * 默认情况下展开的树形表格,传入了此属性代表tree的展开为受控操作 - */ - openRowKeys: PropTypes.array, - /** - * 默认情况下展开的 Expand行 或者 Tree行,非受控模式 - * @version 1.23.22 - */ - defaultOpenRowKeys: PropTypes.array, - /** - * 点击tree展开或者关闭的时候触发的事件 - * @param {Array} openRowKeys tree模式下展开的key - * @param {String} currentRowKey 当前点击行的key - * @param {Boolean} opened 当前点击是展开还是收起 - * @param {Object} currentRecord 当前点击行的记录 - */ - onRowOpen: PropTypes.func, - /** - * dataSource当中数据的主键,如果给定的数据源中的属性不包含该主键,会造成选择状态全部选中 - */ - primaryKey: PropTypes.oneOfType([PropTypes.symbol, PropTypes.string]), - /** - * 在tree模式下的缩进尺寸, 仅在isTree为true时候有效 - */ - indent: PropTypes.number, - /** - * 开启Table的tree模式, 接收的数据格式中包含children则渲染成tree table - */ - isTree: PropTypes.bool, - locale: PropTypes.object, ...BaseComponent.propTypes, }; @@ -60,9 +32,10 @@ export default function tree(BaseComponent) { onTreeNodeClick: PropTypes.func, isTree: PropTypes.bool, }; + ds: RecordItem[]; - constructor(props, context) { - super(props, context); + constructor(props: TreeTableProps) { + super(props); this.state = { openRowKeys: props.openRowKeys || props.defaultOpenRowKeys || [], }; @@ -78,7 +51,7 @@ export default function tree(BaseComponent) { }; } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: TreeTableProps) { if ('openRowKeys' in nextProps) { return { openRowKeys: nextProps.openRowKeys || [], @@ -88,15 +61,19 @@ export default function tree(BaseComponent) { return null; } - normalizeDataSource(dataSource) { + normalizeDataSource(dataSource: TreeTableProps['dataSource']) { const { openRowKeys } = this.state; const { primaryKey } = this.props; - const ret = [], - loop = function(dataSource, level, parentId = null) { - dataSource.forEach(item => { + const ret: RecordItem[] = [], + loop = function ( + dataSource: TreeTableProps['dataSource'], + level: number, + parentId: string | number | null = null + ) { + dataSource!.forEach(item => { item.__level = level; - if (level === 0 || openRowKeys.indexOf(parentId) > -1) { + if (level === 0 || openRowKeys!.indexOf(parentId) > -1) { item.__hidden = false; } else { item.__hidden = true; @@ -104,7 +81,7 @@ export default function tree(BaseComponent) { ret.push(item); if (item.children) { - loop(item.children, level + 1, item[primaryKey]); + loop(item.children, level + 1, item[primaryKey!] as string); } }); }; @@ -113,17 +90,17 @@ export default function tree(BaseComponent) { return ret; } - getTreeNodeStatus(dataSource = []) { + getTreeNodeStatus(dataSource: RecordItem[] = []) { const { openRowKeys } = this.state, { primaryKey } = this.props, - ret = []; + ret: unknown[] = []; - openRowKeys.forEach(openKey => { + openRowKeys!.forEach(openKey => { dataSource.forEach(item => { - if (item[primaryKey] === openKey) { + if (item[primaryKey!] === openKey) { if (item.children) { item.children.forEach(child => { - ret.push(child[primaryKey]); + ret.push(child[primaryKey!]); }); } } @@ -132,24 +109,24 @@ export default function tree(BaseComponent) { return ret; } - onTreeNodeClick = record => { + onTreeNodeClick = (record: RecordItem) => { const { primaryKey } = this.props, - id = record[primaryKey], + id = record[primaryKey!] as string | number, dataSource = this.ds, - openRowKeys = [...this.state.openRowKeys], + openRowKeys = [...this.state.openRowKeys!], index = openRowKeys.indexOf(id), - getChildrenKeyById = function(id) { + getChildrenKeyById = function (id: string | number) { const ret = [id]; - const loop = data => { + const loop = (data: RecordItem[]) => { data.forEach(item => { - ret.push(item[primaryKey]); + ret.push(item[primaryKey!] as string | number); if (item.children) { loop(item.children); } }); }; dataSource.forEach(item => { - if (item[primaryKey] === id) { + if (item[primaryKey!] === id) { if (item.children) { loop(item.children); } @@ -159,7 +136,7 @@ export default function tree(BaseComponent) { }; if (index > -1) { - // 不仅要删除当前的openRowKey,还需要删除关联子节点的openRowKey + // 不仅要删除当前的 openRowKey,还需要删除关联子节点的 openRowKey const ids = getChildrenKeyById(id); ids.forEach(id => { const i = openRowKeys.indexOf(id); @@ -176,20 +153,21 @@ export default function tree(BaseComponent) { openRowKeys, }); } - this.props.onRowOpen(openRowKeys, id, index === -1, record); + this.props.onRowOpen!(openRowKeys, id, index === -1, record); }; render() { - /* eslint-disable no-unused-vars, prefer-const */ - let { components, isTree, dataSource, indent, ...others } = this.props; + const { isTree, indent, ...others } = this.props; + + let { components, dataSource } = this.props; if (isTree) { components = { ...components }; if (!components.Row) { - components.Row = RowComponent; + components.Row = RowComponent as RowLike; } if (!components.Cell) { - components.Cell = CellComponent; + components.Cell = CellComponent as CellLike; } dataSource = this.normalizeDataSource(dataSource); @@ -197,6 +175,5 @@ export default function tree(BaseComponent) { return ; } } - statics(TreeTable, BaseComponent); - return polyfill(TreeTable); + return polyfill(statics(TreeTable, BaseComponent)); } diff --git a/components/table/tree/cell.jsx b/components/table/tree/cell.tsx similarity index 73% rename from components/table/tree/cell.jsx rename to components/table/tree/cell.tsx index 996b563390..edb47ce014 100644 --- a/components/table/tree/cell.jsx +++ b/components/table/tree/cell.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { type KeyboardEvent, type UIEvent } from 'react'; import PropTypes from 'prop-types'; import Icon from '../../icon'; import { KEYCODE } from '../../util'; import CellComponent from '../base/cell'; +import type { RecordItem, TreeCellProps } from '../types'; -export default class TreeCell extends React.Component { +export default class TreeCell extends React.Component { static propTypes = { indent: PropTypes.number, locale: PropTypes.object, @@ -25,12 +26,12 @@ export default class TreeCell extends React.Component { rowSelection: PropTypes.object, }; - onTreeNodeClick = (record, e) => { + onTreeNodeClick = (record: RecordItem, e: UIEvent) => { e.stopPropagation(); this.context.onTreeNodeClick(record); }; - expandedKeydown = (record, e) => { + expandedKeydown = (record: RecordItem, e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); @@ -49,13 +50,18 @@ export default class TreeCell extends React.Component { if (isTree) { const paddingType = rtl ? 'paddingRight' : 'paddingLeft'; firstCellStyle = { - [paddingType]: indent * (record.__level + 1), + [paddingType]: indent * ((record!.__level as number) + 1), }; treeArrowNode = ( - + ); - if (record.children && record.children.length) { - const hasExpanded = openRowKeys.indexOf(record[primaryKey]) > -1; + if (record!.children && record!.children.length) { + const hasExpanded = openRowKeys.indexOf(record![primaryKey!]) > -1; treeArrowType = hasExpanded ? 'arrow-down' : 'arrow-right'; @@ -65,12 +71,12 @@ export default class TreeCell extends React.Component { type={treeArrowType} size="xs" rtl={rtl} - onClick={e => this.onTreeNodeClick(record, e)} - onKeyDown={e => this.expandedKeydown(record, e)} + onClick={e => this.onTreeNodeClick(record!, e)} + onKeyDown={e => this.expandedKeydown(record!, e)} role="button" - tabIndex="0" + tabIndex={0} aria-expanded={hasExpanded} - aria-label={hasExpanded ? locale.expanded : locale.folded} + aria-label={hasExpanded ? locale!.expanded : locale!.folded} /> ); } diff --git a/components/table/tree/row.jsx b/components/table/tree/row.tsx similarity index 54% rename from components/table/tree/row.jsx rename to components/table/tree/row.tsx index ff69123717..d56bf4accb 100644 --- a/components/table/tree/row.jsx +++ b/components/table/tree/row.tsx @@ -2,9 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Row from '../selection/row'; +import type { TreeRowProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class TreeRow extends React.Component { +export default class TreeRow extends React.Component { static propTypes = { ...Row.propTypes, }; @@ -19,15 +19,22 @@ export default class TreeRow extends React.Component { }; render() { - /* eslint-disable no-unused-vars*/ const { className, record, primaryKey, prefix, ...others } = this.props; const { treeStatus, openRowKeys } = this.context; const cls = classnames({ - hidden: !(treeStatus.indexOf(record[primaryKey]) > -1) && record.__level !== 0, + hidden: !(treeStatus.indexOf(record[primaryKey!]) > -1) && record.__level !== 0, [`${prefix}table-row-level-${record.__level}`]: true, - opened: openRowKeys.indexOf(record[primaryKey]) > -1, - [className]: className, + opened: openRowKeys.indexOf(record[primaryKey!]) > -1, + [className!]: className, }); - return ; + return ( + + ); } } diff --git a/components/table/types.ts b/components/table/types.ts new file mode 100644 index 0000000000..8ff5557f7d --- /dev/null +++ b/components/table/types.ts @@ -0,0 +1,1107 @@ +import type React from 'react'; +import type { CommonProps } from '../util'; +import type { AffixProps } from '../affix'; +import type { Locale } from '../locale/types'; +import type { MenuProps } from '../menu'; +import type { DropdownProps } from '../dropdown'; +import type { CheckboxProps } from '../checkbox'; +import type { RadioProps } from '../radio'; + +interface HTMLAttributesWeak + extends Omit, 'title' | 'children'> {} + +export type FilterItem = { value?: React.Key; label: React.ReactNode; children?: FilterItem[] }; + +/** + * @api RecordItem + * @remarks RecordItem 也可能是单纯 string 的情况,但考虑到这样业务的适配成本比较高,因此这里不加入 + * - + * Record may be string, but it's hard to adapt the business, so it's not added + */ +export type RecordItem = Record & { children?: RecordItem[] }; + +export interface FilterProps + extends Pick< + ColumnProps, + 'filters' | 'filterMode' | 'filterMenuProps' | 'filterProps' | 'rtl' | 'locale' | 'prefix' + > { + filterParams?: TableProps['filterParams']; + dataIndex: ColumnProps['dataIndex']; + onFilter?: TableProps['onFilter']; + className?: string; +} + +export interface FilterState { + visible: boolean; + selectedKeys: string[]; + selectedKeysChangedByInner: boolean; +} + +/** + * @api + */ +export type SortOrder = 'desc' | 'asc' | 'default'; + +export interface SortProps + extends Pick { + sort?: TableProps['sort']; + sortIcons?: TableProps['sortIcons']; + className?: string; + dataIndex: ColumnProps['dataIndex']; + onSort: NonNullable; +} + +/** + * @api CellProps + */ +export interface CellProps + extends Pick< + ColumnProps, + | 'cell' + | 'resizable' + | 'asyncResizable' + | 'align' + | 'title' + | 'width' + | 'filterMode' + | 'filters' + | 'sortable' + | 'sortDirections' + | 'lock' + | 'locale' + | 'rtl' + | 'htmlTitle' + | 'wordBreak' + | 'filterMenuProps' + | 'filterProps' + | 'colSpan' + > { + pure?: boolean; + prefix: TableProps['prefix']; + className?: string; + value?: unknown; + record?: RecordItem; + context?: unknown; + colIndex?: number; + rowIndex?: number; + __colIndex?: number | string; + style?: React.CSSProperties; + component?: React.ElementType; + children?: React.ReactNode; + innerStyle?: React.CSSProperties; + __normalized?: boolean; + expandedIndexSimulate?: TableProps['expandedIndexSimulate']; + getCellDomRef?: React.LegacyRef; + primaryKey?: TableProps['primaryKey']; + rowSpan?: number; +} + +export interface ResizeProps extends Pick { + dataIndex: NonNullable; + tableEl: HTMLElement | null; + resizeProxyDomRef: HTMLElement | null; + onChange: (dataIndex: ResizeProps['dataIndex'], changedPageX: number) => void; + col: ColumnProps; + cellDomRef: { current?: HTMLElement }; + hasLock: boolean; +} + +export interface HeaderProps extends CommonProps { + columns: (ColumnProps & + InnerColumnProps & { + ref?: React.Ref; + })[][]; + headerCellRef: (i: number, j: number, cell: InstanceType | null) => void; + components?: TableProps['components']; + onSort: NonNullable; + className?: string; + children?: React.ReactNode; + colGroup?: React.ReactElement; + filterParams?: FilterProps['filterParams']; + // 这个属性未实现 + affixRef?: unknown; + sort?: SortProps['sort']; + sortIcons?: SortProps['sortIcons']; + tableWidth?: TableProps['tableWidth']; + tableEl?: ResizeProps['tableEl']; + resizeProxyDomRef?: ResizeProps['resizeProxyDomRef']; + component?: React.ElementType; + onFilter?: FilterProps['onFilter']; + onResizeChange?: TableProps['onResizeChange']; +} + +export interface FixedHeaderProps extends HeaderProps {} + +export interface FixedHeaderContext { + getNode: (type: string, node: HTMLElement, lockType?: 'left' | 'right') => void; + onFixedScrollSync: (e: React.UIEvent) => void; + lockType: 'left' | 'right'; +} + +export interface LockHeaderContext extends FixedHeaderContext { + getLockNode: (type: string, node: HTMLElement, lockType: LockHeaderContext['lockType']) => void; +} + +export interface StickyHeaderProps extends HeaderProps { + affixRef: (ref: InstanceType> | null) => void; +} +export type ComponentClassLike = React.ComponentClass>; +export type ComponentTypeLike = React.ComponentType>; +export type CellLike = ComponentClassLike; +export type FilterLike = ComponentTypeLike; +export type SortLike = ComponentTypeLike; +export type ResizeLike = ComponentTypeLike; +export type RowLike = ComponentClassLike; +export type HeaderLike = ComponentTypeLike; +export type BodyLike = ComponentTypeLike; +export type WrapperLike = ComponentClassLike; + +/** + * @api RowProps + */ +export interface RowProps + extends Pick< + BodyProps, + | 'prefix' + | 'primaryKey' + | 'columns' + | 'pure' + | 'locale' + | 'rtl' + | 'getCellProps' + | 'cellRef' + | 'expandedIndexSimulate' + | 'tableEl' + | 'colGroup' + >, + Omit { + className?: string; + rowIndex: number; + __rowIndex: number; + onClick: (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void; + onMouseEnter: (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void; + onMouseLeave: (record: RecordItem, rowIndex: number, e: React.MouseEvent) => void; + Cell: CellLike; + children: React.ReactNode; + record: RecordItem; + wrapper: (wrapper: React.ReactElement) => React.ReactNode; +} + +export interface BodyProps + extends Pick< + TableProps, + | 'loading' + | 'emptyContent' + | 'components' + | 'prefix' + | 'getCellProps' + | 'primaryKey' + | 'getRowProps' + | 'dataSource' + | 'onRowClick' + | 'onRowMouseEnter' + | 'onRowMouseLeave' + | 'locale' + | 'pure' + | 'expandedIndexSimulate' + | 'rtl' + | 'crossline' + | 'tableWidth' + > { + onBodyMouseOver: (e: React.MouseEvent) => void; + onBodyMouseOut: (e: React.MouseEvent) => void; + tableEl: HTMLElement | null; + className?: string; + component?: React.ElementType; + colGroup?: React.ReactElement; + rowRef: ( + i: number | string, + row?: InstanceType | null + ) => InstanceType | undefined | null; + cellRef: ( + __rowIndex: number, + colIndex: number, + cell: InstanceType | HTMLTableCellElement | null + ) => void; + children?: React.ReactNode; + columns: (ColumnProps & InnerColumnProps)[]; +} + +export interface FixedBodyProps extends BodyProps { + onLockScroll: (e: React.UIEvent) => void; +} + +export interface FixedBodyContext { + getNode: (type: string, node: HTMLElement) => void; + onFixedScrollSync: (e: React.UIEvent) => void; + maxBodyHeight: number | string; + fixedHeader: boolean; +} + +export interface LockBodyProps extends FixedBodyProps {} + +export interface LockBodyContext extends FixedBodyContext { + onLockScroll: (e: React.UIEvent) => void; + getLockNode: (type: string, node: HTMLElement, lockType: LockBodyContext['lockType']) => void; + lockType: 'left' | 'right'; + onLockBodyScroll: (e: React.UIEvent) => void; +} + +export interface ListBodyProps extends BodyProps {} + +export interface ListBodyContext { + getNode: (type: string, node: HTMLElement) => void; + onFixedScrollSync: (e: React.UIEvent) => void; +} + +export interface VirtualBodyProps extends BodyProps {} + +export interface WrapperProps extends Pick { + colGroup: React.ReactElement; + component?: React.ElementType; + children?: React.ReactNode; +} + +export interface FixedWrapperProps extends Pick { + wrapperContent?: React.ReactNode; +} + +export interface InnerColumnProps { + __normalized?: CellProps['__normalized']; + __colIndex?: number; + cellStyle?: React.CSSProperties; +} + +/** + * @api Table.Column + * @order 1 + */ +export interface ColumnProps extends HTMLAttributesWeak, CommonProps { + /** + * 指定列对应的字段,支持`a.b`形式的快速取值 + * @en Specifies the field corresponding to the column, supporting `a.b` quick retrieval + */ + dataIndex?: string; + + /** + * 行渲染的逻辑 + * @en Row rendering logic + */ + cell?: + | React.ReactNode + | ((value: unknown, rowIndex: number, record: RecordItem) => React.ReactNode); + + /** + * 表头显示的内容 + * @en The content displayed in the header + */ + title?: React.ReactNode | (() => React.ReactNode); + + /** + * 写到 header 单元格上的 title 属性 + * @en The title attribute written to the header cell + */ + htmlTitle?: string; + + /** + * 是否支持排序 + * @en Whether to support sorting + */ + sortable?: boolean; + + /** + * 列宽,注意在锁列的情况下一定需要配置宽度 + * @en Column width, note that the width must be configured in the lock column + */ + width?: number | string; + + /** + * 单元格的对齐方式 + * @en The alignment of the cell + */ + align?: 'left' | 'center' | 'right'; + + /** + * 排序的方向。设置 ['desc', 'asc'],表示降序、升序。设置 ['desc', 'asc', 'default'],表示表示降序、升序、不排序 + * @en The direction of sorting. Set ['desc', 'asc'] to indicate descending and ascending. Set ['desc', 'asc', 'default'] to indicate descending, ascending, and not sorting + * @version 1.23 + */ + sortDirections?: Array; + + /** + * 标题单元格的对齐方式,如果不配置,默认读取 align 值 + * @en The alignment of the title cell, if not configured, the default value is read from align + */ + alignHeader?: 'left' | 'center' | 'right'; + + /** + * 生成标题过滤的菜单,格式为`[{label:'xxx', value:'xxx'}]` + * @en Generate the menu for title filtering, the format is `[{label: 'xxx', value: 'xxx'}]` + */ + filters?: FilterItem[]; + + /** + * 过滤的模式是单选还是多选 + * @en The mode of filtering is single or multiple + * @defaultValue 'multiple' + */ + filterMode?: 'single' | 'multiple'; + + /** + * 是否支持锁列,可选值为`left`,`right`, `true` + * @en Whether to support locking, the value can be `left`, `right`, `true` + */ + lock?: boolean | string; + + /** + * 是否支持列宽调整,当该值设为 true,table 的布局方式会修改为 fixed. + * @en Whether to support column width adjustment, when this value is set to true, the layout of table will be modified to fixed. + * @defaultValue false + */ + resizable?: boolean; + /** + * (推荐使用)是否支持异步列宽调整,当该值设为 true,table 的布局方式会修改为 fixed. + * @en (Recommended) Whether to support asynchronous column width adjustment, when this value is set to true, the layout of table will be modified to fixed. + * @defaultValue false + * @version 1.24 + */ + asyncResizable?: boolean; + + /** + * header cell 横跨的格数,设置为 0 表示不出现此 th + * @en The number of cells that the header cell spans, set to 0 to not appear this th + */ + colSpan?: number | string; + + /** + * 设置该列单元格的 word-break 样式,对于 id 类、中文类适合用 all,对于英文句子适合用 word + * @en Set the word-break style of the column cell, for id class and Chinese class, it is recommended to use all, for English sentences, it is recommended to use word + * @version 1.23 + */ + wordBreak?: 'all' | 'word'; + /** + * filter 模式下传递给 Menu 菜单的属性,默认继承 `Menu` 组件的 API + * @en The properties passed to the Menu menu in the filter mode, the default inherits the API of the Menu component + * @defaultValue \{ subMenuSelectable: false \} + */ + filterMenuProps?: MenuProps & { subMenuSelectable?: boolean }; + /** + * 传递给 Filter 的下拉组件的属性 + * @en The properties passed to the Filter Dropdown component + */ + filterProps?: DropdownProps; + /** + * @skip + */ + locale?: TableProps['locale']; + /** + * @skip + */ + headerCellRowIndex?: number; + /** + * @skip + */ + headerCellColIndex?: number; +} + +export type TableChildProps = ColumnProps | ColumnGroupProps | GroupHeaderProps | GroupFooterProps; + +export type NormalizedColumnProps = Omit< + ColumnProps & InnerColumnProps & ColumnGroupProps & GroupHeaderProps & GroupFooterProps, + 'title' | 'children' +> & { + ref?: React.Ref; + children?: NormalizedColumnProps[]; + title?: ColumnProps['title']; + key?: string; +}; + +export interface LockRowProps extends RowProps {} +export interface ExpandedRowProps extends LockRowProps {} + +export interface SelectionRowProps extends ExpandedRowProps {} + +export interface TreeRowProps extends SelectionRowProps {} + +export interface ListRowContext { + notRenderCellIndex: number[]; + lockType: 'left' | 'right'; + listHeader: GroupHeaderProps; + listFooter: GroupFooterProps; + rowSelection: TableProps['rowSelection']; +} + +export interface TreeCellProps extends CellProps { + locale?: TableProps['locale']; +} + +/** + * @api Table.ColumnGroup + * @order 2 + */ +export interface ColumnGroupProps { + /** + * 表头显示的内容 + * @en The content displayed in the header + */ + title?: React.ReactNode | (() => React.ReactNode); + /** + * @skip + */ + children?: + | React.ReactElement + | Array>; +} + +/** + * @api Table.GroupHeader + * @order 3 + */ +export interface GroupHeaderProps extends HTMLAttributesWeak, CommonProps { + /** + * 行渲染的逻辑 + * @en Row rendering logic + */ + cell?: React.ReactNode | ((value: RecordItem, index: number) => React.ReactNode); + + /** + * 是否在 Children 上面渲染 selection + * @en Whether to render selection on Children + * @defaultValue false + */ + hasChildrenSelection?: boolean; + + /** + * 是否在 GroupHeader 上面渲染 selection + * @en Whether to render selection on GroupHeader + * @defaultValue true + */ + hasSelection?: boolean; + + /** + * 当 dataSource 里没有 children 时,是否使用内容作为数据 + * @en When there is no children in dataSource, whether to use content as data + * @defaultValue false + */ + useFirstLevelDataWhenNoChildren?: boolean; +} + +/** + * @api Table.GroupFooter + * @order 4 + */ +export interface GroupFooterProps extends HTMLAttributesWeak, CommonProps { + /** + * 行渲染的逻辑 + * @en Row rendering logic + */ + cell?: React.ReactNode | ((value: RecordItem, index: number) => React.ReactNode); +} + +export interface BaseTableProps + extends Pick< + TableProps, + | 'sort' + | 'pure' + | 'columns' + | 'children' + | 'filterParams' + | 'emptyContent' + | 'rtl' + | 'sortIcons' + | 'tableWidth' + | 'components' + | 'className' + | 'style' + | 'onSort' + | 'onFilter' + | 'onResizeChange' + | 'scrollToRow' + | 'loadingComponent' + | 'tableLayout' + | 'dataSource' + | 'primaryKey' + | 'prefix' + | 'locale' + | 'getRowProps' + > { + lockType?: 'left' | 'right'; + lengths: { + left: number; + right: number; + origin: number; + }; + wrapperContent?: WrapperProps['children']; + entireDataSource: NonNullable; +} + +export interface BaseTableState { + sort: BaseTableProps['sort']; +} + +export interface BaseTableContext {} + +export interface TreeTableProps + extends BaseTableProps, + Pick< + TableProps, + 'openRowKeys' | 'defaultOpenRowKeys' | 'indent' | 'isTree' | 'onRowOpen' + > {} + +export interface TreeTableState { + openRowKeys?: TreeTableProps['openRowKeys']; +} + +export interface FixedTableProps + extends BaseTableProps, + Pick {} + +export interface SelectionTableProps + extends BaseTableProps, + Pick {} + +export interface SelectionTableState { + selectedRowKeys: (string | number)[]; +} + +export interface ExpandedTableProps + extends BaseTableProps, + Pick< + TableProps, + | 'openRowKeys' + | 'defaultOpenRowKeys' + | 'expandedRowRender' + | 'expandedIndexSimulate' + | 'expandedRowIndent' + | 'getExpandedColProps' + | 'rowExpandable' + | 'onRowOpen' + | 'size' + | 'hasExpandedRowCtrl' + | 'onExpandedRowClick' + > {} + +export interface VirtualTableProps + extends BaseTableProps, + Pick< + TableProps, + | 'rowHeight' + | 'useVirtual' + | 'maxBodyHeight' + | 'rowSelection' + | 'keepForwardRenderRows' + | 'onBodyScroll' + | 'fixedHeader' + > {} + +export interface VirtualTableState { + height: string | number | undefined; + scrollToRow: number | undefined; + hasVirtualData: boolean | undefined; + useVirtual?: boolean; + dataSource?: RecordItem[]; + rowHeight: VirtualTableProps['rowHeight'] | null; +} + +export interface LockTableProps + extends BaseTableProps, + Pick {} + +export interface StickyLockTableProps extends BaseTableProps, Pick {} + +export interface StickyLockTableState { + leftOffsetArr?: number[]; + rightOffsetArr?: number[]; + hasLockLeft?: boolean; + hasLockRight?: boolean; +} + +export interface ListTableProps extends BaseTableProps, Pick {} + +export interface StickyTableProps + extends BaseTableProps, + Pick< + TableProps, + 'offsetTop' | 'affixProps' | 'stickyHeader' | 'maxBodyHeight' | 'fixedHeader' + > {} + +export interface DeprecatedProps { + /** + * @deprecated use `openRowKeys` instead + */ + expandedRowKeys?: TableProps['openRowKeys']; + /** + * @deprecated use `onRowOpen` instead + */ + onExpandedChange?: TableProps['onRowOpen']; + /** + * @deprecated use `loading` instead + */ + isLoading?: TableProps['loading']; + /** + * @deprecated use `indent` instead + */ + indentSize?: TableProps['indent']; + /** + * @deprecated use `pure` instead + */ + optimization?: TableProps['pure']; + /** + * @deprecated use `rowProps` instead + */ + getRowClassName?: (...args: Parameters>) => string; + /** + * @deprecated use `rowProps` instead + */ + getRowProps?: TableProps['rowProps']; + /** + * @deprecated use `cellProps` instead + */ + getCellProps?: TableProps['cellProps']; +} + +/** + * @api + */ +export interface RowSelection { + /** + * 获取 selection 的默认属性 + * @en Get the default properties of the selection + */ + getProps?: ( + record: RecordItem, + index: number + ) => CheckboxProps | RadioProps | void | undefined | null; + /** + * 选择改变的时候触发的事件,**注意:** 其中 records 只会包含当前 dataSource 的数据,很可能会小于 selectedRowKeys 的长度。 + * @en The event triggered when the selection changes. **Note:** records will only contain the data in the dataSource, and it may be less than the length of selectedRowKeys. + */ + onChange?: (selectedRowKeys: Array, records: Array) => void; + /** + * 用户手动选择/取消选择某行的回调 + * @en The callback when the selection changes + */ + onSelect?: (selected: boolean, record: RecordItem, records: RecordItem[]) => void; + /** + * 用户手动选择/取消选择所有行的回调 + * @en The callback when the select all button is clicked + */ + onSelectAll?: (selected: boolean, records: RecordItem[]) => void; + /** + * 设置了此属性,将 rowSelection 变为受控状态,接收值为该行数据的 primaryKey 的值 + * @en Set the rowSelection to be controlled, the value is the primaryKey value of the row + */ + selectedRowKeys?: Array; + /** + * 选择 selection 的模式 + * @en The mode of the selection + * @defaultValue 'multiple' + */ + mode?: 'single' | 'multiple'; + /** + * 选择列表头的 props,仅在 `multiple` 模式下生效 + * @en The props of the selection header, only effective in `multiple` mode + */ + titleProps?: () => CheckboxProps; + /** + * 选择列的 props,例如锁列、对齐等,可使用 `Table.Column` 的所有参数 + * @en The props of the selection column, such as lock, alignment, etc., you can use all parameters of Table.Column + */ + columnProps?: () => Partial; + /** + * 选择列表头添加的元素,在`single` `multiple` 下都生效 + * @en The elements added to the selection header, effective in `single` and `multiple` + */ + titleAddons?: () => React.ReactNode; +} + +/** + * @api Table + * @order 0 + */ +export interface TableProps + extends React.HTMLAttributes, + CommonProps, + DeprecatedProps { + /** + * 尺寸,small 为紧凑模式 + * @en size of table, small is compact mode + */ + size?: 'small' | 'medium'; + + /** + * 自定义类名 + * @en custom class name + */ + className?: string; + + /** + * 自定义内联样式 + * @en custom inline style + */ + style?: React.CSSProperties; + /** + * 等同于写子组件 Table.Column,子组件优先级更高 + * @en equivalent to writing child components Table.Column, child components have higher priority + */ + columns?: Array; + /** + * 表格元素的 table-layout 属性,设为 fixed 表示内容不会影响列的布局 + * @en the table-layout attribute of the table element, set to fixed means that the content does not affect the layout of the column + */ + tableLayout?: 'fixed' | 'auto'; + /** + * 表格的总长度,可以这么用:设置表格总长度、设置部分列的宽度,这样表格会按照剩余空间大小,自动其他列分配宽度 + * @en the total length of the table, you can use it like this: set the total length of the table, set the width of some columns, and then the table will automatically allocate the width of the remaining space + */ + tableWidth?: number; + /** + * 表格展示的数据源 + * @en the data source of the table + */ + dataSource?: Array; + + /** + * 表格是否具有边框 + * @en whether the table has borders + * @defaultValue true + */ + hasBorder?: boolean; + + /** + * 表格是否具有头部 + * @en whether the table has a header + * @defaultValue true + */ + hasHeader?: boolean; + + /** + * 表格是否是斑马线 + * @en whether the table is zebra + * @defaultValue false + */ + isZebra?: boolean; + + /** + * 表格是否在加载中 + * @en whether the table is loading + * @defaultValue false + */ + loading?: boolean; + + /** + * 设置数据为空的时候的表格内容展现 + * @en the content to be displayed when the data is empty + */ + emptyContent?: React.ReactNode; + + /** + * dataSource 中的主键,如果给定的数据源中的属性不包含该主键,会造成所有行全部选中等一系列问题 + * @en the primary key in the data source, if the property in the given data source does not contain the primary key, it will cause all rows to be selected, etc. + * @defaultValue 'id' + */ + primaryKey?: string; + /** + * 点击表格每一行触发的事件 + * @en the event on clicking on a table row + */ + onRowClick?: (record: RecordItem, index: number, e: React.MouseEvent) => void; + + /** + * 悬浮在表格每一行的时候触发的事件 + * @en the event on hovering on a table row + */ + onRowMouseEnter?: (record: RecordItem, index: number, e: React.MouseEvent) => void; + + /** + * 离开表格每一行的时候触发的事件 + * @en the event on leaving a table row + */ + onRowMouseLeave?: (record: RecordItem, index: number, e: React.MouseEvent) => void; + + /** + * 点击列排序触发的事件 + * @en the event on clicking on a column sort + */ + onSort?: (dataIndex: string, order: SortOrder, sort: { [key: string]: SortOrder }) => void; + + /** + * 点击过滤确认按钮触发的事件 + * @en the event on clicking on the filter confirm button + */ + onFilter?: (filterParams: { + [propName: string]: { selectedKeys: string[]; visible: boolean }; + }) => void; + + /** + * 重设列尺寸的时候触发的事件 + * @en the event on resizing the column + */ + onResizeChange?: (dataIndex: string, value: number) => void; + + /** + * 设置每一行的属性,如果返回值和其他针对行操作的属性冲突则无效。 + * @en set the props of each row, if the return value conflicts with other attributes for row operations, it will be invalid. + */ + rowProps?: ( + record: RecordItem, + index: number + ) => (Record & Partial) | undefined | void; + + /** + * 设置单元格的属性,通过该属性可以进行合并单元格 + * @en set the props of each cell, you can use this property to merge cells + */ + cellProps?: ( + rowIndex: number | string, + colIndex: number | string, + dataIndex: string, + record: RecordItem + ) => + | Partial< + Omit< + CellProps, + | 'prefix' + | 'pure' + | 'primaryKey' + | 'record' + | 'value' + | 'colIndex' + | 'rowIndex' + | 'align' + | 'locale' + | 'rtl' + | 'width' + > + > + | undefined + | void; + + /** + * 自定义 Loading 组件 + * @en custom loading component + * @remarks 请务必传递 props, 使用方式:loadingComponent=\{props =\> \\} + * - + * please pass props, usage: loadingComponent=\{props =\> \\} + */ + loadingComponent?: React.ElementType<{ [prop: string]: unknown; className?: string }>; + + /** + * 当前过滤的 keys,使用此属性可以控制表格的头部的过滤选项中哪个菜单被选中,格式为 \{dataIndex: \{selectedKeys:[]\}\} + * @en the current filtered keys, using this property can control which menu in the filter menu of the table's header is selected, the format is \{dataIndex: \{selectedKeys: []\}\} + * @remarks + * 示例: + * 假设要控制 dataIndex 为 id 的列的过滤菜单中 key 为 one 的菜单项选中 + * `
` + * - + * Example: + * Assume you want to control the menu item with key 'one' in the filter menu of the dataIndex column + * `
` + */ + filterParams?: { [propName: string]: { selectedKeys?: string[]; visible?: boolean } } | null; + + /** + * 当前排序的字段,使用此属性可以控制表格的字段的排序,格式为\{dataIndex: 'asc'\} + * @en the current sorted field, using this property can control the sorting of the table's fields, the format is \{dataIndex: 'asc'\} + */ + sort?: { [key: string]: SortOrder } | null; + + /** + * 自定义排序按钮,例如上下排布的:`{desc: , asc: }` + * @en custom sort button, for example: `{desc: , asc: }` + */ + sortIcons?: { desc?: React.ReactNode; asc?: React.ReactNode }; + + /** + * 额外渲染行的渲染函数 + * @en render function for extra rows + */ + expandedRowRender?: (record: RecordItem, index: number) => React.ReactNode; + + /** + * 设置行是否可展开,设置 false 为不可展开 + * @en set whether the row is expandable, set false to disable expansion + */ + rowExpandable?: (record: RecordItem, index: number) => boolean; + + /** + * 额外渲染行的缩进,包含两个数字,第一个数字为左侧缩进,第二个数字为右侧缩进 + * @en the indent of extra rows, contains two numbers, the first number is the left indent, the second number is the right indent + * @defaultValue stickyLock ? [0, 0] : [1, 0] + */ + expandedRowIndent?: [number, number]; + + /** + * 展开的行,传入后展开状态只受此属性控制 + * @en expanded row, after passing, the expanded state is only controlled by this property. + */ + openRowKeys?: Array | null; + /** + * 默认展开的行 + * @en the default expanded row + * @version 1.23.22 + */ + defaultOpenRowKeys?: Array; + + /** + * 是否显示点击展开额外渲染行的 + 号按钮 + * @en whether to show the + button to expand the extra row + * @defaultValue true + */ + hasExpandedRowCtrl?: boolean; + + /** + * 设置额外的列属性 + * @en set the props of extra rows + */ + getExpandedColProps?: ( + record: RecordItem, + index: number + ) => + | (React.DetailedHTMLProps, HTMLSpanElement> & { + disabled?: boolean; + }) + | void; + + /** + * 在额外渲染行或者树展开或者收起的时候触发的事件 + * @en the event on expanding or collapsing the extra row or tree + */ + onRowOpen?: ( + openRowKeys: Array, + currentRowKey: string | number, + expanded: boolean, + currentRecord: RecordItem + ) => void; + + /** + * 点击额外渲染行触发的事件 + * @en the event on clicking on the extra row + */ + onExpandedRowClick?: (record: RecordItem, index: number, e: React.MouseEvent) => void; + + /** + * 表头是否固定,该属性配合 maxBodyHeight 使用,当内容区域的高度超过 maxBodyHeight 的时候,在内容区域会出现滚动条 + * @en whether the header is fixed, this property is combined with maxBodyHeight, when the content area's height exceeds maxBodyHeight, a scroll bar will appear in the content area + * @defaultValue false + */ + fixedHeader?: boolean; + + /** + * 最大内容区域的高度,在`fixedHeader`为`true`的时候,超过这个高度会出现滚动条 + * @en the maximum height of the content area, when `fixedHeader` is `true`, a scroll bar will appear when the height of the content area exceeds this height + * @defaultValue 200 + */ + maxBodyHeight?: number | string; + + /** + * 是否启用选择模式 + * @en whether to enable selection mode + */ + rowSelection?: RowSelection | null; + + /** + * 表头是否是 sticky + * @en whether the header is sticky + */ + stickyHeader?: boolean; + + /** + * 表头在距离窗口顶部达到此属性指定的偏移量后触发 sticky,仅在 stickyHeader 为 true 的时候有效 + * @en the header triggers sticky after the distance from the top of the window reaches this property, only valid when stickyHeader is true + */ + offsetTop?: number; + + /** + * Affix 组件的的属性,stickyHeader 基于 Affix 组件实现。 + * @en the props of affix component, stickyHeader is based on Affix component. + */ + affixProps?: AffixProps; + + /** + * 在 tree 模式下的缩进尺寸,仅在 isTree 为 true 时候有效 + * @en the indent size of tree mode, only valid when isTree is true + * @defaultValue 12 + */ + indent?: number; + + /** + * 开启 Table 的 tree 模式,接收的数据格式中包含 children 则渲染成 tree table + * @en enable tree mode of table, the data format received contains children and is rendered as tree table + */ + isTree?: boolean; + + /** + * 是否开启虚拟滚动 + * @en enable virtual scroll + */ + useVirtual?: boolean; + + /** + * 滚动到指定行 + * @en scroll to specified row + * @version 1.22.15 + */ + scrollToRow?: number; + + /** + *滚动到指定列 + @en scroll to specified column + */ + scrollToCol?: number; + + /** + * 设置行高 + * @en set the row height + */ + rowHeight?: number | (() => number); + + /** + * 在内容区域滚动的时候触发的函数 + * @en the function triggered when the content area is scrolled + */ + onBodyScroll?: (start: number) => void; + + /** + * 开启时,getExpandedColProps() / getRowProps() / expandedRowRender() 的第二个参数 index (该行所对应的序列) 将按照 0,1,2,3,4...的顺序返回,否则返回真实 index(0,2,4,6... / 1,3,5,7...) + * @en when enabled, the second parameter of getExpandedColProps() / getRowProps() / expandedRowRender() will return 0,1,2,3,4... in order, otherwise return the real index (0,2,4,6... / 1,3,5,7...) + * @defaultValue false + */ + expandedIndexSimulate?: boolean; + /** + * 在 hover 时出现十字参考轴,适用于表头比较复杂,需要做表头分类的场景。 + * @en when hover, a cross reference axis appears, suitable for complex table headers that need to do header classification. + * @defaultValue false + */ + crossline?: boolean; + + /** + * 虚拟滚动时向前保留渲染的行数 + * @en the number of rows to be preserved when virtual scrolling + * @defaultValue 10 + */ + keepForwardRenderRows?: number; + /** + * 自定义国际化文案对象 + * @skip + */ + locale?: Locale['Table']; + /** + * 自定义组件,高级用法,用于替换 Table 内部的组件 + * @en custom components, advanced usage, used to replace components inside the table + */ + components?: { + Cell?: CellLike; + Filter?: FilterLike; + Sort?: SortLike; + Resize?: ResizeLike; + Row?: RowLike; + Header?: HeaderLike; + Wrapper?: WrapperLike; + Body?: BodyLike; + }; + /** + * @skip + */ + children?: React.ReactNode; +} diff --git a/components/table/util.js b/components/table/util.ts similarity index 70% rename from components/table/util.js rename to components/table/util.ts index 2669dc6d7c..77df9b32be 100644 --- a/components/table/util.js +++ b/components/table/util.ts @@ -1,4 +1,6 @@ import classnames from 'classnames'; +import { type CSSProperties } from 'react'; +import type { NormalizedColumnProps } from './types'; const blackList = [ 'defaultProps', @@ -9,15 +11,18 @@ const blackList = [ 'getDerivedStateFromProps', ]; -export const statics = (Target, Component) => { +export const statics = (Target: T, Component: U): T & U => { Object.keys(Component).forEach(property => { if (blackList.indexOf(property) === -1) { - Target[property] = Component[property]; + (Target as Record)[property] = (Component as Record)[ + property + ]; } }); + return Target as T & U; }; -export const fetchDataByPath = (object, path) => { +export const fetchDataByPath = (object: Record, path?: string) => { if (!object || !path) { return false; } @@ -30,14 +35,14 @@ export const fetchDataByPath = (object, path) => { if (key.indexOf('[') >= 0) { key = key.match(/(.*)\[(.*)\]/); if (key && typeof key[1] === 'object' && typeof object[key[1]] === 'object') { - val = object[key[1]][key[2]]; + val = (object[key[1]] as Record)[key[2]]; } } else { val = object[field[0]]; } if (val) { for (let colIndex = 1; colIndex < field.length; colIndex++) { - val = val[field[colIndex]]; + val = (val as Record)[field[colIndex]]; if (typeof val === 'undefined') { break; } @@ -47,17 +52,19 @@ export const fetchDataByPath = (object, path) => { return val; }; -/** - * @param {Array} lockChildren - * @param {String} dir 'left', 'right' - */ -export const setStickyStyle = (lockChildren, flatenChildren, dir, offsetArr = [], prefix) => { +export const setStickyStyle = ( + lockChildren: NormalizedColumnProps[], + flatenChildren: NormalizedColumnProps[], + dir: 'left' | 'right', + offsetArr: number[] = [], + prefix: string +) => { const len = flatenChildren.length; flatenChildren.forEach((col, index) => { const isLeftLast = dir === 'left' && index === len - 1; const isRightFirst = dir === 'right' && index === 0; - const style = { + const style: CSSProperties = { position: 'sticky', }; const offset = offsetArr[index]; @@ -75,8 +82,13 @@ export const setStickyStyle = (lockChildren, flatenChildren, dir, offsetArr = [] col.cellStyle = style; }); - const setOffset = (col, index, dir, isBorder) => { - const style = { + const setOffset = ( + col: NormalizedColumnProps, + index: number, + dir: 'left' | 'right', + isBorder: boolean + ) => { + const style: CSSProperties = { position: 'sticky', }; const offset = offsetArr[index]; @@ -95,11 +107,12 @@ export const setStickyStyle = (lockChildren, flatenChildren, dir, offsetArr = [] }; // 查看当前元素的叶子结点数量 - const getLeafNodes = node => { + const getLeafNodes = (node: NormalizedColumnProps | undefined) => { let nodesLen = 0; - const arrLen = (Array.isArray(node && node.children) && node.children.length) || 0; + const arrLen = (Array.isArray(node && node.children) && node!.children!.length) || 0; if (arrLen > 0) { - nodesLen = node.children.reduce((ret, item, idx) => { + nodesLen = node!.children!.reduce((ret, item) => { + // @ts-expect-error 这里实现感觉有些问题,应该传入的是 item return ret + getLeafNodes(item.children); }, 0); } else { @@ -108,7 +121,7 @@ export const setStickyStyle = (lockChildren, flatenChildren, dir, offsetArr = [] return nodesLen; }; - const getPreNodes = (arr, idx) => { + const getPreNodes = (arr: NormalizedColumnProps[], idx: number) => { return arr.reduce((ret, item, i) => { if (i < idx) { return ret + getLeafNodes(item); @@ -119,14 +132,14 @@ export const setStickyStyle = (lockChildren, flatenChildren, dir, offsetArr = [] // for multiple header // nodesLen 前序叶子结点数 - const loop = (arr, i) => { + const loop = (arr: NormalizedColumnProps[], i: number) => { dir === 'right' && arr.reverse(); arr.forEach((child, j) => { const p = dir === 'right' ? i - getPreNodes(arr, j) : i + getPreNodes(arr, j); if (child.children) { // 合并单元格的节点 loop(child.children, p); - // 查询当前元素下的 前序叶子结点数 比如为n + // 查询当前元素下的 前序叶子结点数 比如为 n // const isBorder = (dir === 'right' && j === 0) || (dir === 'left' && j === (arr.length - 1)); setOffset(child, p, dir, j === arr.length - 1); } diff --git a/components/table/virtual.jsx b/components/table/virtual.tsx similarity index 73% rename from components/table/virtual.jsx rename to components/table/virtual.tsx index 465be2822b..0a0f711781 100644 --- a/components/table/virtual.jsx +++ b/components/table/virtual.tsx @@ -5,28 +5,14 @@ import { polyfill } from 'react-lifecycles-compat'; import { dom } from '../util'; import VirtualBody from './virtual/body'; import { statics } from './util'; +import type Base from './base'; +import type { VirtualTableProps, VirtualTableState } from './types'; const noop = () => {}; -export default function virtual(BaseComponent) { - class VirtualTable extends React.Component { +export default function virtual(BaseComponent: typeof Base) { + class VirtualTable extends React.Component { static VirtualBody = VirtualBody; static propTypes = { - /** - * 是否开启虚拟滚动 - */ - useVirtual: PropTypes.bool, - /** - * 设置行高 - */ - rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - maxBodyHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - primaryKey: PropTypes.oneOfType([PropTypes.symbol, PropTypes.string]), - dataSource: PropTypes.array, - /** - * 在内容区域滚动的时候触发的函数 - */ - onBodyScroll: PropTypes.func, - keepForwardRenderRows: PropTypes.number, ...BaseComponent.propTypes, }; @@ -49,9 +35,15 @@ export default function virtual(BaseComponent) { getTableInstanceForVirtual: PropTypes.func, rowSelection: PropTypes.object, }; - - constructor(props, context) { - super(props, context); + lastScrollTop: number; + bodyNode: HTMLElement; + rowSelection: VirtualTableProps['rowSelection']; + start: number; + end: number; + visibleCount: number; + + constructor(props: VirtualTableProps) { + super(props); const { useVirtual, dataSource } = props; const hasVirtualData = useVirtual && dataSource && dataSource.length > 0; @@ -75,8 +67,11 @@ export default function virtual(BaseComponent) { }; } - static getDerivedStateFromProps(nextProps, prevState) { - const state = {}; + static getDerivedStateFromProps( + nextProps: VirtualTableProps, + prevState: VirtualTableState + ) { + const state: Partial = {}; if ('maxBodyHeight' in nextProps) { if (prevState.height !== nextProps.maxBodyHeight) { @@ -88,8 +83,12 @@ export default function virtual(BaseComponent) { state.scrollToRow = nextProps.scrollToRow; } - if (prevState.useVirtual !== nextProps.useVirtual || prevState.dataSource !== nextProps.dataSource) { - state.hasVirtualData = nextProps.useVirtual && nextProps.dataSource && nextProps.dataSource.length > 0; + if ( + prevState.useVirtual !== nextProps.useVirtual || + prevState.dataSource !== nextProps.dataSource + ) { + state.hasVirtualData = + nextProps.useVirtual && nextProps.dataSource && nextProps.dataSource.length > 0; } return state; @@ -113,6 +112,9 @@ export default function virtual(BaseComponent) { this.reComputeSize(); } + [bodyNodeKey: `body${string}Node`]: HTMLElement | null; + [tableKey: `table${string}Inc`]: InstanceType | null; + reComputeSize() { const { rowHeight, hasVirtualData } = this.state; if (typeof rowHeight === 'function' && hasVirtualData) { @@ -133,12 +135,12 @@ export default function virtual(BaseComponent) { return 0; } let count = 0; - dataSource.forEach(item => { + dataSource!.forEach(item => { if (!item.__hidden) { count += 1; } }); - return count * rowHeight; + return count * rowHeight!; } computeInnerTop() { @@ -148,14 +150,14 @@ export default function virtual(BaseComponent) { return 0; } - const start = Math.max(this.start - keepForwardRenderRows, 0); + const start = Math.max(this.start - keepForwardRenderRows!, 0); - return start * rowHeight; + return start * rowHeight!; } - getVisibleRange(ExpectStart) { + getVisibleRange(ExpectStart: number) { const { height, rowHeight } = this.state; - const len = this.props.dataSource.length; + const len = this.props.dataSource!.length; let end, visibleCount = 0; @@ -164,7 +166,8 @@ export default function virtual(BaseComponent) { // try get cell height; end = 1; } else { - visibleCount = parseInt(dom.getPixels(height) / rowHeight, 10); + // @ts-expect-error parseInt 第一个参数是 string,这里是 number,在已经是 number 的情况下,这里其实只是想要取整 + visibleCount = parseInt(dom.getPixels(height) / rowHeight!, 10); if ('number' === typeof ExpectStart) { start = ExpectStart < len ? ExpectStart : 0; @@ -182,13 +185,17 @@ export default function virtual(BaseComponent) { adjustScrollTop() { const { rowHeight, hasVirtualData, scrollToRow } = this.state; + // @ts-expect-error 只有 rowHeight 是数字时才可以这样做,没有考虑 rowHeight 是函数的情况 const oldScrollToRow = Math.floor(this.lastScrollTop / rowHeight); if (hasVirtualData && this.bodyNode) { //根据上次lastScrollTop记录的位置计算而来的scrollToRow位置不准 则以最新的scrollToRow为准重新校准位置(可能是由非用户滚动事件导致的props.scrollToRow发生了变化) if (oldScrollToRow !== scrollToRow) { - this.bodyNode.scrollTop = rowHeight * scrollToRow; + // @ts-expect-error 只有 rowHeight 是数字时才可以这样做,没有考虑 rowHeight 是函数的情况 + this.bodyNode.scrollTop = rowHeight * scrollToRow!; } else { - this.bodyNode.scrollTop = (this.lastScrollTop % rowHeight) + rowHeight * scrollToRow; + this.bodyNode.scrollTop = + // @ts-expect-error 只有 rowHeight 是数字时才可以这样做,没有考虑 rowHeight 是函数的情况 + (this.lastScrollTop % rowHeight) + rowHeight * scrollToRow; } } } @@ -200,10 +207,10 @@ export default function virtual(BaseComponent) { const { clientHeight, clientWidth } = body; const tableInc = this.tableInc; - const tableNode = findDOMNode(tableInc); + const tableNode = findDOMNode(tableInc) as HTMLElement; const { prefix } = this.props; const headerNode = tableNode.querySelector(`.${prefix}table-header table`); - const headerClientWidth = headerNode && headerNode.clientWidth; + const headerClientWidth = headerNode! && headerNode.clientWidth; // todo 2.0 设置宽度这个可以去掉 if (clientWidth < headerClientWidth) { dom.setStyle(virtualScrollNode, 'min-width', headerClientWidth); @@ -229,23 +236,24 @@ export default function virtual(BaseComponent) { scrollToRow: start, }); } - this.props.onBodyScroll(start); + this.props.onBodyScroll!(start); this.lastScrollTop = scrollTop; }; - computeScrollToRow(offset) { + computeScrollToRow(offset: number) { const { rowHeight } = this.state; + // @ts-expect-error rowHeight 只有是数字时才可以这样做,另 parseInt 接受的是 string,这里只是想取整 const start = parseInt(offset / rowHeight); this.start = start; return start; } - getBodyNode = (node, lockType) => { + getBodyNode = (node: HTMLElement, lockType: string) => { lockType = lockType ? lockType.charAt(0).toUpperCase() + lockType.substr(1) : ''; - this[`body${lockType}Node`] = node; + this[`body${lockType}Node` as const] = node; }; - getTableInstance = (type, instance) => { + getTableInstance = (type: string, instance: InstanceType | null) => { type = type ? type.charAt(0).toUpperCase() + type.substr(1) : ''; this[`table${type}Inc`] = instance; }; @@ -255,19 +263,16 @@ export default function virtual(BaseComponent) { // in case of finding an unmounted component due to cached data // need to clear refs of this.tableInc when dataSource Changed // use try catch for temporary - return findDOMNode(this.tableInc.getRowRef(0)); + return findDOMNode(this.tableInc!.getRowRef(0)) as HTMLElement; } catch (error) { return null; } } render() { - /* eslint-disable no-unused-vars, prefer-const */ - let { + const { useVirtual, - components, dataSource, - fixedHeader, rowHeight, scrollToRow, onBodyScroll, @@ -275,6 +280,8 @@ export default function virtual(BaseComponent) { ...others } = this.props; + let { components, fixedHeader } = this.props; + const entireDataSource = dataSource; let newDataSource = dataSource; @@ -282,13 +289,13 @@ export default function virtual(BaseComponent) { if (this.state.hasVirtualData) { newDataSource = []; components = { ...components }; - const { start, end } = this.getVisibleRange(this.state.scrollToRow); + const { start, end } = this.getVisibleRange(this.state.scrollToRow!); let count = -1; - dataSource.forEach((current, index, record) => { + dataSource!.forEach((current, index) => { if (!current.__hidden) { count += 1; - if (count >= Math.max(start - keepForwardRenderRows, 0) && count < end) { - newDataSource.push(current); + if (count >= Math.max(start - keepForwardRenderRows!, 0) && count < end) { + newDataSource!.push(current); } } current.__rowIndex = index; @@ -305,13 +312,12 @@ export default function virtual(BaseComponent) { {...others} scrollToRow={scrollToRow} dataSource={newDataSource} - entireDataSource={entireDataSource} + entireDataSource={entireDataSource!} components={components} fixedHeader={fixedHeader} /> ); } } - statics(VirtualTable, BaseComponent); - return polyfill(VirtualTable); + return polyfill(statics(VirtualTable, BaseComponent)); } diff --git a/components/table/virtual/body.jsx b/components/table/virtual/body.tsx similarity index 77% rename from components/table/virtual/body.jsx rename to components/table/virtual/body.tsx index ecac54fdf9..dc50a8215e 100644 --- a/components/table/virtual/body.jsx +++ b/components/table/virtual/body.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { type CSSProperties, type UIEvent } from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import BodyComponent from '../base/body'; +import type { VirtualBodyProps } from '../types'; -/* eslint-disable react/prefer-stateless-function */ -export default class VirtualBody extends React.Component { +export default class VirtualBody extends React.Component { static propTypes = { children: PropTypes.any, prefix: PropTypes.string, @@ -26,6 +26,8 @@ export default class VirtualBody extends React.Component { getLockNode: PropTypes.func, lockType: PropTypes.oneOf(['left', 'right']), }; + tableNode: HTMLTableElement | null; + virtualScrollNode: HTMLDivElement | null; componentDidMount() { const bodyNode = findDOMNode(this); @@ -37,15 +39,15 @@ export default class VirtualBody extends React.Component { this.context.getLockNode('body', bodyNode, this.context.lockType); } - tableRef = table => { + tableRef = (table: HTMLTableElement | null) => { this.tableNode = table; }; - virtualScrollRef = virtualScroll => { + virtualScrollRef = (virtualScroll: HTMLDivElement | null) => { this.virtualScrollNode = virtualScroll; }; - onScroll = current => { + onScroll = (current: UIEvent) => { // for fixed this.context.onFixedScrollSync(current); // for lock @@ -60,15 +62,19 @@ export default class VirtualBody extends React.Component { const style = { width: tableWidth, }; - const wrapperStyle = { + const wrapperStyle: CSSProperties = { position: 'relative', }; - // todo 2.0 ,这里最好自己画滚动条 + // todo 2.0,这里最好自己画滚动条 if (bodyHeight > maxBodyHeight) { wrapperStyle.height = bodyHeight; } return ( -
+
-1; } } @@ -169,7 +169,7 @@ export type CustomCSSStyle = { [key in CustomCSSStyleKey]: unknown; }; -type LikeCustomCSSStyle> = LikeCustomCSSStyleKey< +type LikeCustomCSSStyle = LikeCustomCSSStyleKey< Exclude > extends never ? never @@ -217,7 +217,7 @@ export function getStyle( ); } -export function setStyle>( +export function setStyle( node: HTMLElement | undefined | null, name: K & LikeCustomCSSStyle ): false | void; diff --git a/components/util/types.ts b/components/util/types.ts index c88a003eb0..1f9b3b9112 100644 --- a/components/util/types.ts +++ b/components/util/types.ts @@ -20,3 +20,55 @@ export type ClassPropsWithDefault< Readonly>> & // eslint-disable-next-line @typescript-eslint/ban-types (Props extends { children: infer C } ? { children: C } : {}); + +interface REACT_STATICS { + childContextTypes: true; + contextType: true; + contextTypes: true; + defaultProps: true; + displayName: true; + getDefaultProps: true; + getDerivedStateFromError: true; + getDerivedStateFromProps: true; + mixins: true; + propTypes: true; + type: true; +} + +interface KNOWN_STATICS { + name: true; + length: true; + prototype: true; + caller: true; + callee: true; + arguments: true; + arity: true; +} + +interface MEMO_STATICS { + $$typeof: true; + compare: true; + defaultProps: true; + displayName: true; + propTypes: true; + type: true; +} + +interface FORWARD_REF_STATICS { + $$typeof: true; + render: true; + defaultProps: true; + displayName: true; + propTypes: true; +} + +export type NonReactStatics, C extends object = object> = { + [key in Exclude< + keyof S, + S extends React.MemoExoticComponent + ? keyof MEMO_STATICS | keyof C + : S extends React.ForwardRefExoticComponent + ? keyof FORWARD_REF_STATICS | keyof C + : keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C + >]: S[key]; +}; diff --git a/global.d.ts b/global.d.ts index b5bd39c505..3e5043f601 100644 --- a/global.d.ts +++ b/global.d.ts @@ -5,7 +5,7 @@ declare module 'es6-promise-polyfill' { export = { Promise }; } declare module 'conventional-changelog' { - import { Readable } from 'stream'; + import type { Readable } from 'stream'; function conventionalChangelog(...args: any[]): Readable; export = conventionalChangelog; @@ -19,6 +19,7 @@ declare module 'markdown-it-anchor'; declare module 'remark'; declare module '@alifd/adaptor-helper'; declare module 'solarlunar'; +declare module 'shallow-element-equals'; declare module 'lodash.clonedeep' { export { cloneDeep as default } from 'lodash'; @@ -27,7 +28,7 @@ declare module 'lodash.clonedeep' { declare const mountNode: HTMLDivElement; declare module 'react-lifecycles-compat' { - import { ComponentType } from 'react'; + import type { ComponentType } from 'react'; export function polyfill(Component: C): C; }