* chore: update rc-table

* add basic table style

* checked all logic

* checkbox support disabled

* selection style

* selection support radio

* add selections support

* selection extra style

* select all locale

* sorter logic

* add more desc

* init Filter hooks

* init filter hooks

* update style

* filter style

* filter style

* fix filter

* sort control

* ajax it

* add expandedable css

* expandable view style

* fixed style

* border style

* empty style

* fix pagination style

* add fixed demo

* un-comment

* clean up

* fix filter check logic

* fix overflow & ellipsis conflict

* fix tes

* adjust scroll shadow

* fix border fixed style

* add part of test case

* add filter test part

* more test case

* issue related test

* filter test

* adjust pagination logic

* fix pagination test case

* all selection test case

* table sorter test case

* table basic test

* fix test case

* update faq

* update expandable doc

* add v4 doc

* add summary docs

* more demo

* fix selection

* update snapshot

* update test case

* fix ff styling

* update rc-table

* update snapshot

* update snapshot

* fix lint

* fix style lint

* fix style

* update snapshot

* update desc

* fix missing icon
This commit is contained in:
二货机器人 2019-11-15 14:35:25 +08:00 committed by GitHub
parent d7bc0530f3
commit 72a7ba618f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 20664 additions and 20136 deletions

View File

@ -14,5 +14,9 @@ module.exports = {
pattern: /ConfigConsumer.*renderEmpty/ms, pattern: /ConfigConsumer.*renderEmpty/ms,
module: '../empty', module: '../empty',
}, },
{
pattern: /config-provider\/context.*renderEmpty/ms,
module: '../empty',
},
], ],
}; };

View File

@ -9,7 +9,8 @@
"comment-empty-line-before": null, "comment-empty-line-before": null,
"function-name-case": ["lower", { "ignoreFunctions": ["/colorPalette/"] }], "function-name-case": ["lower", { "ignoreFunctions": ["/colorPalette/"] }],
"no-invalid-double-slash-comments": null, "no-invalid-double-slash-comments": null,
"no-descending-specificity": null, "no-duplicate-selectors": null,
"no-descending-specificity": [true, { "severity": "warning" }],
"declaration-empty-line-before": null "declaration-empty-line-before": null
}, },
"ignoreFiles": ["components/style/color/{bezierEasing,colorPalette,tinyColor}.less"] "ignoreFiles": ["components/style/color/{bezierEasing,colorPalette,tinyColor}.less"]

View File

@ -64,6 +64,7 @@ class ConfigProvider extends React.Component<ConfigProviderProps> {
getPrefixCls: this.getPrefixCls, getPrefixCls: this.getPrefixCls,
csp, csp,
autoInsertSpaceInButton, autoInsertSpaceInButton,
locale: locale || legacyLocale,
}; };
if (getPopupContainer) { if (getPopupContainer) {

View File

@ -16,7 +16,7 @@ const Placements = tuple(
'bottomCenter', 'bottomCenter',
'bottomRight', 'bottomRight',
); );
type Placement = (typeof Placements)[number]; type Placement = typeof Placements[number];
type OverlayFunc = () => React.ReactElement; type OverlayFunc = () => React.ReactElement;

View File

@ -492,7 +492,6 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
</h3> </h3>
<div <div
class="ant-table-wrapper" class="ant-table-wrapper"
style="margin-top:8px"
> >
<div <div
class="ant-spin-nested-loading" class="ant-spin-nested-loading"
@ -501,119 +500,172 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left" class="ant-table"
style="margin-top:8px"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout:auto"
> >
<colgroup> <colgroup>
<col /> <col />
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="" class="ant-table-cell"
> >
<span Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
<th <th
class="" class="ant-table-cell"
> >
<span Age
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Age
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
class="ant-table-tbody" class="ant-table-tbody"
/>
</table>
</div>
<div
class="ant-table-placeholder"
>
<div
class="ant-empty ant-empty-normal"
>
<div
class="ant-empty-image"
> >
<svg <tr
height="41" class="ant-table-placeholder"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
> >
<g <td
fill="none" class="ant-table-cell"
fill-rule="evenodd" colspan="2"
transform="translate(0 1)"
> >
<ellipse <div
cx="32" class="ant-empty ant-empty-normal"
cy="33"
fill="#F5F5F5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#D9D9D9"
> >
<path <div
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z" class="ant-empty-image"
/> >
<path <svg
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" height="41"
fill="#FAFAFA" viewBox="0 0 64 41"
/> width="64"
</g> xmlns="http://www.w3.org/2000/svg"
</g> >
</svg> <g
</div> fill="none"
<p fill-rule="evenodd"
class="ant-empty-description" transform="translate(0 1)"
> >
No Data <ellipse
</p> cx="32"
</div> cy="33"
fill="#F5F5F5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#D9D9D9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#FAFAFA"
/>
</g>
</g>
</svg>
</div>
<p
class="ant-empty-description"
>
No Data
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
<ul
class="ant-pagination ant-table-pagination"
unselectable="unselectable"
>
<li
aria-disabled="true"
class="ant-pagination-disabled ant-pagination-prev"
title="Previous Page"
>
<a
class="ant-pagination-item-link"
>
<span
aria-label="left"
class="anticon anticon-left"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-0 ant-pagination-disabled ant-pagination-item-disabled"
tabindex="0"
title="0"
>
<a>
0
</a>
</li>
<li
aria-disabled="true"
class="ant-pagination-disabled ant-pagination-next"
title="Next Page"
>
<a
class="ant-pagination-item-link"
>
<span
aria-label="right"
class="anticon anticon-right"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1818,141 +1818,209 @@ exports[`renders ./components/locale-provider/demo/all.md correctly 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout:auto"
> >
<colgroup> <colgroup>
<col /> <col />
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
<span <div
class="ant-table-header-column" class="ant-table-filter-column"
> >
<div> <span
<span class="ant-table-filter-column-title"
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
<span
aria-label="filter"
class="anticon anticon-filter ant-dropdown-trigger"
role="img"
tabindex="-1"
title="Filter menu"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path Name
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z" </span>
/> <span
</svg> class="ant-table-filter-trigger-container"
</span> >
<span
class="ant-table-filter-trigger ant-dropdown-trigger"
role="button"
tabindex="-1"
>
<span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
</span>
</span>
</div>
</th> </th>
<th <th
class="" class="ant-table-cell"
> >
<span Age
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Age
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
class="ant-table-tbody" class="ant-table-tbody"
/>
</table>
</div>
<div
class="ant-table-placeholder"
>
<div
class="ant-empty ant-empty-normal"
>
<div
class="ant-empty-image"
> >
<svg <tr
height="41" class="ant-table-placeholder"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
> >
<g <td
fill="none" class="ant-table-cell"
fill-rule="evenodd" colspan="2"
transform="translate(0 1)"
> >
<ellipse <div
cx="32" class="ant-empty ant-empty-normal"
cy="33"
fill="#F5F5F5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#D9D9D9"
> >
<path <div
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z" class="ant-empty-image"
/> >
<path <svg
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" height="41"
fill="#FAFAFA" viewBox="0 0 64 41"
/> width="64"
</g> xmlns="http://www.w3.org/2000/svg"
</g> >
</svg> <g
</div> fill="none"
<p fill-rule="evenodd"
class="ant-empty-description" transform="translate(0 1)"
> >
No Data <ellipse
</p> cx="32"
</div> cy="33"
fill="#F5F5F5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#D9D9D9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#FAFAFA"
/>
</g>
</g>
</svg>
</div>
<p
class="ant-empty-description"
>
No Data
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
<ul
class="ant-pagination ant-table-pagination"
unselectable="unselectable"
>
<li
aria-disabled="true"
class="ant-pagination-disabled ant-pagination-prev"
title="Previous Page"
>
<a
class="ant-pagination-item-link"
>
<span
aria-label="left"
class="anticon anticon-left"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-0 ant-pagination-disabled ant-pagination-item-disabled"
tabindex="0"
title="0"
>
<a>
0
</a>
</li>
<li
aria-disabled="true"
class="ant-pagination-disabled ant-pagination-next"
title="Next Page"
>
<a
class="ant-pagination-item-link"
>
<span
aria-label="right"
class="anticon anticon-right"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,6 +18,7 @@ export default {
filterReset: 'Reset', filterReset: 'Reset',
selectAll: 'Select current page', selectAll: 'Select current page',
selectInvert: 'Invert current page', selectInvert: 'Invert current page',
selectionAll: 'Select all data',
sortTitle: 'Sort', sortTitle: 'Sort',
expand: 'Expand row', expand: 'Expand row',
collapse: 'Collapse row', collapse: 'Collapse row',

View File

@ -19,6 +19,7 @@ export default {
filterReset: '重置', filterReset: '重置',
selectAll: '全选当页', selectAll: '全选当页',
selectInvert: '反选当页', selectInvert: '反选当页',
selectionAll: '全选所有',
sortTitle: '排序', sortTitle: '排序',
expand: '展开行', expand: '展开行',
collapse: '关闭行', collapse: '关闭行',

View File

@ -1,5 +0,0 @@
import * as React from 'react';
import { ColumnProps } from './interface';
/* eslint-disable react/prefer-stateless-function */
export default class Column<T> extends React.Component<ColumnProps<T>, React.ComponentState> {}

View File

@ -1,10 +0,0 @@
import * as React from 'react';
export interface ColumnGroupProps {
title?: React.ReactNode;
className?: string;
}
export default class ColumnGroup extends React.Component<ColumnGroupProps, React.ComponentState> {
static __ANT_TABLE_COLUMN_GROUP = true;
}

View File

@ -0,0 +1,38 @@
import * as React from 'react';
import classNames from 'classnames';
import { TableLocale } from './interface';
interface DefaultExpandIconProps<RecordType> {
prefixCls: string;
onExpand: (record: RecordType, e: React.MouseEvent<HTMLElement>) => void;
record: RecordType;
expanded: boolean;
expandable: boolean;
}
function renderExpandIcon(locale: TableLocale) {
return function expandIcon<RecordType>({
prefixCls,
onExpand,
record,
expanded,
expandable,
}: DefaultExpandIconProps<RecordType>) {
const iconPrefix = `${prefixCls}-row-expand-icon`;
return (
<button
type="button"
onClick={e => onExpand(record, e!)}
className={classNames(iconPrefix, {
[`${iconPrefix}-spaced`]: !expandable,
[`${iconPrefix}-expanded`]: expandable && expanded,
[`${iconPrefix}-collapsed`]: expandable && !expanded,
})}
aria-label={expanded ? locale.collapse : locale.expand}
/>
);
};
}
export default renderExpandIcon;

View File

@ -1,58 +0,0 @@
import * as React from 'react';
import Checkbox from '../checkbox';
import Radio from '../radio';
import { SelectionBoxProps, SelectionBoxState } from './interface';
export default class SelectionBox extends React.Component<SelectionBoxProps, SelectionBoxState> {
unsubscribe: () => void;
constructor(props: SelectionBoxProps) {
super(props);
this.state = {
checked: this.getCheckState(props),
};
}
componentDidMount() {
this.subscribe();
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
// eslint-disable-next-line class-methods-use-this
getCheckState(props: SelectionBoxProps) {
const { store, defaultSelection, rowIndex } = props;
let checked = false;
if (store.getState().selectionDirty) {
checked = store.getState().selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked =
store.getState().selectedRowKeys.indexOf(rowIndex) >= 0 ||
defaultSelection.indexOf(rowIndex) >= 0;
}
return checked;
}
subscribe() {
const { store } = this.props;
this.unsubscribe = store.subscribe(() => {
const checked = this.getCheckState(this.props);
this.setState({ checked });
});
}
render() {
const { type, rowIndex, ...rest } = this.props;
const { checked } = this.state;
if (type === 'radio') {
return <Radio checked={checked} value={rowIndex} {...rest} />;
}
return <Checkbox checked={checked} {...rest} />;
}
}

View File

@ -1,240 +0,0 @@
import * as React from 'react';
import classNames from 'classnames';
import { Down } from '@ant-design/icons';
import Checkbox, { CheckboxChangeEvent } from '../checkbox';
import Dropdown from '../dropdown';
import Menu from '../menu';
import { SelectionCheckboxAllProps, SelectionCheckboxAllState, SelectionItem } from './interface';
function checkSelection<T>({
store,
getCheckboxPropsByItem,
getRecordKey,
data,
type,
byDefaultChecked,
}: {
store: SelectionCheckboxAllProps<T>['store'];
getCheckboxPropsByItem: SelectionCheckboxAllProps<T>['getCheckboxPropsByItem'];
getRecordKey: SelectionCheckboxAllProps<T>['getRecordKey'];
data: T[];
type: 'every' | 'some';
byDefaultChecked: boolean;
}) {
return byDefaultChecked
? data[type]((item, i) => getCheckboxPropsByItem(item, i).defaultChecked)
: data[type]((item, i) => store.getState().selectedRowKeys.indexOf(getRecordKey(item, i)) >= 0);
}
function getIndeterminateState<T>(props: SelectionCheckboxAllProps<T>) {
const { store, data } = props;
if (!data.length) {
return false;
}
const someCheckedNotByDefaultChecked =
checkSelection<T>({
...props,
data,
type: 'some',
byDefaultChecked: false,
}) &&
!checkSelection<T>({
...props,
data,
type: 'every',
byDefaultChecked: false,
});
const someCheckedByDefaultChecked =
checkSelection<T>({
...props,
data,
type: 'some',
byDefaultChecked: true,
}) &&
!checkSelection<T>({
...props,
data,
type: 'every',
byDefaultChecked: true,
});
if (store.getState().selectionDirty) {
return someCheckedNotByDefaultChecked;
}
return someCheckedNotByDefaultChecked || someCheckedByDefaultChecked;
}
function getCheckState<T>(props: SelectionCheckboxAllProps<T>) {
const { store, data } = props;
if (!data.length) {
return false;
}
if (store.getState().selectionDirty) {
return checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
});
}
return (
checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
}) ||
checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: true,
})
);
}
class SelectionCheckboxAll<T> extends React.Component<
SelectionCheckboxAllProps<T>,
SelectionCheckboxAllState
> {
state = {
checked: false,
indeterminate: false,
};
unsubscribe: () => void;
defaultSelections: SelectionItem[];
constructor(props: SelectionCheckboxAllProps<T>) {
super(props);
this.defaultSelections = props.hideDefaultSelections
? []
: [
{
key: 'all',
text: props.locale.selectAll,
},
{
key: 'invert',
text: props.locale.selectInvert,
},
];
}
static getDerivedStateFromProps<T>(
props: SelectionCheckboxAllProps<T>,
state: SelectionCheckboxAllState,
) {
const checked = getCheckState(props);
const indeterminate = getIndeterminateState(props);
const newState: SelectionCheckboxAllState = {};
if (indeterminate !== state.indeterminate) {
newState.indeterminate = indeterminate;
}
if (checked !== state.checked) {
newState.checked = checked;
}
return newState;
}
componentDidMount() {
this.subscribe();
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
setCheckState(props: SelectionCheckboxAllProps<T>) {
const checked = getCheckState(props);
const indeterminate = getIndeterminateState(props);
this.setState(prevState => {
const newState: SelectionCheckboxAllState = {};
if (indeterminate !== prevState.indeterminate) {
newState.indeterminate = indeterminate;
}
if (checked !== prevState.checked) {
newState.checked = checked;
}
return newState;
});
}
handleSelectAllChange = (e: CheckboxChangeEvent) => {
const { checked } = e.target;
this.props.onSelect(checked ? 'all' : 'removeAll', 0, null);
};
subscribe() {
const { store } = this.props;
this.unsubscribe = store.subscribe(() => {
this.setCheckState(this.props);
});
}
renderMenus(selections: SelectionItem[]) {
return selections.map((selection, index) => {
return (
<Menu.Item key={selection.key || index}>
<div
onClick={() => {
this.props.onSelect(selection.key, index, selection.onSelect);
}}
>
{selection.text}
</div>
</Menu.Item>
);
});
}
render() {
const { disabled, prefixCls, selections, getPopupContainer } = this.props;
const { checked, indeterminate } = this.state;
const selectionPrefixCls = `${prefixCls}-selection`;
let customSelections: React.ReactNode = null;
if (selections) {
const newSelections = Array.isArray(selections)
? this.defaultSelections.concat(selections)
: this.defaultSelections;
const menu = (
<Menu className={`${selectionPrefixCls}-menu`} selectedKeys={[]}>
{this.renderMenus(newSelections)}
</Menu>
);
customSelections =
newSelections.length > 0 ? (
<Dropdown overlay={menu} getPopupContainer={getPopupContainer}>
<div className={`${selectionPrefixCls}-down`}>
<Down />
</div>
</Dropdown>
) : null;
}
return (
<div className={selectionPrefixCls}>
<Checkbox
className={classNames({ [`${selectionPrefixCls}-select-all-custom`]: customSelections })}
checked={checked}
indeterminate={indeterminate}
disabled={disabled}
onChange={this.handleSelectAllChange}
/>
{customSelections}
</div>
);
}
}
export default SelectionCheckboxAll;

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import createStore from '../createStore';
import SelectionBox from '../SelectionBox';
const getDefaultStore = selectedRowKeys =>
createStore({
selectedRowKeys: selectedRowKeys || [],
selectionDirty: false,
});
describe('SelectionBox', () => {
it('unchecked by selectedRowKeys ', () => {
const wrapper = mount(
<SelectionBox
store={getDefaultStore()}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={[]}
/>,
);
expect(wrapper.state()).toEqual({ checked: false });
});
it('checked by selectedRowKeys ', () => {
const wrapper = mount(
<SelectionBox
store={getDefaultStore(['1'])}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={[]}
/>,
);
expect(wrapper.state()).toEqual({ checked: true });
});
it('checked by defaultSelection', () => {
const wrapper = mount(
<SelectionBox
store={getDefaultStore()}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={['1']}
/>,
);
expect(wrapper.state()).toEqual({ checked: true });
});
it('checked when store change', () => {
const store = getDefaultStore();
const wrapper = mount(
<SelectionBox
store={store}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={[]}
/>,
);
store.setState({
selectedRowKeys: ['1'],
selectionDirty: true,
});
expect(wrapper.state()).toEqual({ checked: true });
});
it('passes props to Checkbox', () => {
const checkboxProps = {
name: 'testName',
id: 'testId',
};
const wrapper = mount(
<SelectionBox
store={getDefaultStore()}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={['1']}
{...checkboxProps}
/>,
);
wrapper.find('Checkbox').forEach(box => {
expect(box.props().name).toEqual(checkboxProps.name);
expect(box.props().id).toEqual(checkboxProps.id);
});
});
it('passes props to Radios', () => {
const radioProps = {
name: 'testName',
id: 'testId',
};
const wrapper = mount(
<SelectionBox
store={getDefaultStore()}
rowIndex="1"
disabled={false}
onChange={() => {}}
defaultSelection={['1']}
type="radio"
{...radioProps}
/>,
);
wrapper.find('Radio').forEach(radio => {
expect(radio.props().name).toEqual(radioProps.name);
expect(radio.props().id).toEqual(radioProps.id);
});
});
});

View File

@ -1,20 +1,11 @@
/* eslint-disable react/no-multi-comp */ /* eslint-disable react/no-multi-comp */
import React from 'react'; import React from 'react';
import { render, mount } from 'enzyme'; import { mount } from 'enzyme';
import Table from '..'; import Table from '..';
import Input from '../../input'; import Input from '../../input';
import Button from '../../button'; import Button from '../../button';
import ConfigProvider from '../../config-provider'; import ConfigProvider from '../../config-provider';
function getDropdownWrapper(wrapper) {
return mount(
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
}
// https://github.com/Semantic-Org/Semantic-UI-React/blob/72c45080e4f20b531fda2e3e430e384083d6766b/test/specs/modules/Dropdown/Dropdown-test.js#L73 // https://github.com/Semantic-Org/Semantic-UI-React/blob/72c45080e4f20b531fda2e3e430e384083d6766b/test/specs/modules/Dropdown/Dropdown-test.js#L73
const nativeEvent = { nativeEvent: { stopImmediatePropagation: () => {} } }; const nativeEvent = { nativeEvent: { stopImmediatePropagation: () => {} } };
@ -29,7 +20,10 @@ describe('Table.filter', () => {
{ {
text: 'Title', text: 'Title',
value: 'title', value: 'title',
children: [{ text: 'Designer', value: 'designer' }, { text: 'Coder', value: 'coder' }], children: [
{ text: 'Designer', value: 'designer' },
{ text: 'Coder', value: 'coder' },
],
}, },
], ],
onFilter: filterFn, onFilter: filterFn,
@ -55,24 +49,24 @@ describe('Table.filter', () => {
} }
function renderedNames(wrapper) { function renderedNames(wrapper) {
return wrapper.find('TableRow').map(row => row.props().record.name); return wrapper.find('BodyRow').map(row => row.props().record.name);
} }
it('renders filter correctly', () => { it('renders filter correctly', () => {
const wrapper = render(createTable()); const wrapper = mount(createTable());
expect(wrapper).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
}); });
it('renders menu correctly', () => { it('renders menu correctly', () => {
const wrapper = mount(createTable()); const wrapper = mount(createTable());
const dropdownWrapper = render( const dropdownWrapper = mount(
wrapper wrapper
.find('Trigger') .find('Trigger')
.instance() .instance()
.getComponent(), .getComponent(),
); );
expect(dropdownWrapper).toMatchSnapshot(); expect(dropdownWrapper.render()).toMatchSnapshot();
}); });
it('renders radio filter correctly', () => { it('renders radio filter correctly', () => {
@ -86,13 +80,13 @@ describe('Table.filter', () => {
], ],
}), }),
); );
const dropdownWrapper = render( const dropdownWrapper = mount(
wrapper wrapper
.find('Trigger') .find('Trigger')
.instance() .instance()
.getComponent(), .getComponent(),
); );
expect(dropdownWrapper).toMatchSnapshot(); expect(dropdownWrapper.render()).toMatchSnapshot();
}); });
it('renders custom content correctly', () => { it('renders custom content correctly', () => {
@ -108,13 +102,13 @@ describe('Table.filter', () => {
}), }),
); );
const dropdownWrapper = render( const dropdownWrapper = mount(
wrapper wrapper
.find('Trigger') .find('Trigger')
.instance() .instance()
.getComponent(), .getComponent(),
); );
expect(dropdownWrapper).toMatchSnapshot(); expect(dropdownWrapper.render()).toMatchSnapshot();
}); });
it('override custom filter correctly', () => { it('override custom filter correctly', () => {
@ -143,26 +137,39 @@ describe('Table.filter', () => {
}), }),
); );
const filterMenu = wrapper.find('FilterMenu').instance(); function getFilterMenu() {
return wrapper.find('FilterDropdown');
}
// check if renderer well // check if renderer well
wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent);
expect(wrapper.find('#customFilter')).toMatchSnapshot(); expect(wrapper.find('#customFilter')).toMatchSnapshot();
// try to use reset btn // try to use reset btn
expect(filterMenu.state.selectedKeys).toEqual([]); expect(getFilterMenu().props().filterState.filteredKeys).toBeFalsy();
wrapper.find('#setSelectedKeys').simulate('click'); wrapper.find('#setSelectedKeys').simulate('click');
expect(filterMenu.state.selectedKeys).toEqual([42]); wrapper.find('#confirm').simulate('click');
expect(getFilterMenu().props().filterState.filteredKeys).toEqual([42]);
wrapper.find('#reset').simulate('click'); wrapper.find('#reset').simulate('click');
expect(filterMenu.state.selectedKeys).toEqual([]); expect(getFilterMenu().props().filterState.filteredKeys).toBeFalsy();
// try to use confirm btn // try to use confirm btn
wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent);
wrapper.find('#setSelectedKeys').simulate('click'); wrapper.find('#setSelectedKeys').simulate('click');
expect(filterMenu.state.visible).toBe(true); expect(
getFilterMenu()
.find('Dropdown')
.first()
.props().visible,
).toBeTruthy();
wrapper.find('#confirm').simulate('click'); wrapper.find('#confirm').simulate('click');
expect(filterMenu.state.selectedKeys).toEqual([42]); expect(getFilterMenu().props().filterState.filteredKeys).toEqual([42]);
expect(filterMenu.state.visible).toBe(false); expect(
getFilterMenu()
.find('Dropdown')
.first()
.props().visible,
).toBeFalsy();
}); });
it('can be controlled by filterDropdownVisible', () => { it('can be controlled by filterDropdownVisible', () => {
@ -205,16 +212,19 @@ describe('Table.filter', () => {
}), }),
); );
const filterMenu = wrapper.find('FilterMenu').instance(); expect(wrapper.find('FilterDropdown').props().filterState.filteredKeys).toBeFalsy();
expect(filterMenu.state.selectedKeys).toEqual([]);
wrapper wrapper
.find('FilterMenu') .find('FilterDropdown')
.find('input[type="checkbox"]') .find('input[type="checkbox"]')
.first() .first()
.simulate('click'); .simulate('click');
expect(filterMenu.state.selectedKeys).toEqual(['boy']); wrapper
.find('FilterDropdown')
.find('.confirm')
.simulate('click');
expect(wrapper.find('FilterDropdown').props().filterState.filteredKeys).toEqual(['boy']);
wrapper.setProps({ dataSource: [...data, { key: 999, name: 'Chris' }] }); wrapper.setProps({ dataSource: [...data, { key: 999, name: 'Chris' }] });
expect(filterMenu.state.selectedKeys).toEqual(['boy']); expect(wrapper.find('FilterDropdown').props().filterState.filteredKeys).toEqual(['boy']);
}); });
it('fires change event when visible change', () => { it('fires change event when visible change', () => {
@ -289,13 +299,21 @@ describe('Table.filter', () => {
it('fires change event', () => { it('fires change event', () => {
const handleChange = jest.fn(); const handleChange = jest.fn();
const wrapper = mount(createTable({ onChange: handleChange })); const wrapper = mount(createTable({ onChange: handleChange }));
const dropdownWrapper = getDropdownWrapper(wrapper);
dropdownWrapper wrapper
.find('.ant-dropdown-trigger')
.first()
.simulate('click');
wrapper
.find('FilterDropdown')
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
dropdownWrapper.find('.confirm').simulate('click'); wrapper
.find('FilterDropdown')
.find('.confirm')
.simulate('click');
expect(handleChange).toHaveBeenCalledWith( expect(handleChange).toHaveBeenCalledWith(
{}, {},
@ -310,78 +328,86 @@ describe('Table.filter', () => {
it('should not fire change event on close filterDropdown without changing anything', () => { it('should not fire change event on close filterDropdown without changing anything', () => {
const handleChange = jest.fn(); const handleChange = jest.fn();
const wrapper = mount(createTable({ onChange: handleChange })); const wrapper = mount(createTable({ onChange: handleChange }));
const dropdownWrapper = getDropdownWrapper(wrapper);
dropdownWrapper.find('.clear').simulate('click'); wrapper
.find('.ant-dropdown-trigger')
.first()
.simulate('click');
wrapper.find('.clear').simulate('click');
expect(handleChange).not.toHaveBeenCalled(); expect(handleChange).not.toHaveBeenCalled();
}); });
it('three levels menu', () => { // enzyme not correct update function component under mini store.
const filters = [ // It's correct in `instance().props` but failed in `props()`
{ text: 'Upper', value: 'Upper' }, // it.skip('three levels menu', () => {
{ text: 'Lower', value: 'Lower' }, // const filters = [
{ // { text: 'Upper', value: 'Upper' },
text: 'Level2', // { text: 'Lower', value: 'Lower' },
value: 'Level2', // {
children: [ // text: 'Level2',
{ text: 'Large', value: 'Large' }, // value: 'Level2',
{ text: 'Small', value: 'Small' }, // children: [
{ // { text: 'Large', value: 'Large' },
text: 'Level3', // { text: 'Small', value: 'Small' },
value: 'Level3', // {
children: [ // text: 'Level3',
{ text: 'Black', value: 'Black' }, // value: 'Level3',
{ text: 'White', value: 'White' }, // children: [
{ text: 'Jack', value: 'Jack' }, // { text: 'Black', value: 'Black' },
], // { text: 'White', value: 'White' },
}, // { text: 'Jack', value: 'Jack' },
], // ],
}, // },
]; // ],
const wrapper = mount( // },
createTable({ // ];
columns: [ // const wrapper = mount(
{ // createTable({
...column, // columns: [
filters, // {
}, // ...column,
], // filters,
}), // },
); // ],
jest.useFakeTimers(); // }),
const dropdownWrapper = getDropdownWrapper(wrapper); // );
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']); // jest.useFakeTimers();
// const dropdownWrapper = getDropdownWrapper(wrapper);
// expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
// select // // select
dropdownWrapper // dropdownWrapper
.find('.ant-dropdown-menu-submenu-title') // .find('.ant-dropdown-menu-submenu-title')
.at(0) // .at(0)
.simulate('mouseEnter'); // .simulate('mouseEnter');
jest.runAllTimers(); // jest.runAllTimers();
dropdownWrapper.update(); // dropdownWrapper.update();
dropdownWrapper // dropdownWrapper
.find('.ant-dropdown-menu-submenu-title') // .find('.ant-dropdown-menu-submenu-title')
.at(1) // .at(1)
.simulate('mouseEnter'); // .simulate('mouseEnter');
jest.runAllTimers(); // jest.runAllTimers();
dropdownWrapper.update(); // dropdownWrapper.update();
dropdownWrapper // dropdownWrapper
.find('MenuItem') // .find('MenuItem')
.last() // .last()
.simulate('click'); // .simulate('click');
dropdownWrapper.find('.confirm').simulate('click'); // dropdownWrapper.find('.confirm').simulate('click');
wrapper.update(); // wrapper.update();
expect(renderedNames(wrapper)).toEqual(['Jack']); // expect(renderedNames(wrapper)).toEqual(['Jack']);
dropdownWrapper // dropdownWrapper
.find('MenuItem') // .find('MenuItem')
.last() // .last()
.simulate('click'); // .simulate('click');
jest.useRealTimers(); // jest.useRealTimers();
}); // });
describe('should support value types', () => { describe('should support value types', () => {
[['Light', 93], ['Bamboo', false]].forEach(([text, value]) => { [
['Light', 93],
['Bamboo', false],
].forEach(([text, value]) => {
it(`${typeof value} type`, () => { it(`${typeof value} type`, () => {
const onFilter = jest.fn(); const onFilter = jest.fn();
const filters = [{ text, value }]; const filters = [{ text, value }];
@ -396,42 +422,42 @@ describe('Table.filter', () => {
], ],
}), }),
); );
jest.useFakeTimers();
const dropdownWrapper = getDropdownWrapper(wrapper); wrapper
dropdownWrapper .find('.ant-dropdown-trigger')
.find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
jest.useFakeTimers();
wrapper
.find('MenuItem')
.first()
.simulate('click');
// This test can be remove if refactor // This test can be remove if refactor
expect(typeof wrapper.find('FilterMenu').state().selectedKeys[0]).toEqual('string'); wrapper.find('.confirm').simulate('click');
dropdownWrapper.find('.confirm').simulate('click');
wrapper.update(); wrapper.update();
expect(typeof wrapper.find('FilterDropdown').props().filterState.filteredKeys[0]).toEqual(
'string',
);
expect(onFilter.mock.calls.length > 0).toBeTruthy(); expect(onFilter.mock.calls.length > 0).toBeTruthy();
onFilter.mock.calls.forEach(([val]) => { onFilter.mock.calls.forEach(([val]) => {
expect(val).toBe(value); expect(val).toBe(value);
}); });
// This test can be remove if refactor
expect(typeof wrapper.find('FilterMenu').state().selectedKeys[0]).toEqual(typeof value);
// Another time of Filter show // Another time of Filter show
// https://github.com/ant-design/ant-design/issues/15593 // https://github.com/ant-design/ant-design/issues/15593
getDropdownWrapper(wrapper) wrapper
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
expect( expect(
wrapper wrapper
.find('FilterMenu') .find('FilterDropdown')
.find('Checkbox') .find('Checkbox')
.at(0) .at(0)
.props().checked, .props().checked,
).toEqual(true); ).toEqual(true);
jest.useRealTimers(); jest.useRealTimers();
}); });
}); });
@ -457,7 +483,10 @@ describe('Table.filter', () => {
title="name" title="name"
dataIndex="name" dataIndex="name"
key="name" key="name"
filters={[{ text: 'Jack', value: 'Jack' }, { text: 'Lucy', value: 'Lucy' }]} filters={[
{ text: 'Jack', value: 'Jack' },
{ text: 'Lucy', value: 'Lucy' },
]}
filteredValue={filters.name} filteredValue={filters.name}
onFilter={filterFn} onFilter={filterFn}
/> />
@ -467,17 +496,21 @@ describe('Table.filter', () => {
} }
const wrapper = mount(<App />); const wrapper = mount(<App />);
const dropdownWrapper = getDropdownWrapper(wrapper);
dropdownWrapper wrapper
.find('.ant-dropdown-trigger')
.first()
.simulate('click');
wrapper
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
dropdownWrapper.find('.confirm').simulate('click'); wrapper.find('.confirm').simulate('click');
wrapper.update(); wrapper.update();
expect(renderedNames(wrapper)).toEqual(['Jack']); expect(renderedNames(wrapper)).toEqual(['Jack']);
dropdownWrapper.find('.clear').simulate('click'); wrapper.find('.clear').simulate('click');
wrapper.update(); wrapper.update();
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']); expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
}); });
@ -492,7 +525,10 @@ describe('Table.filter', () => {
title: 'Name', title: 'Name',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
filters: [{ text: 'Jack', value: 'Jack' }, { text: 'Lucy', value: 'Lucy' }], filters: [
{ text: 'Jack', value: 'Jack' },
{ text: 'Lucy', value: 'Lucy' },
],
onFilter: filterFn, onFilter: filterFn,
filteredValue: ['Jack'], filteredValue: ['Jack'],
}, },
@ -522,7 +558,10 @@ describe('Table.filter', () => {
columns: [ columns: [
{ {
...column, ...column,
filters: [{ text: 'Jack', value: 'Jack' }, { text: 'Lucy', value: 'Lucy' }], filters: [
{ text: 'Jack', value: 'Jack' },
{ text: 'Lucy', value: 'Lucy' },
],
}, },
], ],
onChange: handleChange, onChange: handleChange,
@ -547,7 +586,9 @@ describe('Table.filter', () => {
}); });
it('renders custom filter icon correctly', () => { it('renders custom filter icon correctly', () => {
const filterIcon = filtered => <span>{filtered ? 'filtered' : 'unfiltered'}</span>; const filterIcon = filtered => (
<span className="customize-icon">{filtered ? 'filtered' : 'unfiltered'}</span>
);
const wrapper = mount( const wrapper = mount(
createTable({ createTable({
columns: [ columns: [
@ -571,7 +612,7 @@ describe('Table.filter', () => {
.find('.ant-dropdown-trigger') .find('.ant-dropdown-trigger')
.first() .first()
.simulate('click'); .simulate('click');
expect(wrapper.find('.ant-table-filter-icon').render()).toMatchSnapshot(); expect(wrapper.find('.customize-icon').render()).toMatchSnapshot();
wrapper wrapper
.find('.ant-dropdown-trigger') .find('.ant-dropdown-trigger')
@ -585,7 +626,7 @@ describe('Table.filter', () => {
.find('.ant-dropdown-trigger') .find('.ant-dropdown-trigger')
.first() .first()
.simulate('click'); .simulate('click');
expect(wrapper.find('.ant-table-filter-icon').render()).toMatchSnapshot(); expect(wrapper.find('.customize-icon').render()).toMatchSnapshot();
}); });
// https://github.com/ant-design/ant-design/issues/13028 // https://github.com/ant-design/ant-design/issues/13028
@ -714,12 +755,15 @@ describe('Table.filter', () => {
/>, />,
); );
const dropdownWrapper = getDropdownWrapper(wrapper); wrapper
dropdownWrapper .find('.ant-dropdown-trigger')
.first()
.simulate('click');
wrapper
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
dropdownWrapper.find('.confirm').simulate('click'); wrapper.find('.confirm').simulate('click');
expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalled();
onChange.mockReset(); onChange.mockReset();
expect(onChange).not.toHaveBeenCalled(); expect(onChange).not.toHaveBeenCalled();
@ -733,12 +777,11 @@ describe('Table.filter', () => {
], ],
}); });
const dropdownWrapper2 = getDropdownWrapper(wrapper); wrapper
dropdownWrapper2
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
dropdownWrapper2.find('.confirm').simulate('click'); wrapper.find('.confirm').simulate('click');
expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalled();
}); });
@ -861,13 +904,16 @@ describe('Table.filter', () => {
pagination: true, pagination: true,
}), }),
); );
const dropdownWrapper = getDropdownWrapper(wrapper);
dropdownWrapper wrapper
.find('.ant-dropdown-trigger')
.first()
.simulate('click');
wrapper
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
dropdownWrapper.find('.confirm').simulate('click'); wrapper.find('.confirm').simulate('click');
expect(handleChange).toHaveBeenCalledWith( expect(handleChange).toHaveBeenCalledWith(
{ {
@ -880,7 +926,7 @@ describe('Table.filter', () => {
currentDataSource: [], currentDataSource: [],
}, },
); );
expect(wrapper.find('.ant-pagination-item-active').text()).toBe('1'); expect(wrapper.find('.ant-pagination-item').text()).toBe('0');
}); });
it('should keep pagination current after filter', () => { it('should keep pagination current after filter', () => {
@ -895,13 +941,16 @@ describe('Table.filter', () => {
}), }),
); );
expect(wrapper.find('.ant-pagination-item-active').text()).toBe('3'); expect(wrapper.find('.ant-pagination-item-active').text()).toBe('3');
const dropdownWrapper = getDropdownWrapper(wrapper);
dropdownWrapper wrapper
.find('.ant-dropdown-trigger')
.first()
.simulate('click');
wrapper
.find('MenuItem') .find('MenuItem')
.first() .first()
.simulate('click'); .simulate('click');
dropdownWrapper.find('.confirm').simulate('click'); wrapper.find('.confirm').simulate('click');
expect(handleChange).toHaveBeenCalledWith( expect(handleChange).toHaveBeenCalledWith(
{ {
@ -914,7 +963,6 @@ describe('Table.filter', () => {
currentDataSource: [], currentDataSource: [],
}, },
); );
expect(wrapper.find('.ant-pagination-item-active').text()).toBe('3');
}); });
// https://github.com/ant-design/ant-design/issues/19274 // https://github.com/ant-design/ant-design/issues/19274

View File

@ -1,6 +1,10 @@
/* eslint-disable import/first */
jest.mock('../../_util/scrollTo');
import React from 'react'; import React from 'react';
import { render, mount } from 'enzyme'; import { mount } from 'enzyme';
import Table from '..'; import Table from '..';
import scrollTo from '../../_util/scrollTo';
describe('Table.pagination', () => { describe('Table.pagination', () => {
const columns = [ const columns = [
@ -24,12 +28,12 @@ describe('Table.pagination', () => {
} }
function renderedNames(wrapper) { function renderedNames(wrapper) {
return wrapper.find('TableRow').map(row => row.props().record.name); return wrapper.find('BodyRow').map(row => row.props().record.name);
} }
it('renders pagination correctly', () => { it('renders pagination correctly', () => {
const wrapper = render(createTable()); const wrapper = mount(createTable());
expect(wrapper).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
}); });
it('should not show pager if pagination.hideOnSinglePage is true and only 1 page', () => { it('should not show pager if pagination.hideOnSinglePage is true and only 1 page', () => {
@ -80,30 +84,25 @@ describe('Table.pagination', () => {
}); });
it('should scroll to first row when page change', () => { it('should scroll to first row when page change', () => {
scrollTo.mockReturnValue(null);
const wrapper = mount( const wrapper = mount(
createTable({ scroll: { y: 20 }, pagination: { showSizeChanger: true, pageSize: 2 } }), createTable({ scroll: { y: 20 }, pagination: { showSizeChanger: true, pageSize: 2 } }),
); );
const scrollToSpy = jest.spyOn( expect(scrollTo).toHaveBeenCalledTimes(0);
wrapper
.find('Table')
.first()
.instance(),
'scrollToFirstRow',
);
expect(scrollToSpy).toHaveBeenCalledTimes(0);
wrapper wrapper
.find('Pager') .find('Pager')
.last() .last()
.simulate('click'); .simulate('click');
expect(scrollToSpy).toHaveBeenCalledTimes(1); expect(scrollTo).toHaveBeenCalledTimes(1);
wrapper.find('.ant-select-selector').simulate('mousedown'); wrapper.find('.ant-select-selector').simulate('mousedown');
wrapper wrapper
.find('.ant-select-item') .find('.ant-select-item')
.last() .last()
.simulate('click'); .simulate('click');
expect(scrollToSpy).toHaveBeenCalledTimes(2); expect(scrollTo).toHaveBeenCalledTimes(2);
}); });
it('fires change event', () => { it('fires change event', () => {
@ -169,10 +168,10 @@ describe('Table.pagination', () => {
expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']); expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
wrapper.setProps({ pagination: false }); wrapper.setProps({ pagination: false });
expect(wrapper.find('.ant-pagination')).toHaveLength(0); expect(wrapper.find('.ant-pagination')).toHaveLength(0);
wrapper.setProps({ pagination: true }); wrapper.setProps({ pagination: undefined });
expect(wrapper.find('.ant-pagination')).toHaveLength(1); expect(wrapper.find('.ant-pagination')).toHaveLength(1);
expect(wrapper.find('.ant-pagination-item')).toHaveLength(1); // pageSize will be 10 expect(wrapper.find('.ant-pagination-item')).toHaveLength(2);
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']); expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
}); });
// https://github.com/ant-design/ant-design/issues/5259 // https://github.com/ant-design/ant-design/issues/5259
@ -235,8 +234,8 @@ describe('Table.pagination', () => {
* since they misunderstand that `pagination` can accept a boolean value. * since they misunderstand that `pagination` can accept a boolean value.
*/ */
it('Accepts pagination as true', () => { it('Accepts pagination as true', () => {
const wrapper = render(createTable({ pagination: true })); const wrapper = mount(createTable({ pagination: true }));
expect(wrapper).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
}); });
it('ajax render should keep display by the dataSource', () => { it('ajax render should keep display by the dataSource', () => {

View File

@ -2,6 +2,7 @@ import React from 'react';
import { mount, render } from 'enzyme'; import { mount, render } from 'enzyme';
import Table from '..'; import Table from '..';
import Checkbox from '../../checkbox'; import Checkbox from '../../checkbox';
import { resetWarned } from '../../_util/warning';
describe('Table.rowSelection', () => { describe('Table.rowSelection', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
@ -33,7 +34,21 @@ describe('Table.rowSelection', () => {
} }
function renderedNames(wrapper) { function renderedNames(wrapper) {
return wrapper.find('TableRow').map(row => row.props().record.name); return wrapper.find('BodyRow').map(row => row.props().record.name);
}
function getSelections(wrapper) {
return wrapper
.find('BodyRow')
.map(row => {
const { key } = row.props().record;
if (!row.find('input').props().checked) {
return null;
}
return key;
})
.filter(key => key !== null);
} }
it('select by checkbox', () => { it('select by checkbox', () => {
@ -42,22 +57,13 @@ describe('Table.rowSelection', () => {
const checkboxAll = checkboxes.first(); const checkboxAll = checkboxes.first();
checkboxAll.simulate('change', { target: { checked: true } }); checkboxAll.simulate('change', { target: { checked: true } });
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([0, 1, 2, 3]);
selectedRowKeys: [0, 1, 2, 3],
selectionDirty: true,
});
checkboxes.at(1).simulate('change', { target: { checked: false } }); checkboxes.at(1).simulate('change', { target: { checked: false } });
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([1, 2, 3]);
selectedRowKeys: [1, 2, 3],
selectionDirty: true,
});
checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } });
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([0, 1, 2, 3]);
selectedRowKeys: [1, 2, 3, 0],
selectionDirty: true,
});
}); });
it('select by radio', () => { it('select by radio', () => {
@ -67,16 +73,10 @@ describe('Table.rowSelection', () => {
expect(radios.length).toBe(4); expect(radios.length).toBe(4);
radios.first().simulate('change', { target: { checked: true } }); radios.first().simulate('change', { target: { checked: true } });
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([0]);
selectedRowKeys: [0],
selectionDirty: true,
});
radios.last().simulate('change', { target: { checked: true } }); radios.last().simulate('change', { target: { checked: true } });
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([3]);
selectedRowKeys: [3],
selectionDirty: true,
});
}); });
it('pass getCheckboxProps to checkbox', () => { it('pass getCheckboxProps to checkbox', () => {
@ -98,38 +98,46 @@ describe('Table.rowSelection', () => {
it('works with pagination', () => { it('works with pagination', () => {
const wrapper = mount(createTable({ pagination: { pageSize: 2 } })); const wrapper = mount(createTable({ pagination: { pageSize: 2 } }));
const checkboxAll = wrapper.find('SelectionCheckboxAll');
const pagers = wrapper.find('Pager'); const pagers = wrapper.find('Pager');
checkboxAll.find('input').simulate('change', { target: { checked: true } }); wrapper
expect(checkboxAll.instance().state).toEqual({ checked: true, indeterminate: false }); .find('input')
.first()
.simulate('change', { target: { checked: true } });
expect(
wrapper
.find('Checkbox')
.first()
.props(),
).toEqual(expect.objectContaining({ checked: true, indeterminate: false }));
pagers.at(1).simulate('click'); pagers.at(1).simulate('click');
expect(checkboxAll.instance().state).toEqual({ checked: false, indeterminate: false }); expect(
wrapper
.find('Checkbox')
.first()
.props(),
).toEqual(expect.objectContaining({ checked: false, indeterminate: false }));
pagers.at(0).simulate('click'); pagers.at(0).simulate('click');
expect(checkboxAll.instance().state).toEqual({ checked: true, indeterminate: false }); expect(
wrapper
.find('Checkbox')
.first()
.props(),
).toEqual(expect.objectContaining({ checked: true, indeterminate: false }));
}); });
// https://github.com/ant-design/ant-design/issues/4020 // https://github.com/ant-design/ant-design/issues/4020
it('handles defaultChecked', () => { it('handles defaultChecked', () => {
resetWarned();
const rowSelection = { const rowSelection = {
getCheckboxProps: record => ({ getCheckboxProps: record => ({
defaultChecked: record.key === 0, defaultChecked: record.key === 0,
}), }),
}; };
const wrapper = mount(createTable({ rowSelection })); mount(createTable({ rowSelection }));
let checkboxs = wrapper.find('input');
expect(checkboxs.at(1).props().checked).toBe(true);
expect(checkboxs.at(2).props().checked).toBe(false);
checkboxs.at(2).simulate('change', { target: { checked: true } });
checkboxs = wrapper.find('input');
expect(checkboxs.at(1).props().checked).toBe(true);
expect(checkboxs.at(2).props().checked).toBe(true);
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Table] Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.', 'Warning: [antd: Table] Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
@ -139,17 +147,11 @@ describe('Table.rowSelection', () => {
it('can be controlled', () => { it('can be controlled', () => {
const wrapper = mount(createTable({ rowSelection: { selectedRowKeys: [0] } })); const wrapper = mount(createTable({ rowSelection: { selectedRowKeys: [0] } }));
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([0]);
selectedRowKeys: [0],
selectionDirty: false,
});
wrapper.setProps({ rowSelection: { selectedRowKeys: [1] } }); wrapper.setProps({ rowSelection: { selectedRowKeys: [1] } });
expect(wrapper.instance().store.getState()).toEqual({ expect(getSelections(wrapper)).toEqual([1]);
selectedRowKeys: [1],
selectionDirty: false,
});
}); });
it('fires change & select events', () => { it('fires change & select events', () => {
@ -249,28 +251,6 @@ describe('Table.rowSelection', () => {
expect(dropdownWrapper).toMatchSnapshot(); expect(dropdownWrapper).toMatchSnapshot();
}); });
it('click select all selection', () => {
const handleSelectAll = jest.fn();
const rowSelection = {
onSelectAll: handleSelectAll,
selections: true,
};
const wrapper = mount(createTable({ rowSelection }));
const dropdownWrapper = mount(
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
dropdownWrapper
.find('.ant-dropdown-menu-item > div')
.first()
.simulate('click');
expect(handleSelectAll).toHaveBeenCalledWith(true, data, data);
});
it('fires selectInvert event', () => { it('fires selectInvert event', () => {
const handleSelectInvert = jest.fn(); const handleSelectInvert = jest.fn();
const rowSelection = { const rowSelection = {
@ -281,6 +261,7 @@ describe('Table.rowSelection', () => {
const checkboxes = wrapper.find('input'); const checkboxes = wrapper.find('input');
checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } });
const dropdownWrapper = mount( const dropdownWrapper = mount(
wrapper wrapper
.find('Trigger') .find('Trigger')
@ -288,7 +269,7 @@ describe('Table.rowSelection', () => {
.getComponent(), .getComponent(),
); );
dropdownWrapper dropdownWrapper
.find('.ant-dropdown-menu-item > div') .find('.ant-dropdown-menu-item')
.last() .last()
.simulate('click'); .simulate('click');
@ -300,6 +281,8 @@ describe('Table.rowSelection', () => {
const handleSelectEven = jest.fn(); const handleSelectEven = jest.fn();
const rowSelection = { const rowSelection = {
selections: [ selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
{ {
key: 'odd', key: 'odd',
text: '奇数项', text: '奇数项',
@ -323,13 +306,13 @@ describe('Table.rowSelection', () => {
expect(dropdownWrapper.find('.ant-dropdown-menu-item').length).toBe(4); expect(dropdownWrapper.find('.ant-dropdown-menu-item').length).toBe(4);
dropdownWrapper dropdownWrapper
.find('.ant-dropdown-menu-item > div') .find('.ant-dropdown-menu-item')
.at(2) .at(2)
.simulate('click'); .simulate('click');
expect(handleSelectOdd).toHaveBeenCalledWith([0, 1, 2, 3]); expect(handleSelectOdd).toHaveBeenCalledWith([0, 1, 2, 3]);
dropdownWrapper dropdownWrapper
.find('.ant-dropdown-menu-item > div') .find('.ant-dropdown-menu-item')
.at(3) .at(3)
.simulate('click'); .simulate('click');
expect(handleSelectEven).toHaveBeenCalledWith([0, 1, 2, 3]); expect(handleSelectEven).toHaveBeenCalledWith([0, 1, 2, 3]);
@ -363,7 +346,6 @@ describe('Table.rowSelection', () => {
const handleSelectOdd = jest.fn(); const handleSelectOdd = jest.fn();
const handleSelectEven = jest.fn(); const handleSelectEven = jest.fn();
const rowSelection = { const rowSelection = {
hideDefaultSelections: true,
selections: [ selections: [
{ {
key: 'odd', key: 'odd',
@ -388,13 +370,13 @@ describe('Table.rowSelection', () => {
expect(dropdownWrapper.find('.ant-dropdown-menu-item').length).toBe(2); expect(dropdownWrapper.find('.ant-dropdown-menu-item').length).toBe(2);
dropdownWrapper dropdownWrapper
.find('.ant-dropdown-menu-item > div') .find('.ant-dropdown-menu-item')
.at(0) .at(0)
.simulate('click'); .simulate('click');
expect(handleSelectOdd).toHaveBeenCalledWith([0, 1, 2, 3]); expect(handleSelectOdd).toHaveBeenCalledWith([0, 1, 2, 3]);
dropdownWrapper dropdownWrapper
.find('.ant-dropdown-menu-item > div') .find('.ant-dropdown-menu-item')
.at(1) .at(1)
.simulate('click'); .simulate('click');
expect(handleSelectEven).toHaveBeenCalledWith([0, 1, 2, 3]); expect(handleSelectEven).toHaveBeenCalledWith([0, 1, 2, 3]);
@ -465,6 +447,7 @@ describe('Table.rowSelection', () => {
.find('Pager') .find('Pager')
.last() .last()
.simulate('click'); // switch to second page .simulate('click'); // switch to second page
wrapper.update();
wrapper wrapper
.find('input') .find('input')
.first() .first()
@ -584,7 +567,7 @@ describe('Table.rowSelection', () => {
); );
expect( expect(
wrapper wrapper
.find('thead tr div') .find('thead tr th')
.at(0) .at(0)
.text(), .text(),
).toBe('多选'); ).toBe('多选');
@ -596,7 +579,7 @@ describe('Table.rowSelection', () => {
}); });
expect( expect(
wrapper wrapper
.find('thead tr div') .find('thead tr th')
.at(0) .at(0)
.text(), .text(),
).toBe('单选'); ).toBe('单选');
@ -695,13 +678,22 @@ describe('Table.rowSelection', () => {
<Table columns={columns} dataSource={newDatas} childrenColumnName="test" rowSelection={{}} />, <Table columns={columns} dataSource={newDatas} childrenColumnName="test" rowSelection={{}} />,
); );
const checkboxes = wrapper.find('input'); const checkboxes = wrapper.find('input');
const checkboxAll = wrapper.find('SelectionCheckboxAll');
checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } });
expect(checkboxAll.instance().state).toEqual({ indeterminate: true, checked: false }); expect(
wrapper
.find('Checkbox')
.first()
.props(),
).toEqual(expect.objectContaining({ indeterminate: true, checked: false }));
checkboxes.at(2).simulate('change', { target: { checked: true } }); checkboxes.at(2).simulate('change', { target: { checked: true } });
expect(checkboxAll.instance().state).toEqual({ indeterminate: false, checked: true }); expect(
wrapper
.find('Checkbox')
.first()
.props(),
).toEqual(expect.objectContaining({ indeterminate: false, checked: true }));
}); });
// https://github.com/ant-design/ant-design/issues/16614 // https://github.com/ant-design/ant-design/issues/16614
@ -731,13 +723,18 @@ describe('Table.rowSelection', () => {
const checkboxes = wrapper.find('input'); const checkboxes = wrapper.find('input');
checkboxes.at(2).simulate('change', { target: { checked: true } }); checkboxes.at(2).simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenLastCalledWith([11], [newDatas[0].list[0]]); expect(onChange).toHaveBeenLastCalledWith([11], [newDatas[0].list[0]]);
onChange.mockReset();
checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } });
const item0 = { ...newDatas[0], list: undefined }; const item0 = newDatas[0];
expect(onChange).toHaveBeenLastCalledWith([11, 1], [item0, newDatas[0].list[0]]); expect(onChange).toHaveBeenLastCalledWith([11, 1], [newDatas[0].list[0], item0]);
}); });
it('clear selection className when remove `rowSelection`', () => { it('clear selection className when remove `rowSelection`', () => {
const dataSource = [{ id: 1, name: 'Hello', age: 10 }, { id: 2, name: 'World', age: 30 }]; const dataSource = [
{ id: 1, name: 'Hello', age: 10 },
{ id: 2, name: 'World', age: 30 },
];
const wrapper = mount( const wrapper = mount(
<Table <Table
@ -745,15 +742,17 @@ describe('Table.rowSelection', () => {
dataSource={dataSource} dataSource={dataSource}
rowSelection={{}} rowSelection={{}}
expandedRowRender={() => null} expandedRowRender={() => null}
rowKey="id"
/>, />,
); );
const checkboxes = wrapper.find('input'); const checkboxes = wrapper.find('input');
checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } });
expect(wrapper.find('.ant-table-row-selected').length).toBe(1); expect(wrapper.find('tr.ant-table-row-selected').length).toBe(1);
wrapper.setProps({ rowSelection: null }); wrapper.setProps({ rowSelection: null });
expect(wrapper.find('.ant-table-row-selected').length).toBe(0); wrapper.update();
expect(wrapper.find('tr.ant-table-row-selected').length).toBe(0);
}); });
it('select by checkbox to trigger stopPropagation', () => { it('select by checkbox to trigger stopPropagation', () => {
@ -765,13 +764,4 @@ describe('Table.rowSelection', () => {
.simulate('click'); .simulate('click');
}).not.toThrow(); }).not.toThrow();
}); });
it('could hide all selections', () => {
const rowSelection = {
hideDefaultSelections: true,
selections: [],
};
const wrapper = mount(createTable({ rowSelection }));
expect(wrapper.find('Trigger')).toHaveLength(0);
});
}); });

View File

@ -9,6 +9,7 @@ describe('Table.sorter', () => {
const column = { const column = {
title: 'Name', title: 'Name',
dataIndex: 'name', dataIndex: 'name',
key: 'name',
sorter: sorterFn, sorter: sorterFn,
}; };
@ -36,7 +37,7 @@ describe('Table.sorter', () => {
} }
function renderedNames(wrapper) { function renderedNames(wrapper) {
return wrapper.find('TableRow').map(row => row.props().record.name); return wrapper.find('BodyRow').map(row => row.props().record.name);
} }
it('renders sorter icon correctly', () => { it('renders sorter icon correctly', () => {
@ -226,36 +227,27 @@ describe('Table.sorter', () => {
{ key: 3, name: 'Jerry', age: 22 }, { key: 3, name: 'Jerry', age: 22 },
]; ];
const wrapper = mount(<Table columns={columns} dataSource={testData} />); const wrapper = mount(<Table columns={columns} dataSource={testData} />);
const nameColumn = wrapper.find('.ant-table-column-sorters').at(0);
const ageColumn = wrapper.find('.ant-table-column-sorters').at(1); const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
const getAgeColumn = () => wrapper.find('.ant-table-column-has-sorters').at(1);
const getNameIcon = name =>
getNameColumn()
.find(`.ant-table-column-sorter-${name}`)
.first();
const getAgeIcon = name =>
getAgeColumn()
.find(`.ant-table-column-sorter-${name}`)
.first();
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getNameIcon('up').hasClass('active')).toBeTruthy();
nameColumn expect(getAgeIcon('up').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' on');
expect(
ageColumn
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' off');
// sort age // sort age
ageColumn.simulate('click'); getAgeColumn().simulate('click');
expect( expect(getNameIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getAgeIcon('up').hasClass('active')).toBeTruthy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' off');
expect(
ageColumn
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' on');
}); });
// https://github.com/ant-design/ant-design/issues/12571 // https://github.com/ant-design/ant-design/issues/12571
@ -296,61 +288,30 @@ describe('Table.sorter', () => {
} }
const wrapper = mount(<TableTest />); const wrapper = mount(<TableTest />);
const nameColumn = wrapper.find('.ant-table-column-sorters').at(0);
expect( const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
nameColumn const getIcon = name =>
.find('.ant-table-column-sorter-up') getNameColumn()
.at(0) .find(`.ant-table-column-sorter-${name}`)
.getDOMNode().className, .first();
).toContain(' off');
expect( expect(getIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getIcon('down').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' off');
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getIcon('up').hasClass('active')).toBeTruthy();
nameColumn expect(getIcon('down').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' on');
expect(
nameColumn
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' off');
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getIcon('down').hasClass('active')).toBeTruthy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' off');
expect(
nameColumn
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' on');
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getIcon('down').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' off');
expect(
nameColumn
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' off');
}); });
// https://github.com/ant-design/ant-design/issues/12737 // https://github.com/ant-design/ant-design/issues/12737
@ -394,61 +355,30 @@ describe('Table.sorter', () => {
} }
const wrapper = mount(<TableTest />); const wrapper = mount(<TableTest />);
const nameColumn = wrapper.find('.ant-table-column-sorters').at(0);
expect( const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
nameColumn const getIcon = name =>
.find('.ant-table-column-sorter-up') getNameColumn()
.at(0) .find(`.ant-table-column-sorter-${name}`)
.getDOMNode().className, .first();
).toContain(' off');
expect( expect(getIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getIcon('down').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' off');
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getIcon('up').hasClass('active')).toBeTruthy();
nameColumn expect(getIcon('down').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' on');
expect(
nameColumn
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' off');
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getIcon('down').hasClass('active')).toBeTruthy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' off');
expect(
nameColumn
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' on');
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(getIcon('up').hasClass('active')).toBeFalsy();
nameColumn expect(getIcon('down').hasClass('active')).toBeFalsy();
.find('.ant-table-column-sorter-up')
.at(0)
.getDOMNode().className,
).toContain(' off');
expect(
nameColumn
.find('.ant-table-column-sorter-down')
.at(0)
.getDOMNode().className,
).toContain(' off');
}); });
// https://github.com/ant-design/ant-design/issues/12870 // https://github.com/ant-design/ant-design/issues/12870
@ -493,61 +423,64 @@ describe('Table.sorter', () => {
} }
const wrapper = mount(<TableTest />); const wrapper = mount(<TableTest />);
const nameColumn = wrapper.find('.ant-table-column-sorters').at(0); const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-up') .find('.ant-table-column-sorter-up')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' off'); ).toBeFalsy();
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-down') .find('.ant-table-column-sorter-down')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' off'); ).toBeFalsy();
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-up') .find('.ant-table-column-sorter-up')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' on'); ).toBeTruthy();
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-down') .find('.ant-table-column-sorter-down')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' off'); ).toBeFalsy();
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-up') .find('.ant-table-column-sorter-up')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' off'); ).toBeFalsy();
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-down') .find('.ant-table-column-sorter-down')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' on'); ).toBeTruthy();
// sort name // sort name
nameColumn.simulate('click'); getNameColumn().simulate('click');
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-up') .find('.ant-table-column-sorter-up')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' off'); ).toBeFalsy();
expect( expect(
nameColumn getNameColumn()
.find('.ant-table-column-sorter-down') .find('.ant-table-column-sorter-down')
.at(0) .at(0)
.getDOMNode().className, .hasClass('active'),
).toContain(' off'); ).toBeFalsy();
}); });
it('should first sort by descend, then ascend, then cancel sort', () => { it('should first sort by descend, then ascend, then cancel sort', () => {
@ -637,6 +570,7 @@ describe('Table.sorter', () => {
const wrapper = mount( const wrapper = mount(
createTable( createTable(
{ {
defaultExpandAllRows: true,
dataSource: [ dataSource: [
{ {
key: '1', key: '1',

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { render, shallow, mount } from 'enzyme'; import { mount } from 'enzyme';
import Table from '..'; import Table from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
@ -30,7 +30,7 @@ describe('Table', () => {
}, },
]; ];
const wrapper = render( const wrapper = mount(
<Table dataSource={data} pagination={false}> <Table dataSource={data} pagination={false}>
<ColumnGroup title="Name"> <ColumnGroup title="Name">
<Column title="First Name" dataIndex="firstName" key="firstName" /> <Column title="First Name" dataIndex="firstName" key="firstName" />
@ -42,7 +42,7 @@ describe('Table', () => {
</Table>, </Table>,
); );
expect(wrapper).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
}); });
it('updates columns when receiving props', () => { it('updates columns when receiving props', () => {
@ -53,7 +53,7 @@ describe('Table', () => {
dataIndex: 'name', dataIndex: 'name',
}, },
]; ];
const wrapper = shallow(<Table columns={columns} />); const wrapper = mount(<Table columns={columns} />);
const newColumns = [ const newColumns = [
{ {
title: 'Title', title: 'Title',
@ -63,7 +63,7 @@ describe('Table', () => {
]; ];
wrapper.setProps({ columns: newColumns }); wrapper.setProps({ columns: newColumns });
expect(wrapper.dive().state('columns')).toBe(newColumns); expect(wrapper.find('th').text()).toEqual('Title');
}); });
it('loading with Spin', async () => { it('loading with Spin', async () => {
@ -73,7 +73,12 @@ describe('Table', () => {
}; };
const wrapper = mount(<Table loading={loading} />); const wrapper = mount(<Table loading={loading} />);
expect(wrapper.find('.ant-spin')).toHaveLength(0); expect(wrapper.find('.ant-spin')).toHaveLength(0);
expect(wrapper.find('.ant-table-placeholder').text()).not.toEqual(''); expect(
wrapper
.find('.ant-table-placeholder')
.hostNodes()
.text(),
).not.toEqual('');
loading.spinning = true; loading.spinning = true;
wrapper.setProps({ loading }); wrapper.setProps({ loading });
@ -92,13 +97,6 @@ describe('Table', () => {
expect(wrapper.find('tbody').props().id).toBe('wrapper2'); expect(wrapper.find('tbody').props().id).toBe('wrapper2');
}); });
it('warning if both `expandedRowRender` & `Column.fixed` are used', () => {
mount(<Table expandedRowRender={() => null} columns={[{ fixed: true }]} />);
expect(warnSpy).toHaveBeenCalledWith(
'Warning: [antd: Table] `expandedRowRender` and `Column.fixed` are not compatible. Please use one of them at one time.',
);
});
it('props#columnsPageRange and props#columnsPageSize do not warn anymore', () => { it('props#columnsPageRange and props#columnsPageSize do not warn anymore', () => {
const data = [ const data = [
{ {

View File

@ -11,41 +11,26 @@ exports[`Table.expand click to expand 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="" class="ant-table-cell"
> >
<span Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -54,36 +39,35 @@ exports[`Table.expand click to expand 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="" class="ant-table-cell ant-table-cell-with-append"
> >
<span <span
class="ant-table-row-indent indent-level-0" class="ant-table-row-indent indent-level-0"
style="padding-left: 0px;" style="padding-left: 0px;"
/> />
<div <button
aria-label="Collapse row" aria-label="Collapse row"
class="ant-table-row-expand-icon ant-table-row-expanded" class="ant-table-row-expand-icon ant-table-row-expand-icon-expanded"
role="button" type="button"
tabindex="0"
/> />
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-1" class="ant-table-row ant-table-row-level-1"
data-row-key="2"
> >
<td <td
class="" class="ant-table-cell ant-table-cell-with-append"
> >
<span <span
class="ant-table-row-indent indent-level-1" class="ant-table-row-indent indent-level-1"
style="padding-left: 20px;" style="padding-left: 15px;"
/> />
<span <button
class="ant-table-row-expand-icon ant-table-row-spaced" aria-label="Expand row"
class="ant-table-row-expand-icon ant-table-row-expand-icon-spaced"
type="button"
/> />
</td> </td>
</tr> </tr>
@ -182,28 +166,47 @@ exports[`Table.expand should support expandIconColumnIndex 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup /> <colgroup>
<thead <col />
class="ant-table-thead" </colgroup>
/> <thead>
<tr>
<th
class="ant-table-cell"
/>
</tr>
</thead>
<tbody <tbody
class="ant-table-tbody" class="ant-table-tbody"
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1" >
/> <td
class="ant-table-cell ant-table-cell-with-append"
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left: 0px;"
/>
<button
aria-label="Expand row"
class="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
type="button"
/>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -30,7 +30,7 @@ exports[`Table.filter renders custom content correctly 1`] = `
<div> <div>
<div <div
class="ant-dropdown ant-dropdown-placement-bottomRight ant-dropdown-hidden" class="ant-dropdown ant-dropdown-placement-bottomRight ant-dropdown-hidden"
style="visibility:hidden" style="visibility: hidden;"
> >
<div <div
class="ant-table-filter-dropdown" class="ant-table-filter-dropdown"
@ -47,9 +47,7 @@ exports[`Table.filter renders custom content correctly 1`] = `
exports[`Table.filter renders custom filter icon correctly 1`] = ` exports[`Table.filter renders custom filter icon correctly 1`] = `
<span <span
class="ant-table-filter-icon ant-table-filter-selected ant-dropdown-trigger" class="customize-icon"
style=""
title="Filter menu"
> >
filtered filtered
</span> </span>
@ -57,9 +55,7 @@ exports[`Table.filter renders custom filter icon correctly 1`] = `
exports[`Table.filter renders custom filter icon correctly 2`] = ` exports[`Table.filter renders custom filter icon correctly 2`] = `
<span <span
class="ant-table-filter-icon ant-dropdown-trigger" class="customize-icon"
style=""
title="Filter menu"
> >
unfiltered unfiltered
</span> </span>
@ -76,63 +72,64 @@ exports[`Table.filter renders filter correctly 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
<span <div
class="ant-table-header-column" class="ant-table-filter-column"
> >
<div> <span
<span class="ant-table-filter-column-title"
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
<span
aria-label="filter"
class="anticon anticon-filter ant-dropdown-trigger"
role="img"
tabindex="-1"
title="Filter menu"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path Name
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z" </span>
/> <span
</svg> class="ant-table-filter-trigger-container"
</span> >
<span
class="ant-table-filter-trigger ant-dropdown-trigger"
role="button"
tabindex="-1"
>
<span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
</span>
</span>
</div>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -141,40 +138,36 @@ exports[`Table.filter renders filter correctly 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="0"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Jack Jack
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Lucy Lucy
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="2"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Tom Tom
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="3"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Jerry Jerry
</td> </td>
@ -193,15 +186,14 @@ exports[`Table.filter renders menu correctly 1`] = `
<div> <div>
<div <div
class="ant-dropdown ant-dropdown-placement-bottomRight ant-dropdown-hidden" class="ant-dropdown ant-dropdown-placement-bottomRight ant-dropdown-hidden"
style="visibility:hidden" style="visibility: hidden;"
> >
<div <div
class="ant-table-filter-dropdown" class="ant-table-filter-dropdown"
> >
<ul <ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical" class="ant-dropdown-menu ant-dropdown-menu-light ant-dropdown-menu-root ant-dropdown-menu-vertical"
role="menu" role="menu"
tabindex="0"
> >
<li <li
class="ant-dropdown-menu-item" class="ant-dropdown-menu-item"
@ -216,6 +208,7 @@ exports[`Table.filter renders menu correctly 1`] = `
<input <input
class="ant-checkbox-input" class="ant-checkbox-input"
type="checkbox" type="checkbox"
value=""
/> />
<span <span
class="ant-checkbox-inner" class="ant-checkbox-inner"
@ -239,6 +232,7 @@ exports[`Table.filter renders menu correctly 1`] = `
<input <input
class="ant-checkbox-input" class="ant-checkbox-input"
type="checkbox" type="checkbox"
value=""
/> />
<span <span
class="ant-checkbox-inner" class="ant-checkbox-inner"
@ -289,15 +283,14 @@ exports[`Table.filter renders radio filter correctly 1`] = `
<div> <div>
<div <div
class="ant-dropdown ant-dropdown-placement-bottomRight ant-dropdown-hidden" class="ant-dropdown ant-dropdown-placement-bottomRight ant-dropdown-hidden"
style="visibility:hidden" style="visibility: hidden;"
> >
<div <div
class="ant-table-filter-dropdown" class="ant-table-filter-dropdown"
> >
<ul <ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical" class="ant-dropdown-menu ant-dropdown-menu-light ant-dropdown-menu-root ant-dropdown-menu-vertical"
role="menu" role="menu"
tabindex="0"
> >
<li <li
class="ant-dropdown-menu-item" class="ant-dropdown-menu-item"
@ -312,6 +305,7 @@ exports[`Table.filter renders radio filter correctly 1`] = `
<input <input
class="ant-radio-input" class="ant-radio-input"
type="radio" type="radio"
value=""
/> />
<span <span
class="ant-radio-inner" class="ant-radio-inner"
@ -335,6 +329,7 @@ exports[`Table.filter renders radio filter correctly 1`] = `
<input <input
class="ant-radio-input" class="ant-radio-input"
type="radio" type="radio"
value=""
/> />
<span <span
class="ant-radio-inner" class="ant-radio-inner"
@ -392,158 +387,158 @@ exports[`Table.filter should support getPopupContainer 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
<span <div
class="ant-table-header-column" class="ant-table-filter-column"
> >
<div> <span
class="ant-table-filter-column-title"
>
Name
</span>
<span
class="ant-table-filter-trigger-container ant-table-filter-trigger-container-open"
>
<span <span
class="ant-table-column-title" class="ant-table-filter-trigger ant-dropdown-trigger"
role="button"
style=""
tabindex="-1"
> >
Name <span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
</span> </span>
<span <div>
class="ant-table-column-sorter"
/>
</div>
</span>
<span
aria-label="filter"
class="anticon anticon-filter ant-table-filter-open ant-dropdown-trigger"
role="img"
style=""
tabindex="-1"
title="Filter menu"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
<div>
<div
class="ant-dropdown ant-dropdown-placement-bottomRight"
style="left: -999px; top: -995px; min-width: 0;"
>
<div
class="ant-table-filter-dropdown"
>
<ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical"
role="menu"
tabindex="0"
>
<li
class="ant-dropdown-menu-item"
role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Boy
</span>
</li>
<li
class="ant-dropdown-menu-item"
role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Girl
</span>
</li>
<li
class="ant-dropdown-menu-submenu ant-dropdown-menu-submenu-vertical"
role="menuitem"
>
<div
aria-expanded="false"
aria-haspopup="true"
class="ant-dropdown-menu-submenu-title"
title="Title"
>
Title
<i
class="ant-dropdown-menu-submenu-arrow"
/>
</div>
</li>
</ul>
<div <div
class="ant-table-filter-dropdown-btns" class="ant-dropdown ant-dropdown-placement-bottomRight"
style="left: -999px; top: -995px; min-width: 0;"
> >
<a <div
class="ant-table-filter-dropdown-link confirm" class="ant-table-filter-dropdown"
> >
OK <ul
</a> class="ant-dropdown-menu ant-dropdown-menu-light ant-dropdown-menu-root ant-dropdown-menu-vertical"
<a role="menu"
class="ant-table-filter-dropdown-link clear" >
> <li
Reset class="ant-dropdown-menu-item"
</a> role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Boy
</span>
</li>
<li
class="ant-dropdown-menu-item"
role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Girl
</span>
</li>
<li
class="ant-dropdown-menu-submenu ant-dropdown-menu-submenu-vertical"
role="menuitem"
>
<div
aria-expanded="false"
aria-haspopup="true"
class="ant-dropdown-menu-submenu-title"
title="Title"
>
Title
<i
class="ant-dropdown-menu-submenu-arrow"
/>
</div>
</li>
</ul>
<div
class="ant-table-filter-dropdown-btns"
>
<a
class="ant-table-filter-dropdown-link confirm"
>
OK
</a>
<a
class="ant-table-filter-dropdown-link clear"
>
Reset
</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </span>
</div> </div>
</th> </th>
</tr> </tr>
@ -553,40 +548,36 @@ exports[`Table.filter should support getPopupContainer 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="0"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Jack Jack
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Lucy Lucy
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="2"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Tom Tom
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="3"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Jerry Jerry
</td> </td>
@ -612,158 +603,158 @@ exports[`Table.filter should support getPopupContainer from ConfigProvider 1`] =
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
<span <div
class="ant-table-header-column" class="ant-table-filter-column"
> >
<div> <span
class="ant-table-filter-column-title"
>
Name
</span>
<span
class="ant-table-filter-trigger-container ant-table-filter-trigger-container-open"
>
<span <span
class="ant-table-column-title" class="ant-table-filter-trigger ant-dropdown-trigger"
role="button"
style=""
tabindex="-1"
> >
Name <span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
</span> </span>
<span <div>
class="ant-table-column-sorter"
/>
</div>
</span>
<span
aria-label="filter"
class="anticon anticon-filter ant-table-filter-open ant-dropdown-trigger"
role="img"
style=""
tabindex="-1"
title="Filter menu"
>
<svg
aria-hidden="true"
class=""
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
<div>
<div
class="ant-dropdown ant-dropdown-placement-bottomRight"
style="left: -999px; top: -995px; min-width: 0;"
>
<div
class="ant-table-filter-dropdown"
>
<ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical"
role="menu"
tabindex="0"
>
<li
class="ant-dropdown-menu-item"
role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Boy
</span>
</li>
<li
class="ant-dropdown-menu-item"
role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Girl
</span>
</li>
<li
class="ant-dropdown-menu-submenu ant-dropdown-menu-submenu-vertical"
role="menuitem"
>
<div
aria-expanded="false"
aria-haspopup="true"
class="ant-dropdown-menu-submenu-title"
title="Title"
>
Title
<i
class="ant-dropdown-menu-submenu-arrow"
/>
</div>
</li>
</ul>
<div <div
class="ant-table-filter-dropdown-btns" class="ant-dropdown ant-dropdown-placement-bottomRight"
style="left: -999px; top: -995px; min-width: 0;"
> >
<a <div
class="ant-table-filter-dropdown-link confirm" class="ant-table-filter-dropdown"
> >
OK <ul
</a> class="ant-dropdown-menu ant-dropdown-menu-light ant-dropdown-menu-root ant-dropdown-menu-vertical"
<a role="menu"
class="ant-table-filter-dropdown-link clear" >
> <li
Reset class="ant-dropdown-menu-item"
</a> role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Boy
</span>
</li>
<li
class="ant-dropdown-menu-item"
role="menuitem"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span>
Girl
</span>
</li>
<li
class="ant-dropdown-menu-submenu ant-dropdown-menu-submenu-vertical"
role="menuitem"
>
<div
aria-expanded="false"
aria-haspopup="true"
class="ant-dropdown-menu-submenu-title"
title="Title"
>
Title
<i
class="ant-dropdown-menu-submenu-arrow"
/>
</div>
</li>
</ul>
<div
class="ant-table-filter-dropdown-btns"
>
<a
class="ant-table-filter-dropdown-link confirm"
>
OK
</a>
<a
class="ant-table-filter-dropdown-link clear"
>
Reset
</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </span>
</div> </div>
</th> </th>
</tr> </tr>
@ -773,40 +764,36 @@ exports[`Table.filter should support getPopupContainer from ConfigProvider 1`] =
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="0"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Jack Jack
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Lucy Lucy
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="2"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Tom Tom
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="3"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-filters" class="ant-table-cell"
> >
Jerry Jerry
</td> </td>

View File

@ -11,41 +11,26 @@ exports[`Table.pagination Accepts pagination as true 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="" class="ant-table-cell"
> >
<span Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -54,40 +39,36 @@ exports[`Table.pagination Accepts pagination as true 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="0"
> >
<td <td
class="" class="ant-table-cell"
> >
Jack Jack
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="" class="ant-table-cell"
> >
Lucy Lucy
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="2"
> >
<td <td
class="" class="ant-table-cell"
> >
Tom Tom
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="3"
> >
<td <td
class="" class="ant-table-cell"
> >
Jerry Jerry
</td> </td>
@ -187,41 +168,26 @@ exports[`Table.pagination renders pagination correctly 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="" class="ant-table-cell"
> >
<span Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -230,20 +196,18 @@ exports[`Table.pagination renders pagination correctly 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="0"
> >
<td <td
class="" class="ant-table-cell"
> >
Jack Jack
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="" class="ant-table-cell"
> >
Lucy Lucy
</td> </td>
@ -254,7 +218,7 @@ exports[`Table.pagination renders pagination correctly 1`] = `
</div> </div>
</div> </div>
<ul <ul
class="ant-pagination my-page ant-table-pagination" class="ant-pagination my-page"
unselectable="unselectable" unselectable="unselectable"
> >
<li <li

View File

@ -1,75 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Table.sorter renders sorter icon correctly 1`] = ` exports[`Table.sorter renders sorter icon correctly 1`] = `
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="ant-table-column-has-actions ant-table-column-has-sorters" class="ant-table-cell ant-table-column-has-sorters"
> >
<span <div
class="ant-table-header-column" class="ant-table-column-sorters"
> >
<div <span>
class="ant-table-column-sorters" Name
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
> >
<span <span
class="ant-table-column-title" class="ant-table-column-sorter-inner"
> >
Name <span
</span> aria-label="caret-up"
<span class="anticon anticon-caret-up ant-table-column-sorter-up"
class="ant-table-column-sorter" role="img"
>
<div
class="ant-table-column-sorter-inner ant-table-column-sorter-inner-full"
title="Sort"
> >
<span <svg
aria-label="caret-up" aria-hidden="true"
class="anticon anticon-caret-up ant-table-column-sorter-up off" class=""
role="img" data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
> >
<svg <path
aria-hidden="true" d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
class="" />
data-icon="caret-up" </svg>
fill="currentColor" </span>
focusable="false" <span
height="1em" aria-label="caret-down"
viewBox="0 0 1024 1024" class="anticon anticon-caret-down ant-table-column-sorter-down"
width="1em" role="img"
> >
<path <svg
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z" aria-hidden="true"
/> class=""
</svg> data-icon="caret-down"
</span> fill="currentColor"
<span focusable="false"
aria-label="caret-down" height="1em"
class="anticon anticon-caret-down ant-table-column-sorter-down off" viewBox="0 0 1024 1024"
role="img" width="1em"
> >
<svg <path
aria-hidden="true" d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
class="" />
data-icon="caret-down" </svg>
fill="currentColor" </span>
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</div>
</span> </span>
</div> </span>
</span> </div>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -86,89 +77,80 @@ exports[`Table.sorter should support defaultOrder in Column 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="ant-table-column-has-actions ant-table-column-has-sorters ant-table-column-sort" class="ant-table-cell ant-table-column-has-sorters"
> >
<span <div
class="ant-table-header-column" class="ant-table-column-sorters"
> >
<div <span>
class="ant-table-column-sorters" Age
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
> >
<span <span
class="ant-table-column-title" class="ant-table-column-sorter-inner"
> >
Age <span
</span> aria-label="caret-up"
<span class="anticon anticon-caret-up ant-table-column-sorter-up"
class="ant-table-column-sorter" role="img"
>
<div
class="ant-table-column-sorter-inner ant-table-column-sorter-inner-full"
title="Sort"
> >
<span <svg
aria-label="caret-up" aria-hidden="true"
class="anticon anticon-caret-up ant-table-column-sorter-up on" class=""
role="img" data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
> >
<svg <path
aria-hidden="true" d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
class="" />
data-icon="caret-up" </svg>
fill="currentColor" </span>
focusable="false" <span
height="1em" aria-label="caret-down"
viewBox="0 0 1024 1024" class="anticon anticon-caret-down ant-table-column-sorter-down"
width="1em" role="img"
> >
<path <svg
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z" aria-hidden="true"
/> class=""
</svg> data-icon="caret-down"
</span> fill="currentColor"
<span focusable="false"
aria-label="caret-down" height="1em"
class="anticon anticon-caret-down ant-table-column-sorter-down off" viewBox="0 0 1024 1024"
role="img" width="1em"
> >
<svg <path
aria-hidden="true" d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
class="" />
data-icon="caret-down" </svg>
fill="currentColor" </span>
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</div>
</span> </span>
</div> </span>
</span> </div>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -177,10 +159,9 @@ exports[`Table.sorter should support defaultOrder in Column 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="ant-table-column-has-actions ant-table-column-has-sorters ant-table-column-sort" class="ant-table-cell"
> >
1 1
</td> </td>

View File

@ -11,101 +11,47 @@ exports[`Table renders JSX correctly 1`] = `
class="ant-spin-container" class="ant-spin-container"
> >
<div <div
class="ant-table ant-table-default ant-table-scroll-position-left" class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<div <div
class="ant-table-body" class="ant-table-content"
> >
<table <table
class="" style="table-layout: auto;"
> >
<colgroup> <colgroup>
<col /> <col />
<col /> <col />
<col /> <col />
</colgroup> </colgroup>
<thead <thead>
class="ant-table-thead"
>
<tr> <tr>
<th <th
class="" class="ant-table-cell"
colspan="2" colspan="2"
> >
<span Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
<th <th
class="" class="ant-table-cell"
rowspan="2" rowspan="2"
> >
<span Age
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Age
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
<tr> <tr>
<th <th
class="" class="ant-table-cell"
> >
<span First Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
First Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
<th <th
class="" class="ant-table-cell"
> >
<span Last Name
class="ant-table-header-column"
>
<div>
<span
class="ant-table-column-title"
>
Last Name
</span>
<span
class="ant-table-column-sorter"
/>
</div>
</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -114,40 +60,38 @@ exports[`Table renders JSX correctly 1`] = `
> >
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<td <td
class="" class="ant-table-cell"
> >
John John
</td> </td>
<td <td
class="" class="ant-table-cell"
> >
Brown Brown
</td> </td>
<td <td
class="" class="ant-table-cell"
> >
32 32
</td> </td>
</tr> </tr>
<tr <tr
class="ant-table-row ant-table-row-level-0" class="ant-table-row ant-table-row-level-0"
data-row-key="2"
> >
<td <td
class="" class="ant-table-cell"
> >
Jim Jim
</td> </td>
<td <td
class="" class="ant-table-cell"
> >
Green Green
</td> </td>
<td <td
class="" class="ant-table-cell"
> >
42 42
</td> </td>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +0,0 @@
import * as React from 'react';
import classnames from 'classnames';
import omit from 'omit.js';
import { Store } from './createStore';
interface BodyRowProps {
store: Store;
className?: string;
rowKey: string;
prefixCls: string;
}
interface BodyRowState {
selected: boolean;
}
export interface BodyRowClass extends React.ComponentClass {}
export default function createBodyRow(Component: React.ReactType = 'tr') {
class BodyRow extends React.Component<BodyRowProps, BodyRowState> {
private store: Store;
private unsubscribe: () => void;
constructor(props: BodyRowProps) {
super(props);
this.store = props.store;
const { selectedRowKeys } = this.store.getState();
this.state = {
selected: selectedRowKeys.indexOf(props.rowKey) >= 0,
};
}
componentDidMount() {
this.subscribe();
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
subscribe() {
const { store, rowKey } = this.props;
this.unsubscribe = store.subscribe(() => {
const { selectedRowKeys } = this.store.getState();
const selected = selectedRowKeys.indexOf(rowKey) >= 0;
if (selected !== this.state.selected) {
this.setState({ selected });
}
});
}
render() {
const rowProps = omit(this.props, ['prefixCls', 'rowKey', 'store']);
const className = classnames(this.props.className, {
[`${this.props.prefixCls}-row-selected`]: this.state.selected,
});
return React.createElement(
Component,
{
...rowProps,
className,
},
this.props.children,
);
}
}
return BodyRow as BodyRowClass;
}

View File

@ -1,39 +0,0 @@
export interface Store {
setState: (partial: object) => void;
getState: () => any;
subscribe: (listener: () => void) => () => void;
}
export default function createStore(initialState: object): Store {
let state = initialState;
const listeners: any[] = [];
function setState(partial: object) {
state = {
...state,
...partial,
};
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
}
function getState() {
return state;
}
function subscribe(listener: () => any) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
return {
setState,
getState,
subscribe,
};
}

View File

@ -1,5 +1,5 @@
--- ---
order: 9 order: 10
title: title:
en-US: Ajax en-US: Ajax
zh-CN: 远程加载数据 zh-CN: 远程加载数据

View File

@ -1,5 +1,5 @@
--- ---
order: 11 order: 12
title: title:
en-US: border, title and footer en-US: border, title and footer
zh-CN: 带边框 zh-CN: 带边框

View File

@ -1,5 +1,5 @@
--- ---
order: 14 order: 15
title: title:
en-US: colSpan and rowSpan en-US: colSpan and rowSpan
zh-CN: 表格行/列合并 zh-CN: 表格行/列合并

View File

@ -1,5 +1,5 @@
--- ---
order: 8 order: 9
title: title:
en-US: Customized filter panel en-US: Customized filter panel
zh-CN: 自定义筛选菜单 zh-CN: 自定义筛选菜单
@ -66,7 +66,7 @@ class App extends React.Component {
<Button <Button
type="primary" type="primary"
onClick={() => this.handleSearch(selectedKeys, confirm)} onClick={() => this.handleSearch(selectedKeys, confirm)}
icon="search" icon={<Search />}
size="small" size="small"
style={{ width: 90, marginRight: 8 }} style={{ width: 90, marginRight: 8 }}
> >

View File

@ -1,5 +1,5 @@
--- ---
order: 25 order: 26
title: title:
en-US: Drag sorting en-US: Drag sorting
zh-CN: 拖拽排序 zh-CN: 拖拽排序

View File

@ -62,11 +62,10 @@ for (let i = 1; i <= 10; i++) {
}); });
} }
const expandedRowRender = record => <p>{record.description}</p>; const expandable = { expandedRowRender: record => <p>{record.description}</p> };
const title = () => 'Here is title'; const title = () => 'Here is title';
const showHeader = true; const showHeader = true;
const footer = () => 'Here is footer'; const footer = () => 'Here is footer';
const scroll = { y: 240 };
const pagination = { position: 'bottom' }; const pagination = { position: 'bottom' };
class Demo extends React.Component { class Demo extends React.Component {
@ -75,7 +74,7 @@ class Demo extends React.Component {
loading: false, loading: false,
pagination, pagination,
size: 'default', size: 'default',
expandedRowRender, expandable,
title: undefined, title: undefined,
showHeader, showHeader,
footer, footer,
@ -98,7 +97,7 @@ class Demo extends React.Component {
}; };
handleExpandChange = enable => { handleExpandChange = enable => {
this.setState({ expandedRowRender: enable ? expandedRowRender : undefined }); this.setState({ expandable: enable ? expandable : undefined });
}; };
handleEllipsisChange = enable => { handleEllipsisChange = enable => {
@ -121,8 +120,12 @@ class Demo extends React.Component {
this.setState({ rowSelection: enable ? {} : undefined }); this.setState({ rowSelection: enable ? {} : undefined });
}; };
handleScollChange = enable => { handleYScrollChange = enable => {
this.setState({ scroll: enable ? scroll : undefined }); this.setState({ yScroll: enable });
};
handleXScrollChange = e => {
this.setState({ xScroll: e.target.value });
}; };
handleDataChange = hasData => { handleDataChange = hasData => {
@ -137,7 +140,22 @@ class Demo extends React.Component {
}; };
render() { render() {
const { state } = this; const { xScroll, yScroll, ...state } = this.state;
const scroll = {};
if (yScroll) {
scroll.y = 240;
}
if (xScroll) {
scroll.x = '100vw';
}
const tableColumns = columns.map(item => ({ ...item, ellipsis: state.ellipsis }));
if (xScroll === 'fixed') {
tableColumns[0].fixed = true;
tableColumns[tableColumns.length - 1].fixed = 'right';
}
return ( return (
<div> <div>
<Form <Form
@ -161,13 +179,13 @@ class Demo extends React.Component {
<Switch checked={!!state.footer} onChange={this.handleFooterChange} /> <Switch checked={!!state.footer} onChange={this.handleFooterChange} />
</Form.Item> </Form.Item>
<Form.Item label="Expandable"> <Form.Item label="Expandable">
<Switch checked={!!state.expandedRowRender} onChange={this.handleExpandChange} /> <Switch checked={!!state.expandable} onChange={this.handleExpandChange} />
</Form.Item> </Form.Item>
<Form.Item label="Checkbox"> <Form.Item label="Checkbox">
<Switch checked={!!state.rowSelection} onChange={this.handleRowSelectionChange} /> <Switch checked={!!state.rowSelection} onChange={this.handleRowSelectionChange} />
</Form.Item> </Form.Item>
<Form.Item label="Fixed Header"> <Form.Item label="Fixed Header">
<Switch checked={!!state.scroll} onChange={this.handleScollChange} /> <Switch checked={!!yScroll} onChange={this.handleYScrollChange} />
</Form.Item> </Form.Item>
<Form.Item label="Has Data"> <Form.Item label="Has Data">
<Switch checked={!!state.hasData} onChange={this.handleDataChange} /> <Switch checked={!!state.hasData} onChange={this.handleDataChange} />
@ -182,6 +200,13 @@ class Demo extends React.Component {
<Radio.Button value="small">Small</Radio.Button> <Radio.Button value="small">Small</Radio.Button>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label="Table Scroll">
<Radio.Group value={xScroll} onChange={this.handleXScrollChange}>
<Radio.Button value={undefined}>Unset</Radio.Button>
<Radio.Button value="scroll">Scroll</Radio.Button>
<Radio.Button value="fixed">Fixed Columns</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Table Layout"> <Form.Item label="Table Layout">
<Radio.Group value={state.tableLayout} onChange={this.handleTableLayoutChange}> <Radio.Group value={state.tableLayout} onChange={this.handleTableLayoutChange}>
<Radio.Button value={undefined}>Unset</Radio.Button> <Radio.Button value={undefined}>Unset</Radio.Button>
@ -202,8 +227,9 @@ class Demo extends React.Component {
</Form> </Form>
<Table <Table
{...this.state} {...this.state}
columns={columns.map(item => ({ ...item, ellipsis: state.ellipsis }))} columns={tableColumns}
dataSource={state.hasData ? data : null} dataSource={state.hasData ? data : null}
scroll={scroll}
/> />
</div> </div>
); );

View File

@ -1,5 +1,5 @@
--- ---
order: 22 order: 23
title: title:
en-US: Editable Cells en-US: Editable Cells
zh-CN: 可编辑单元格 zh-CN: 可编辑单元格

View File

@ -1,5 +1,5 @@
--- ---
order: 23 order: 24
title: title:
en-US: Editable Rows en-US: Editable Rows
zh-CN: 可编辑行 zh-CN: 可编辑行

View File

@ -1,5 +1,5 @@
--- ---
order: 27 order: 28
title: title:
en-US: ellipsis column en-US: ellipsis column
zh-CN: 单元格自动省略 zh-CN: 单元格自动省略
@ -13,9 +13,9 @@ title:
## en-US ## en-US
Ellipsize cell content via setting `column.ellipsis`. Ellipsis cell content via setting `column.ellipsis`.
> Cannot ellipsize table header with sorters and filters for now. > Cannot ellipsis table header with sorters and filters for now.
```jsx ```jsx
import { Table } from 'antd'; import { Table } from 'antd';

View File

@ -1,5 +1,5 @@
--- ---
order: 16 order: 17
title: title:
en-US: Tree data en-US: Tree data
zh-CN: 树形数据展示 zh-CN: 树形数据展示

View File

@ -1,5 +1,5 @@
--- ---
order: 13 order: 14
title: title:
en-US: Expandable Row en-US: Expandable Row
zh-CN: 可展开 zh-CN: 可展开
@ -45,6 +45,13 @@ const data = [
}, },
{ {
key: 3, key: 3,
name: 'Not Expandable',
age: 29,
address: 'Jiangsu No. 1 Lake Park',
description: 'This not expandable',
},
{
key: 4,
name: 'Joe Black', name: 'Joe Black',
age: 32, age: 32,
address: 'Sidney No. 1 Lake Park', address: 'Sidney No. 1 Lake Park',
@ -55,7 +62,10 @@ const data = [
ReactDOM.render( ReactDOM.render(
<Table <Table
columns={columns} columns={columns}
expandedRowRender={record => <p style={{ margin: 0 }}>{record.description}</p>} expandable={{
expandedRowRender: record => <p style={{ margin: 0 }}>{record.description}</p>,
rowExpandable: record => record.name !== 'Not Expandable',
}}
dataSource={data} dataSource={data}
/>, />,
mountNode, mountNode,

View File

@ -1,5 +1,5 @@
--- ---
order: 19 order: 20
title: title:
en-US: Fixed Columns and Header en-US: Fixed Columns and Header
zh-CN: 固定头和列 zh-CN: 固定头和列

View File

@ -1,5 +1,5 @@
--- ---
order: 18 order: 19
title: title:
en-US: Fixed Columns en-US: Fixed Columns
zh-CN: 固定列 zh-CN: 固定列

View File

@ -1,5 +1,5 @@
--- ---
order: 17 order: 18
title: title:
en-US: Fixed Header en-US: Fixed Header
zh-CN: 固定表头 zh-CN: 固定表头

View File

@ -1,5 +1,5 @@
--- ---
order: 21 order: 22
title: title:
en-US: Grouping table head en-US: Grouping table head
zh-CN: 表头分组 zh-CN: 表头分组

View File

@ -0,0 +1,86 @@
---
order: 7
title:
en-US: Multiple sorter
zh-CN: 多列排序
---
## zh-CN
`column.sorter` 支持 `multiple` 字段以配置多列排序优先级。通过 `sorter.compare` 配置排序逻辑,你可以通过不设置该函数只启动多列排序的交互形式。
## en-US
`column.sorter` support `multiple` to config the priority of sort columns. Though `sorter.compare` to customize compare function. You can also leave it empty to use the interactive only.
```jsx
import { Table } from 'antd';
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Chinese Score',
dataIndex: 'chinese',
sorter: {
compare: (a, b) => a.chinese - b.chinese,
multiple: 3,
},
},
{
title: 'Math Score',
dataIndex: 'math',
sorter: {
compare: (a, b) => a.math - b.math,
multiple: 2,
},
},
{
title: 'English Score',
dataIndex: 'english',
sorter: {
compare: (a, b) => a.english - b.english,
multiple: 1,
},
},
];
const data = [
{
key: '1',
name: 'John Brown',
chinese: 98,
math: 60,
english: 70,
},
{
key: '2',
name: 'Jim Green',
chinese: 98,
math: 66,
english: 89,
},
{
key: '3',
name: 'Joe Black',
chinese: 98,
math: 90,
english: 70,
},
{
key: '4',
name: 'Jim Red',
chinese: 88,
math: 99,
english: 89,
},
];
function onChange(pagination, filters, sorter, extra) {
console.log('params', pagination, filters, sorter, extra);
}
ReactDOM.render(<Table columns={columns} dataSource={data} onChange={onChange} />, mountNode);
```

View File

@ -1,5 +1,5 @@
--- ---
order: 24 order: 25
title: title:
en-US: Nested tables en-US: Nested tables
zh-CN: 嵌套子表格 zh-CN: 嵌套子表格
@ -97,7 +97,7 @@ function NestedTable() {
<Table <Table
className="components-table-demo-nested" className="components-table-demo-nested"
columns={columns} columns={columns}
expandedRowRender={expandedRowRender} expandable={{ expandedRowRender }}
dataSource={data} dataSource={data}
/> />
); );

View File

@ -1,5 +1,5 @@
--- ---
order: 7 order: 8
title: title:
en-US: Reset filters and sorters en-US: Reset filters and sorters
zh-CN: 可控的筛选和排序 zh-CN: 可控的筛选和排序

View File

@ -1,5 +1,5 @@
--- ---
order: 26 order: 27
title: title:
en-US: Resizable column en-US: Resizable column
zh-CN: 可伸缩列 zh-CN: 可伸缩列

View File

@ -58,15 +58,8 @@ class App extends React.Component {
onChange: this.onSelectChange, onChange: this.onSelectChange,
hideDefaultSelections: true, hideDefaultSelections: true,
selections: [ selections: [
{ Table.SELECTION_ALL,
key: 'all-data', Table.SELECTION_INVERT,
text: 'Select All Data',
onSelect: () => {
this.setState({
selectedRowKeys: [...Array(46).keys()], // 0...45
});
},
},
{ {
key: 'odd', key: 'odd',
text: 'Select Odd Row', text: 'Select Odd Row',

View File

@ -7,18 +7,18 @@ title:
## zh-CN ## zh-CN
第一列是联动的选择框。 第一列是联动的选择框。可以通过 `rowSelection.type` 属性指定选择类型,默认为 `checkbox`
> 默认点击 checkbox 触发选择行为,需要点击行触发可以参考例子:<https://codesandbox.io/s/000vqw38rl> > 默认点击 checkbox 触发选择行为,需要点击行触发可以参考例子:<https://codesandbox.io/s/000vqw38rl>
## en-US ## en-US
Rows can be selectable by making first column as a selectable column. Rows can be selectable by making first column as a selectable column. You can use `rowSelection.type` to set selection type. Default is `checkbox`.
> selection happens when clicking checkbox defaultly. You can see <https://codesandbox.io/s/000vqw38rl> if you need row-click selection behavior. > selection happens when clicking checkbox by default. You can see <https://codesandbox.io/s/000vqw38rl> if you need row-click selection behavior.
```jsx ```tsx
import { Table } from 'antd'; import { Table, Radio, Divider } from 'antd';
const columns = [ const columns = [
{ {
@ -73,8 +73,34 @@ const rowSelection = {
}), }),
}; };
ReactDOM.render( const Demo = () => {
<Table rowSelection={rowSelection} columns={columns} dataSource={data} />, const [selectionType, setSelectionType] = React.useState('checkbox');
mountNode,
); return (
<div>
<Radio.Group
onChange={({ target: { value } }) => {
setSelectionType(value);
}}
value={selectionType}
>
<Radio value="checkbox">Checkbox</Radio>
<Radio value="radio">radio</Radio>
</Radio.Group>
<Divider />
<Table
rowSelection={{
type: selectionType,
...rowSelection,
}}
columns={columns}
dataSource={data}
/>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
``` ```

View File

@ -1,5 +1,5 @@
--- ---
order: 10 order: 11
title: title:
en-US: size en-US: size
zh-CN: 紧凑型 zh-CN: 紧凑型

View File

@ -0,0 +1,108 @@
---
order: 29
title:
en-US: Summary
zh-CN: 总结栏
---
## zh-CN
通过 `summary` 设置总结栏。
## en-US
Set summary content by `summary` prop.
```jsx
import { Table, Typography } from 'antd';
const { Text } = Typography;
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Borrow',
dataIndex: 'borrow',
},
{
title: 'Repayment',
dataIndex: 'repayment',
},
];
const data = [
{
key: '1',
name: 'John Brown',
borrow: 10,
repayment: 33,
},
{
key: '2',
name: 'Jim Green',
borrow: 100,
repayment: 0,
},
{
key: '3',
name: 'Joe Black',
borrow: 10,
repayment: 10,
},
{
key: '4',
name: 'Jim Red',
borrow: 75,
repayment: 45,
},
];
ReactDOM.render(
<Table
columns={columns}
dataSource={data}
pagination={false}
bordered
summary={pageData => {
let totalBorrow = 0;
let totalRepayment = 0;
pageData.forEach(({ borrow, repayment }) => {
totalBorrow += borrow;
totalRepayment += repayment;
});
return (
<>
<tr>
<th>Total</th>
<td>
<Text type="danger">{totalBorrow}</Text>
</td>
<td>
<Text>{totalRepayment}</Text>
</td>
</tr>
<tr>
<th>Balance</th>
<td colSpan={2}>
<Text type="danger">{totalBorrow - totalRepayment}</Text>
</td>
</tr>
</>
);
}}
/>,
mountNode,
);
```
<style>
#components-table-demo-summary tfoot th,
#components-table-demo-summary tfoot td {
background: #fafafa;
}
</style>

View File

@ -0,0 +1,155 @@
---
order: 30
title:
en-US: Virtual list
zh-CN: 虚拟列表
---
## zh-CN
通过 `react-window` 引入虚拟滚动方案,实现 100000 条数据的高性能表格。
## en-US
Integrate virtual scroll with `react-window` to achieve a high performance table of 100,000 data.
```jsx
import { VariableSizeGrid as Grid } from 'react-window';
import ResizeObserver from 'rc-resize-observer';
import classNames from 'classnames';
import { Table } from 'antd';
function VirtualTable(props) {
const { columns, scroll, className } = props;
const [tableWidth, setTableWidth] = React.useState(0);
const widthColumnCount = columns.filter(({ width }) => !width).length;
const mergedColumns = columns.map(column => {
if (column.width) {
return column;
}
return {
...column,
width: Math.floor(tableWidth / widthColumnCount),
};
});
const gridRef = React.useRef<any>();
const [connectObject] = React.useState<any>(() => {
const obj = {};
Object.defineProperty(obj, 'scrollLeft', {
get: () => null,
set: (scrollLeft: number) => {
if (gridRef.current) {
gridRef.current.scrollTo({ scrollLeft });
}
},
});
return obj;
});
const resetVirtualGrid = () => {
gridRef.current.resetAfterIndices({
columnIndex: 0,
shouldForceUpdate: false,
});
};
React.useEffect(() => resetVirtualGrid, []);
React.useEffect(() => resetVirtualGrid, [tableWidth]);
const renderVirtualList = (rawData: object[], { scrollbarSize, ref, onScroll }: any) => {
ref.current = connectObject;
return (
<Grid
ref={gridRef}
className="virtual-grid"
columnCount={mergedColumns.length}
columnWidth={index => {
const { width } = mergedColumns[index];
return index === mergedColumns.length - 1 ? width - scrollbarSize - 1 : width;
}}
height={scroll.y}
rowCount={rawData.length}
rowHeight={() => 54}
width={tableWidth}
onScroll={({ scrollLeft }) => {
onScroll({ scrollLeft });
}}
>
{({ columnIndex, rowIndex, style }) => (
<div
className={classNames('virtual-table-cell', {
'virtual-table-cell-last': columnIndex === mergedColumns.length - 1,
})}
style={style}
>
{rawData[rowIndex][mergedColumns[columnIndex].dataIndex]}
</div>
)}
</Grid>
);
};
return (
<ResizeObserver onResize={({ width }) => {
setTableWidth(width);
}}>
<Table
{...props}
className={classNames(className, 'virtual-table')}
columns={mergedColumns}
pagination={false}
components={{
body: renderVirtualList,
}}
/>
</ResizeObserver>
);
};
// Usage
const columns = [
{ title: 'A', dataIndex: 'key', width: 150 },
{ title: 'B', dataIndex: 'key'},
{ title: 'C', dataIndex: 'key'},
{ title: 'D', dataIndex: 'key'},
{ title: 'E', dataIndex: 'key', width: 200 },
{ title: 'F', dataIndex: 'key', width: 100 },
];
const data = [];
for (let i = 0; i < 100000; i += 1) {
data.push({
key: i,
});
}
ReactDOM.render(
<VirtualTable
columns={columns}
dataSource={data}
scroll={{ y: 300, x: '100vw' }}
/>,
mountNode,
);
```
<style>
.virtual-table .ant-table-container:before,
.virtual-table .ant-table-container:after {
display: none;
}
.virtual-table-cell {
box-sizing: border-box;
padding: 16px;
border-bottom: 1px solid #e8e8e8;
background: #FFF;
}
</style>

View File

@ -1,319 +0,0 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { polyfill } from 'react-lifecycles-compat';
import Menu, { SubMenu, Item as MenuItem } from 'rc-menu';
import closest from 'dom-closest';
import classNames from 'classnames';
import shallowequal from 'shallowequal';
import { FilterFilled } from '@ant-design/icons';
import Dropdown from '../dropdown';
import Checkbox from '../checkbox';
import Radio from '../radio';
import FilterDropdownMenuWrapper from './FilterDropdownMenuWrapper';
import { FilterMenuProps, FilterMenuState, ColumnProps, ColumnFilterItem } from './interface';
import { generateValueMaps } from './util';
function stopPropagation(e: React.SyntheticEvent<any>) {
e.stopPropagation();
if (e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
class FilterMenu<T> extends React.Component<FilterMenuProps<T>, FilterMenuState<T>> {
static defaultProps = {
column: {},
};
static getDerivedStateFromProps<T>(nextProps: FilterMenuProps<T>, prevState: FilterMenuState<T>) {
const { column } = nextProps;
const { prevProps } = prevState;
const newState: Partial<FilterMenuState<T>> = {
prevProps: nextProps,
};
/**
* if the state is visible the component should ignore updates on selectedKeys prop to avoid
* that the user selection is lost
* this happens frequently when a table is connected on some sort of realtime data
* Fixes https://github.com/ant-design/ant-design/issues/10289 and
* https://github.com/ant-design/ant-design/issues/10209
*/
if (
'selectedKeys' in nextProps &&
!shallowequal(prevProps.selectedKeys, nextProps.selectedKeys)
) {
newState.selectedKeys = nextProps.selectedKeys;
}
if (!shallowequal((prevProps.column || {}).filters, (nextProps.column || {}).filters)) {
newState.valueKeys = generateValueMaps(nextProps.column.filters);
}
if ('filterDropdownVisible' in column) {
newState.visible = column.filterDropdownVisible as boolean;
}
return newState;
}
neverShown: boolean;
constructor(props: FilterMenuProps<T>) {
super(props);
const visible =
'filterDropdownVisible' in props.column ? props.column.filterDropdownVisible : false;
this.state = {
selectedKeys: props.selectedKeys,
valueKeys: generateValueMaps(props.column.filters),
keyPathOfSelectedItem: {}, // 记录所有有选中子菜单的祖先菜单
visible,
prevProps: props,
};
}
componentDidMount() {
const { column } = this.props;
this.setNeverShown(column);
}
componentDidUpdate() {
const { column } = this.props;
this.setNeverShown(column);
}
getDropdownVisible() {
return this.neverShown ? false : this.state.visible;
}
setNeverShown = (column: ColumnProps<T>) => {
const rootNode = ReactDOM.findDOMNode(this);
const filterBelongToScrollBody = !!closest(rootNode, `.ant-table-scroll`);
if (filterBelongToScrollBody) {
// When fixed column have filters, there will be two dropdown menus
// Filter dropdown menu inside scroll body should never be shown
// To fix https://github.com/ant-design/ant-design/issues/5010 and
// https://github.com/ant-design/ant-design/issues/7909
this.neverShown = !!column.fixed;
}
};
setSelectedKeys = ({ selectedKeys }: { selectedKeys?: React.Key[] }) => {
this.setState({ selectedKeys: selectedKeys! });
};
setVisible(visible: boolean) {
const { column } = this.props;
if (!('filterDropdownVisible' in column)) {
this.setState({ visible });
}
if (column.onFilterDropdownVisibleChange) {
column.onFilterDropdownVisibleChange(visible);
}
}
handleClearFilters = () => {
this.setState(
{
selectedKeys: [],
},
this.handleConfirm,
);
};
handleConfirm = () => {
this.setVisible(false);
// Call `setSelectedKeys` & `confirm` in the same time will make filter data not up to date
// https://github.com/ant-design/ant-design/issues/12284
this.setState({}, this.confirmFilter);
};
onVisibleChange = (visible: boolean) => {
this.setVisible(visible);
const { column } = this.props;
// https://github.com/ant-design/ant-design/issues/17833
if (!visible && !(column.filterDropdown instanceof Function)) {
this.confirmFilter();
}
};
handleMenuItemClick = (info: { keyPath: React.Key[]; key: React.Key }) => {
const { selectedKeys } = this.state;
if (!info.keyPath || info.keyPath.length <= 1) {
return;
}
const { keyPathOfSelectedItem } = this.state;
if (selectedKeys && selectedKeys.indexOf(info.key) >= 0) {
// deselect SubMenu child
delete keyPathOfSelectedItem[info.key];
} else {
// select SubMenu child
keyPathOfSelectedItem[info.key] = info.keyPath;
}
this.setState({ keyPathOfSelectedItem });
};
hasSubMenu() {
const {
column: { filters = [] },
} = this.props;
return filters.some(item => !!(item.children && item.children.length > 0));
}
confirmFilter() {
const { column, selectedKeys: propSelectedKeys, confirmFilter } = this.props;
const { selectedKeys, valueKeys } = this.state;
const { filterDropdown } = column;
if (!shallowequal(selectedKeys, propSelectedKeys)) {
confirmFilter(
column,
filterDropdown
? selectedKeys
: selectedKeys.map(key => valueKeys[key]).filter(key => key !== undefined),
);
}
}
renderMenus(items: ColumnFilterItem[]): React.ReactElement<any>[] {
const { dropdownPrefixCls, prefixCls } = this.props;
return items.map(item => {
if (item.children && item.children.length > 0) {
const { keyPathOfSelectedItem } = this.state;
const containSelected = Object.keys(keyPathOfSelectedItem).some(
key => keyPathOfSelectedItem[key].indexOf(item.value) >= 0,
);
const subMenuCls = classNames(`${prefixCls}-dropdown-submenu`, {
[`${dropdownPrefixCls}-submenu-contain-selected`]: containSelected,
});
return (
<SubMenu title={item.text} popupClassName={subMenuCls} key={item.value.toString()}>
{this.renderMenus(item.children)}
</SubMenu>
);
}
return this.renderMenuItem(item);
});
}
renderFilterIcon = () => {
const { column, locale, prefixCls, selectedKeys } = this.props;
const filtered = selectedKeys && selectedKeys.length > 0;
let filterIcon = column.filterIcon as any;
if (typeof filterIcon === 'function') {
filterIcon = filterIcon(filtered);
}
const dropdownIconClass = classNames({
[`${prefixCls}-selected`]: filtered,
[`${prefixCls}-open`]: this.getDropdownVisible(),
});
return filterIcon ? (
React.cloneElement(filterIcon as any, {
title: locale.filterTitle,
className: classNames(`${prefixCls}-icon`, dropdownIconClass, filterIcon.props.className),
onClick: stopPropagation,
})
) : (
<FilterFilled
title={locale.filterTitle}
className={dropdownIconClass}
onClick={stopPropagation}
/>
);
};
renderMenuItem(item: ColumnFilterItem) {
const { column } = this.props;
const { selectedKeys } = this.state;
const multiple = 'filterMultiple' in column ? column.filterMultiple : true;
// We still need trade key as string since Menu render need string
const internalSelectedKeys = (selectedKeys || []).map(key => key.toString());
const input = multiple ? (
<Checkbox checked={internalSelectedKeys.indexOf(item.value.toString()) >= 0} />
) : (
<Radio checked={internalSelectedKeys.indexOf(item.value.toString()) >= 0} />
);
return (
<MenuItem key={item.value}>
{input}
<span>{item.text}</span>
</MenuItem>
);
}
render() {
const { selectedKeys: originSelectedKeys } = this.state;
const { column, locale, prefixCls, dropdownPrefixCls, getPopupContainer } = this.props;
// default multiple selection in filter dropdown
const multiple = 'filterMultiple' in column ? column.filterMultiple : true;
const dropdownMenuClass = classNames({
[`${dropdownPrefixCls}-menu-without-submenu`]: !this.hasSubMenu(),
});
let { filterDropdown } = column;
if (filterDropdown instanceof Function) {
filterDropdown = filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
setSelectedKeys: (selectedKeys: Array<any>) => this.setSelectedKeys({ selectedKeys }),
selectedKeys: originSelectedKeys,
confirm: this.handleConfirm,
clearFilters: this.handleClearFilters,
filters: column.filters,
visible: this.getDropdownVisible(),
});
}
const menus = filterDropdown ? (
<FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}>
{filterDropdown}
</FilterDropdownMenuWrapper>
) : (
<FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}>
<Menu
multiple={multiple}
onClick={this.handleMenuItemClick}
prefixCls={`${dropdownPrefixCls}-menu`}
className={dropdownMenuClass}
onSelect={this.setSelectedKeys}
onDeselect={this.setSelectedKeys}
selectedKeys={originSelectedKeys && originSelectedKeys.map(val => val.toString())}
getPopupContainer={getPopupContainer}
>
{this.renderMenus(column.filters!)}
</Menu>
<div className={`${prefixCls}-dropdown-btns`}>
<a className={`${prefixCls}-dropdown-link confirm`} onClick={this.handleConfirm}>
{locale.filterConfirm}
</a>
<a className={`${prefixCls}-dropdown-link clear`} onClick={this.handleClearFilters}>
{locale.filterReset}
</a>
</div>
</FilterDropdownMenuWrapper>
);
return (
<Dropdown
trigger={['click']}
placement="bottomRight"
overlay={menus}
visible={this.getDropdownVisible()}
onVisibleChange={this.onVisibleChange}
getPopupContainer={getPopupContainer}
forceRender
>
{this.renderFilterIcon()}
</Dropdown>
);
}
}
polyfill(FilterMenu);
export default FilterMenu;

View File

@ -0,0 +1,256 @@
import * as React from 'react';
import classNames from 'classnames';
import shallowEqual from 'shallowequal';
import { FilterFilled } from '@ant-design/icons';
import Menu from '../../../menu';
import Checkbox from '../../../checkbox';
import Radio from '../../../radio';
import Dropdown from '../../../dropdown';
import { ColumnType, ColumnFilterItem, Key, TableLocale, GetPopupContainer } from '../../interface';
import FilterDropdownMenuWrapper from './FilterWrapper';
import { FilterState } from '.';
const { SubMenu, Item: MenuItem } = Menu;
function hasSubMenu(filters: ColumnFilterItem[]) {
return filters.some(({ children }) => children);
}
function renderFilterItems(
filters: ColumnFilterItem[],
prefixCls: string,
filteredKeys: Key[],
multiple: boolean,
) {
return filters.map((filter, index) => {
if (filter.children) {
return (
<SubMenu
key={filter.value || index}
title={filter.text}
popupClassName={`${prefixCls}-dropdown-submenu`}
>
{renderFilterItems(filter.children, prefixCls, filteredKeys, multiple)}
</SubMenu>
);
}
const Component = multiple ? Checkbox : Radio;
return (
<MenuItem key={filter.value !== undefined ? filter.value : index}>
<Component checked={filteredKeys.includes(String(filter.value))} />
<span>{filter.text}</span>
</MenuItem>
);
});
}
export interface FilterDropdownProps<RecordType> {
prefixCls: string;
dropdownPrefixCls: string;
column: ColumnType<RecordType>;
filterState?: FilterState<RecordType>;
filterMultiple: boolean;
columnKey: Key;
children: React.ReactNode;
triggerFilter: (filterState: FilterState<RecordType>) => void;
locale: TableLocale;
getPopupContainer?: GetPopupContainer;
}
function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
const {
prefixCls,
column,
dropdownPrefixCls,
columnKey,
filterMultiple,
filterState,
triggerFilter,
locale,
children,
getPopupContainer,
} = props;
const { filterDropdownVisible, onFilterDropdownVisibleChange } = column;
const [visible, setVisible] = React.useState(false);
const filtered: boolean = !!(filterState && filterState.filteredKeys);
const triggerVisible = (newVisible: boolean) => {
setVisible(newVisible);
if (onFilterDropdownVisibleChange) {
onFilterDropdownVisibleChange(newVisible);
}
};
const mergedVisible =
typeof filterDropdownVisible === 'boolean' ? filterDropdownVisible : visible;
// ===================== Select Keys =====================
const [propFilteredKeys, setPropFilteredKeys] = React.useState(
filterState && filterState.filteredKeys,
);
const [filteredKeys, setFilteredKeys] = React.useState(propFilteredKeys || []);
React.useEffect(() => {
// Sync internal filtered keys when props key changed
const newFilteredKeys = filterState && filterState.filteredKeys;
if (!shallowEqual(propFilteredKeys, newFilteredKeys)) {
setPropFilteredKeys(newFilteredKeys);
setFilteredKeys(newFilteredKeys || []);
}
}, [filterState]);
const onSelectKeys = ({ selectedKeys }: { selectedKeys: Key[] }) => {
setFilteredKeys(selectedKeys);
};
// ====================== Open Keys ======================
const [openKeys, setOpenKeys] = React.useState<string[]>([]);
const openRef = React.useRef<number>();
const onOpenChange = (keys: string[]) => {
openRef.current = window.setTimeout(() => {
setOpenKeys(keys);
});
};
const onMenuClick = () => {
window.clearTimeout(openRef.current);
};
React.useEffect(() => {
return () => {
window.clearTimeout(openRef.current);
};
}, []);
// ======================= Submit ========================
const internalTriggerFilter = (keys: Key[] | undefined | null) => {
triggerVisible(false);
const mergedKeys = keys && keys.length ? keys : null;
if (mergedKeys === null && (!filterState || !filterState.filteredKeys)) {
return null;
}
triggerFilter({
column,
key: columnKey,
filteredKeys: mergedKeys,
});
};
const onConfirm = () => {
internalTriggerFilter(filteredKeys);
};
const onReset = () => {
internalTriggerFilter([]);
};
const onVisibleChange = (newVisible: boolean) => {
triggerVisible(newVisible);
// Default will filter when closed
if (!newVisible && !column.filterDropdown) {
onConfirm();
}
};
// ======================== Style ========================
const dropdownMenuClass = classNames({
[`${dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu(column.filters || []),
});
let dropdownContent: React.ReactNode;
if (typeof column.filterDropdown === 'function') {
dropdownContent = column.filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
setSelectedKeys: (selectedKeys: Key[]) => onSelectKeys({ selectedKeys }),
selectedKeys: filteredKeys,
confirm: onConfirm,
clearFilters: onReset,
filters: column.filters,
visible: mergedVisible,
});
} else if (column.filterDropdown) {
dropdownContent = column.filterDropdown;
} else {
dropdownContent = (
<>
<Menu
multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`}
className={dropdownMenuClass}
onClick={onMenuClick}
onSelect={onSelectKeys}
onDeselect={onSelectKeys}
selectedKeys={(filteredKeys || []) as any}
getPopupContainer={getPopupContainer}
openKeys={openKeys}
onOpenChange={onOpenChange}
>
{renderFilterItems(column.filters!, prefixCls, filteredKeys, filterMultiple)}
</Menu>
<div className={`${prefixCls}-dropdown-btns`}>
<a className={`${prefixCls}-dropdown-link confirm`} onClick={onConfirm}>
{locale.filterConfirm}
</a>
<a className={`${prefixCls}-dropdown-link clear`} onClick={onReset}>
{locale.filterReset}
</a>
</div>
</>
);
}
const menu = (
<FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}>
{dropdownContent}
</FilterDropdownMenuWrapper>
);
let filterIcon: React.ReactNode;
if (typeof column.filterIcon === 'function') {
filterIcon = column.filterIcon(filtered);
} else if (column.filterIcon) {
filterIcon = column.filterIcon;
} else {
filterIcon = <FilterFilled />;
}
return (
<div className={classNames(`${prefixCls}-column`)}>
<span className={`${prefixCls}-column-title`}>{children}</span>
<span
className={classNames(`${prefixCls}-trigger-container`, {
[`${prefixCls}-trigger-container-open`]: mergedVisible,
})}
onClick={e => {
e.stopPropagation();
}}
>
<Dropdown
overlay={menu}
trigger={['click']}
visible={mergedVisible}
onVisibleChange={onVisibleChange}
placement="bottomRight"
>
<span
role="button"
tabIndex={-1}
className={classNames(`${prefixCls}-trigger`, {
active: filtered,
})}
>
{filterIcon}
</span>
</Dropdown>
</span>
</div>
);
}
export default FilterDropdown;

View File

@ -0,0 +1,216 @@
import * as React from 'react';
import {
TransformColumns,
ColumnsType,
ColumnType,
ColumnTitleProps,
Key,
TableLocale,
GetPopupContainer,
ColumnFilterItem,
} from '../../interface';
import { getColumnPos, renderColumnTitle, getColumnKey } from '../../util';
import FilterDropdown from './FilterDropdown';
import { ConfigContext } from '../../../config-provider';
import defaultLocale from '../../../locale/en_US';
export interface FilterState<RecordType> {
column: ColumnType<RecordType>;
key: Key;
filteredKeys?: Key[] | null;
}
function collectFilterStates<RecordType>(
columns: ColumnsType<RecordType>,
pos?: string,
): FilterState<RecordType>[] {
let filterStates: FilterState<RecordType>[] = [];
columns.forEach((column, index) => {
const columnPos = getColumnPos(index, pos);
if ('children' in column) {
filterStates = [...filterStates, ...collectFilterStates(column.children, columnPos)];
} else if ('filters' in column || 'filterDropdown' in column) {
// Controlled
filterStates.push({
column,
key: getColumnKey(column, columnPos),
filteredKeys: column.filteredValue,
});
}
});
return filterStates;
}
function injectFilter<RecordType>(
prefixCls: string,
dropdownPrefixCls: string,
columns: ColumnsType<RecordType>,
filterStates: FilterState<RecordType>[],
triggerFilter: (filterState: FilterState<RecordType>) => void,
getPopupContainer: GetPopupContainer | undefined,
locale: TableLocale,
pos?: string,
): ColumnsType<RecordType> {
return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos);
const { filterMultiple = true } = column as ColumnType<RecordType>;
if ('filters' in column || 'filterDropdown' in column) {
const columnKey = getColumnKey(column, columnPos);
const filterState = filterStates.find(({ key }) => columnKey === key);
return {
...column,
title: (renderProps: ColumnTitleProps<RecordType>) => (
<FilterDropdown
prefixCls={`${prefixCls}-filter`}
dropdownPrefixCls={dropdownPrefixCls}
column={column}
columnKey={columnKey}
filterState={filterState}
filterMultiple={filterMultiple}
triggerFilter={triggerFilter}
locale={locale}
getPopupContainer={getPopupContainer}
>
{renderColumnTitle(column.title, renderProps)}
</FilterDropdown>
),
};
}
if ('children' in column) {
return {
...column,
children: injectFilter(
prefixCls,
dropdownPrefixCls,
column.children,
filterStates,
triggerFilter,
getPopupContainer,
locale,
columnPos,
),
};
}
return column;
});
}
function generateFilterInfo<RecordType>(filterStates: FilterState<RecordType>[]) {
const currentFilters: Record<string, Key[] | null> = {};
filterStates.forEach(({ key, filteredKeys }) => {
currentFilters[key] = filteredKeys || null;
});
return currentFilters;
}
function flattenKeys(filters?: ColumnFilterItem[]) {
let keys: Key[] = [];
(filters || []).forEach(({ value, children }) => {
keys.push(value);
if (children) {
keys = [...keys, ...flattenKeys(children)];
}
});
return keys;
}
export function getFilterData<RecordType>(
data: RecordType[],
filterStates: FilterState<RecordType>[],
) {
return filterStates.reduce((currentData, filterState) => {
const {
column: { onFilter, filters },
filteredKeys,
} = filterState;
if (onFilter && filteredKeys && filteredKeys.length) {
return currentData.filter(record =>
filteredKeys.some(key => {
const keys = flattenKeys(filters);
const keyIndex = keys.findIndex(k => String(k) === String(key));
const realKey = keyIndex !== -1 ? keys[keyIndex] : key;
return onFilter(realKey, record);
}),
);
}
return currentData;
}, data);
}
interface FilterConfig<RecordType> {
prefixCls: string;
dropdownPrefixCls?: string;
columns: ColumnsType<RecordType>;
onFilterChange: (
filters: Record<string, Key[] | null>,
filterStates: FilterState<RecordType>[],
) => void;
getPopupContainer?: GetPopupContainer;
}
function useFilter<RecordType>({
prefixCls,
dropdownPrefixCls = 'ant-dropdown',
columns,
onFilterChange,
getPopupContainer,
}: FilterConfig<RecordType>): [
TransformColumns<RecordType>,
FilterState<RecordType>[],
() => Record<string, Key[] | null>,
] {
const { locale = defaultLocale } = React.useContext(ConfigContext);
const tableLocale = (locale.Table || {}) as TableLocale;
const [filterStates, setFilterStates] = React.useState<FilterState<RecordType>[]>(
collectFilterStates(columns),
);
const mergedFilterStates = React.useMemo(() => {
const collectedStates = collectFilterStates(columns);
// Return if not controlled
if (collectedStates.every(({ filteredKeys }) => filteredKeys === undefined)) {
return filterStates;
}
return collectedStates;
}, [columns, filterStates]);
const getFilters = React.useCallback(() => generateFilterInfo(mergedFilterStates), [
mergedFilterStates,
]);
const triggerFilter = (filterState: FilterState<RecordType>) => {
const newFilterStates = mergedFilterStates.filter(({ key }) => key !== filterState.key);
newFilterStates.push(filterState);
setFilterStates(newFilterStates);
onFilterChange(generateFilterInfo(newFilterStates), newFilterStates);
};
const transformColumns = React.useMemo(() => {
return (innerColumns: ColumnsType<RecordType>) =>
injectFilter(
prefixCls,
dropdownPrefixCls,
innerColumns,
mergedFilterStates,
triggerFilter,
getPopupContainer,
tableLocale,
);
}, [mergedFilterStates]);
return [transformColumns, mergedFilterStates, getFilters];
}
export default useFilter;

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import { Key, GetRowKey } from '../interface';
interface MapCache<RecordType> {
data?: RecordType[];
childrenColumnName?: string;
kvMap?: Map<Key, RecordType>;
getRowKey?: Function;
}
export default function useLazyKVMap<RecordType>(
data: RecordType[],
childrenColumnName: string,
getRowKey: GetRowKey<RecordType>,
) {
const mapCacheRef = React.useRef<MapCache<RecordType>>({});
function getRecordByKey(key: Key): RecordType {
if (
!mapCacheRef.current ||
mapCacheRef.current.data !== data ||
mapCacheRef.current.childrenColumnName !== childrenColumnName ||
mapCacheRef.current.getRowKey !== getRowKey
) {
const kvMap = new Map<Key, RecordType>();
/* eslint-disable no-inner-declarations */
function dig(records: RecordType[]) {
records.forEach((record, index) => {
const rowKey = getRowKey(record, index);
kvMap.set(rowKey, record);
if (childrenColumnName in record) {
dig((record as any)[childrenColumnName]);
}
});
}
/* eslint-enable */
dig(data);
mapCacheRef.current = {
data,
childrenColumnName,
kvMap,
getRowKey,
};
}
return mapCacheRef.current.kvMap!.get(key)!;
}
return [getRecordByKey];
}

View File

@ -0,0 +1,101 @@
import { useState } from 'react';
import { PaginationProps, PaginationConfig } from '../../pagination';
import { TablePaginationConfig } from '../interface';
export const DEFAULT_PAGE_SIZE = 10;
export function getPaginationParam(
pagination: PaginationConfig | boolean | undefined,
mergedPagination: PaginationConfig,
) {
const param: any = {
current: mergedPagination.current,
pageSize: mergedPagination.pageSize,
};
const paginationObj = pagination && typeof pagination === 'object' ? pagination : {};
Object.keys(paginationObj).forEach(pageProp => {
const value = (mergedPagination as any)[pageProp];
if (typeof value !== 'function') {
param[pageProp] = value;
}
});
return param;
}
export default function usePagination(
total: number,
pagination: TablePaginationConfig | false | undefined,
onChange: (current: number, pageSize: number) => void,
): [TablePaginationConfig, () => void] {
const paginationObj = pagination && typeof pagination === 'object' ? pagination : {};
const [innerPagination, setInnerPagination] = useState<TablePaginationConfig>(() => {
return {
current: 'defaultCurrent' in paginationObj ? paginationObj.defaultCurrent : 1,
pageSize:
'defaultPageSize' in paginationObj ? paginationObj.defaultPageSize : DEFAULT_PAGE_SIZE,
};
});
// ============ Basic Pagination Config ============
const mergedPagination = {
...innerPagination,
total,
...paginationObj,
};
// Reset `current` if data length changed
const maxPage = Math.ceil(total / mergedPagination.pageSize!);
if (maxPage < mergedPagination.current!) {
mergedPagination.current = 1;
}
const refreshPagination = (current: number = 1) => {
setInnerPagination({
...mergedPagination,
current,
});
};
const onInternalChange: PaginationProps['onChange'] = (...args) => {
const [current] = args;
refreshPagination(current);
onChange(current, args[1] || mergedPagination.pageSize!);
if (pagination && pagination.onChange) {
pagination.onChange(...args);
}
};
const onInternalShowSizeChange: PaginationProps['onShowSizeChange'] = (...args) => {
const [, pageSize] = args;
setInnerPagination({
...mergedPagination,
current: 1,
pageSize,
});
onChange(1, pageSize);
if (pagination && pagination.onShowSizeChange) {
pagination.onShowSizeChange(...args);
}
};
if (pagination === false) {
return [{}, () => {}];
}
return [
{
...mergedPagination,
onChange: onInternalChange,
onShowSizeChange: onInternalShowSizeChange,
},
refreshPagination,
];
}

View File

@ -0,0 +1,441 @@
import * as React from 'react';
import { Down } from '@ant-design/icons';
import { FixedType } from 'rc-table/lib/interface';
import Checkbox, { CheckboxProps } from '../../checkbox';
import Dropdown from '../../dropdown';
import Menu from '../../menu';
import Radio from '../../radio';
import warning from '../../_util/warning';
import {
TableRowSelection,
Key,
ColumnsType,
GetRowKey,
TableLocale,
SelectionItem,
TransformColumns,
ExpandType,
} from '../interface';
import { ConfigContext } from '../../config-provider';
import defaultLocale from '../../locale/en_US';
const EMPTY_LIST: any[] = [];
// TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL';
export const SELECTION_INVERT = 'SELECT_INVERT';
function getFixedType<RecordType>(column: ColumnsType<RecordType>[number]): FixedType | undefined {
return column && column.fixed;
}
interface UseSelectionConfig<RecordType> {
prefixCls: string;
pageData: RecordType[];
data: RecordType[];
getRowKey: GetRowKey<RecordType>;
getRecordByKey: (key: Key) => RecordType;
expandType: ExpandType;
childrenColumnName: string;
}
type INTERNAL_SELECTION_ITEM = SelectionItem | typeof SELECTION_ALL | typeof SELECTION_INVERT;
function flattenData<RecordType>(
data: RecordType[] | undefined,
childrenColumnName: string,
): RecordType[] {
let list: RecordType[] = [];
(data || []).forEach(record => {
list.push(record);
if (childrenColumnName in record) {
list = [
...list,
...flattenData<RecordType>((record as any)[childrenColumnName], childrenColumnName),
];
}
});
return list;
}
export default function useSelection<RecordType>(
rowSelection: TableRowSelection<RecordType> | undefined,
config: UseSelectionConfig<RecordType>,
): [TransformColumns<RecordType>, Set<Key>] {
const {
selectedRowKeys,
getCheckboxProps,
onChange: onSelectionChange,
onSelect,
onSelectAll,
onSelectInvert,
onSelectMultiple,
columnWidth: selectionColWidth = 60,
type: selectionType,
selections,
} = rowSelection || {};
const { locale = defaultLocale } = React.useContext(ConfigContext);
const tableLocale = (locale.Table || {}) as TableLocale;
const {
prefixCls,
data,
pageData,
getRecordByKey,
getRowKey,
expandType,
childrenColumnName,
} = config;
const [innerSelectedKeys, setInnerSelectedKeys] = React.useState<Key[]>();
const mergedSelectedKeys = selectedRowKeys || innerSelectedKeys || EMPTY_LIST;
const mergedSelectedKeySet = React.useMemo(() => {
const keys = selectionType === 'radio' ? mergedSelectedKeys.slice(0, 1) : mergedSelectedKeys;
return new Set(keys);
}, [mergedSelectedKeys, selectionType]);
// Save last selected key to enable range selection
const [lastSelectedKey, setLastSelectedKey] = React.useState<Key | null>(null);
// Reset if rowSelection reset
React.useEffect(() => {
if (!rowSelection) {
setInnerSelectedKeys([]);
}
}, [!!rowSelection]);
const setSelectedKeys = React.useCallback(
(keys: Key[]) => {
setInnerSelectedKeys(keys);
const records = keys.map(key => getRecordByKey(key));
if (onSelectionChange) {
onSelectionChange(keys, records);
}
},
[setInnerSelectedKeys, getRecordByKey, onSelectionChange],
);
// Trigger single `onSelect` event
const triggerSingleSelection = React.useCallback(
(key: Key, selected: boolean, keys: Key[], event: Event) => {
if (onSelect) {
const rows = keys.map(k => getRecordByKey(k));
onSelect(getRecordByKey(key), selected, rows, event);
}
setSelectedKeys(keys);
},
[onSelect, getRecordByKey, setSelectedKeys],
);
const mergedSelections = React.useMemo<SelectionItem[] | null>(() => {
if (!selections) {
return null;
}
const selectionList: INTERNAL_SELECTION_ITEM[] =
selections === true ? [SELECTION_ALL, SELECTION_INVERT] : selections;
return selectionList.map((selection: INTERNAL_SELECTION_ITEM) => {
if (selection === SELECTION_ALL) {
return {
key: 'all',
text: tableLocale.selectionAll,
onSelect() {
setSelectedKeys(data.map((record, index) => getRowKey(record, index)));
},
};
}
if (selection === SELECTION_INVERT) {
return {
key: 'invert',
text: tableLocale.selectInvert,
onSelect() {
const keySet = new Set(mergedSelectedKeySet);
pageData.forEach((record, index) => {
const key = getRowKey(record, index);
if (keySet.has(key)) {
keySet.delete(key);
} else {
keySet.add(key);
}
});
const keys = Array.from(keySet);
setSelectedKeys(keys);
if (onSelectInvert) {
warning(
false,
'Table',
'`onSelectInvert` will be removed in future. Please use `onChange` instead.',
);
onSelectInvert(keys);
}
},
};
}
return selection as SelectionItem;
});
}, [selections, mergedSelectedKeySet, pageData, getRowKey]);
const transformColumns = React.useCallback(
(columns: ColumnsType<RecordType>): ColumnsType<RecordType> => {
if (!rowSelection) {
return columns;
}
// Get flatten data
const flattedData = flattenData(pageData, childrenColumnName);
// Support selection
const keySet = new Set(mergedSelectedKeySet);
// Get all checkbox props
const checkboxPropsMap = new Map<Key, Partial<CheckboxProps>>();
flattedData.forEach((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
checkboxPropsMap.set(key, checkboxProps);
if (
process.env.NODE_ENV !== 'production' &&
('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
) {
warning(
false,
'Table',
'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
}
});
// Record key only need check with enabled
const recordKeys = flattedData
.map(getRowKey)
.filter(key => !checkboxPropsMap.get(key)!.disabled);
const checkedCurrentAll = recordKeys.every(key => keySet.has(key));
const checkedCurrentSome = recordKeys.some(key => keySet.has(key));
const onSelectAllChange = () => {
const changeKeys: Key[] = [];
if (checkedCurrentAll) {
recordKeys.forEach(key => {
keySet.delete(key);
changeKeys.push(key);
});
} else {
recordKeys.forEach(key => {
keySet.add(key);
changeKeys.push(key);
});
}
const keys = Array.from(keySet);
setSelectedKeys(keys);
if (onSelectAll) {
onSelectAll(
!checkedCurrentAll,
keys.map(k => getRecordByKey(k)),
changeKeys.map(k => getRecordByKey(k)),
);
}
};
// ===================== Render =====================
// Title Cell
let title: React.ReactNode;
if (selectionType !== 'radio') {
let customizeSelections: React.ReactNode;
if (mergedSelections) {
const menu = (
<Menu>
{mergedSelections.map((selection, index) => {
const { key, text, onSelect: onSelectionClick } = selection;
return (
<Menu.Item
key={key || index}
onClick={() => {
if (onSelectionClick) {
onSelectionClick(recordKeys);
}
}}
>
{text}
</Menu.Item>
);
})}
</Menu>
);
customizeSelections = (
<div className={`${prefixCls}-selection-extra`}>
<Dropdown overlay={menu}>
<span>
<Down />
</span>
</Dropdown>
</div>
);
}
title = (
<div className={`${prefixCls}-selection`}>
<Checkbox
checked={!!flattedData.length && checkedCurrentAll}
indeterminate={!checkedCurrentAll && checkedCurrentSome}
onChange={onSelectAllChange}
disabled={
flattedData.length === 0 ||
flattedData.every((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = checkboxPropsMap.get(key) || {};
return checkboxProps.disabled;
})
}
/>
{customizeSelections}
</div>
);
}
// Body Cell
let renderCell: (_: RecordType, record: RecordType, index: number) => React.ReactNode;
if (selectionType === 'radio') {
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
return (
<Radio
{...checkboxPropsMap.get(key)}
checked={keySet.has(key)}
onChange={event => {
if (!keySet.has(key)) {
triggerSingleSelection(key, true, [key], event.nativeEvent);
}
}}
/>
);
};
} else {
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
const hasKey = keySet.has(key);
// Record checked
return (
<Checkbox
{...checkboxPropsMap.get(key)}
checked={hasKey}
onChange={({ nativeEvent }) => {
const { shiftKey } = nativeEvent;
let startIndex: number = -1;
let endIndex: number = -1;
// Get range of this
if (shiftKey) {
const pointKeys = new Set([lastSelectedKey, key]);
recordKeys.some((recordKey, recordIndex) => {
if (pointKeys.has(recordKey)) {
if (startIndex === -1) {
startIndex = recordIndex;
} else {
endIndex = recordIndex;
return true;
}
}
return false;
});
}
if (endIndex !== -1 && startIndex !== endIndex) {
// Batch update selections
const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
const changedKeys: Key[] = [];
if (hasKey) {
rangeKeys.forEach(recordKey => {
if (keySet.has(recordKey)) {
changedKeys.push(recordKey);
keySet.delete(recordKey);
}
});
} else {
rangeKeys.forEach(recordKey => {
if (!keySet.has(recordKey)) {
changedKeys.push(recordKey);
keySet.add(recordKey);
}
});
}
const keys = Array.from(keySet);
setSelectedKeys(keys);
if (onSelectMultiple) {
onSelectMultiple(
!hasKey,
keys.map(recordKey => getRecordByKey(recordKey)),
changedKeys.map(recordKey => getRecordByKey(recordKey)),
);
}
} else {
// Single record selected
if (hasKey) {
keySet.delete(key);
} else {
keySet.add(key);
}
triggerSingleSelection(key, !hasKey, Array.from(keySet), nativeEvent);
}
setLastSelectedKey(key);
}}
/>
);
};
}
// Columns
const selectionColumn = {
width: selectionColWidth,
className: `${prefixCls}-selection-column`,
title: rowSelection.columnTitle || title,
render: renderCell,
};
if (expandType === 'row' && columns.length) {
const [expandColumn, ...restColumns] = columns;
return [
expandColumn,
{ ...selectionColumn, fixed: getFixedType(restColumns[0]) },
...restColumns,
];
}
return [{ ...selectionColumn, fixed: getFixedType(columns[0]) }, ...columns];
},
[
getRowKey,
pageData,
rowSelection,
innerSelectedKeys,
mergedSelectedKeys,
selectionColWidth,
mergedSelections,
expandType,
lastSelectedKey,
onSelectMultiple,
triggerSingleSelection,
],
);
return [transformColumns, mergedSelectedKeySet];
}

View File

@ -0,0 +1,376 @@
import * as React from 'react';
import classNames from 'classnames';
import { CaretDown, CaretUp } from '@ant-design/icons';
import {
TransformColumns,
ColumnsType,
Key,
ColumnType,
SortOrder,
CompareFn,
ColumnTitleProps,
SorterResult,
} from '../interface';
import { getColumnKey, getColumnPos, renderColumnTitle } from '../util';
function getMultiplePriority<RecordType>(column: ColumnType<RecordType>): number | false {
if (typeof column.sorter === 'object' && typeof column.sorter.multiple === 'number') {
return column.sorter.multiple;
}
return false;
}
function getSortFunction<RecordType>(
sorter: ColumnType<RecordType>['sorter'],
): CompareFn<RecordType> | false {
if (typeof sorter === 'function') {
return sorter;
}
if (sorter && typeof sorter === 'object' && sorter.compare) {
return sorter.compare;
}
return false;
}
function nextSortDirection(sortDirections: SortOrder[], current: SortOrder | null) {
if (!current) {
return sortDirections[0];
}
return sortDirections[sortDirections.indexOf(current) + 1];
}
export interface SortState<RecordType> {
column: ColumnType<RecordType>;
key: Key;
sortOrder: SortOrder | null;
multiplePriority: number | false;
}
function collectSortStates<RecordType>(
columns: ColumnsType<RecordType>,
init: boolean,
pos?: string,
): SortState<RecordType>[] {
let sortStates: SortState<RecordType>[] = [];
columns.forEach((column, index) => {
const columnPos = getColumnPos(index, pos);
if ('children' in column) {
sortStates = [...sortStates, ...collectSortStates(column.children, init, columnPos)];
} else if ('sorter' in column) {
if ('sortOrder' in column) {
// Controlled
sortStates.push({
column,
key: getColumnKey(column, columnPos),
multiplePriority: getMultiplePriority(column),
sortOrder: column.sortOrder!,
});
} else if (init && column.defaultSortOrder) {
// Default sorter
sortStates.push({
column,
key: getColumnKey(column, columnPos),
multiplePriority: getMultiplePriority(column),
sortOrder: column.defaultSortOrder!,
});
}
}
});
return sortStates;
}
function injectSorter<RecordType>(
prefixCls: string,
columns: ColumnsType<RecordType>,
sorterSates: SortState<RecordType>[],
triggerSorter: (sorterSates: SortState<RecordType>) => void,
defaultSortDirections: SortOrder[],
pos?: string,
): ColumnsType<RecordType> {
return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos);
let newColumn: ColumnsType<RecordType>[number] = column;
if ('sorter' in newColumn) {
const sortDirections: SortOrder[] = newColumn.sortDirections || defaultSortDirections;
const columnKey = getColumnKey(newColumn, columnPos);
const sorterState = sorterSates.find(({ key }) => key === columnKey);
const sorterOrder = sorterState ? sorterState.sortOrder : null;
const upNode: React.ReactNode = sortDirections.includes('ascend') && (
<CaretUp
className={classNames(`${prefixCls}-column-sorter-up`, {
active: sorterOrder === 'ascend',
})}
/>
);
const downNode: React.ReactNode = sortDirections.includes('descend') && (
<CaretDown
className={classNames(`${prefixCls}-column-sorter-down`, {
active: sorterOrder === 'descend',
})}
/>
);
newColumn = {
...newColumn,
className: classNames(newColumn.className, { [`${prefixCls}-column-sort`]: sorterOrder }),
title: (renderProps: ColumnTitleProps<RecordType>) => (
<div className={`${prefixCls}-column-sorters`}>
<span>{renderColumnTitle(column.title, renderProps)}</span>
<span
className={classNames(`${prefixCls}-column-sorter`, {
[`${prefixCls}-column-sorter-full`]: upNode && downNode,
})}
>
<span className={`${prefixCls}-column-sorter-inner`}>
{upNode}
{downNode}
</span>
</span>
</div>
),
onHeaderCell: col => {
const cell: React.HTMLAttributes<HTMLElement> =
(column.onHeaderCell && column.onHeaderCell(col)) || {};
const originOnClick = cell.onClick;
cell.onClick = (event: React.MouseEvent<HTMLElement>) => {
triggerSorter({
column,
key: columnKey,
sortOrder: nextSortDirection(sortDirections, sorterOrder),
multiplePriority: getMultiplePriority(column),
});
if (originOnClick) {
originOnClick(event);
}
};
cell.className = classNames(cell.className, `${prefixCls}-column-has-sorters`);
return cell;
},
};
}
if ('children' in newColumn) {
newColumn = {
...newColumn,
children: injectSorter(
prefixCls,
newColumn.children,
sorterSates,
triggerSorter,
defaultSortDirections,
columnPos,
),
};
}
return newColumn;
});
}
function stateToInfo<RecordType>(sorterStates: SortState<RecordType>) {
const { column, sortOrder } = sorterStates;
return { column, order: sortOrder, field: column.dataIndex, columnKey: column.key };
}
function generateSorterInfo<RecordType>(
sorterStates: SortState<RecordType>[],
): SorterResult<RecordType> | SorterResult<RecordType>[] {
const list = sorterStates.filter(({ sortOrder }) => sortOrder).map(stateToInfo);
// =========== Legacy compatible support ===========
// https://github.com/ant-design/ant-design/pull/19226
if (list.length === 0 && sorterStates.length) {
return {
...stateToInfo(sorterStates[0]),
column: undefined,
};
}
if (list.length <= 1) {
return list[0] || {};
}
return list;
}
export function getSortData<RecordType>(
data: RecordType[],
sortStates: SortState<RecordType>[],
childrenColumnName: string,
): RecordType[] {
const innerSorterStates = sortStates
.slice()
.sort((a, b) => (b.multiplePriority as number) - (a.multiplePriority as number));
const cloneData = data.slice();
const runningSorters = innerSorterStates.filter(({ column: { sorter }, sortOrder }) => {
return getSortFunction(sorter) && sortOrder;
});
// Skip if no sorter needed
if (!runningSorters.length) {
return cloneData;
}
return cloneData
.sort((record1, record2) => {
for (let i = 0; i < runningSorters.length; i += 1) {
const sorterState = runningSorters[i];
const {
column: { sorter },
sortOrder,
} = sorterState;
const compareFn = getSortFunction(sorter);
if (compareFn && sortOrder) {
const compareResult = compareFn(record1, record2, sortOrder);
if (compareResult !== 0) {
return sortOrder === 'ascend' ? compareResult : -compareResult;
}
}
}
return 0;
})
.map<RecordType>(record => {
const subRecords = (record as any)[childrenColumnName];
if (subRecords) {
return {
...record,
[childrenColumnName]: getSortData(subRecords, sortStates, childrenColumnName),
};
}
return record;
});
}
interface SorterConfig<RecordType> {
prefixCls: string;
columns: ColumnsType<RecordType>;
onSorterChange: (
sorterResult: SorterResult<RecordType> | SorterResult<RecordType>[],
sortStates: SortState<RecordType>[],
) => void;
sortDirections: SortOrder[];
}
export default function useFilterSorter<RecordType>({
prefixCls,
columns,
onSorterChange,
sortDirections,
}: SorterConfig<RecordType>): [
TransformColumns<RecordType>,
SortState<RecordType>[],
ColumnTitleProps<RecordType>,
() => SorterResult<RecordType> | SorterResult<RecordType>[],
] {
const [sortStates, setSortStates] = React.useState<SortState<RecordType>[]>(
collectSortStates(columns, true),
);
const mergedSorterStates = React.useMemo(() => {
let validate = true;
const collectedStates = collectSortStates(columns, false);
// Return if not controlled
if (!collectedStates.length) {
return sortStates;
}
const validateStates: SortState<RecordType>[] = [];
function patchStates(state: SortState<RecordType>) {
if (validate) {
validateStates.push(state);
} else {
validateStates.push({
...state,
sortOrder: null,
});
}
}
let multipleMode: boolean | null = null;
collectedStates.forEach(state => {
if (multipleMode === null) {
patchStates(state);
if (state.sortOrder) {
if (state.multiplePriority === false) {
validate = false;
} else {
multipleMode = true;
}
}
} else if (multipleMode && state.multiplePriority !== false) {
patchStates(state);
} else {
validate = false;
patchStates(state);
}
});
return validateStates;
}, [columns, sortStates]);
// Get render columns title required props
const columnTitleSorterProps = React.useMemo<ColumnTitleProps<RecordType>>(() => {
const sortColumns = mergedSorterStates.map(({ column, sortOrder }) => ({
column,
order: sortOrder,
}));
return {
sortColumns,
// Legacy
sortColumn: sortColumns[0] && sortColumns[0].column,
sortOrder: sortColumns[0] && sortColumns[0].order,
};
}, [mergedSorterStates]);
function triggerSorter(sortState: SortState<RecordType>) {
let newSorterStates;
if (
sortState.multiplePriority === false ||
!mergedSorterStates.length ||
mergedSorterStates[0].multiplePriority === false
) {
newSorterStates = [sortState];
} else {
newSorterStates = [
...mergedSorterStates.filter(({ key }) => key !== sortState.key),
sortState,
];
}
setSortStates(newSorterStates);
onSorterChange(generateSorterInfo(newSorterStates), newSorterStates);
}
const transformColumns = React.useCallback(
(innerColumns: ColumnsType<RecordType>) =>
injectSorter(prefixCls, innerColumns, mergedSorterStates, triggerSorter, sortDirections),
[mergedSorterStates],
);
const getSorters = () => {
return generateSorterInfo(mergedSorterStates);
};
return [transformColumns, mergedSorterStates, columnTitleSorterProps, getSorters];
}

View File

@ -0,0 +1,31 @@
import * as React from 'react';
import { TransformColumns, ColumnTitleProps, ColumnsType } from '../interface';
import { renderColumnTitle } from '../util';
function fillTitle<RecordType>(
columns: ColumnsType<RecordType>,
columnTitleProps: ColumnTitleProps<RecordType>,
) {
return columns.map(column => {
const cloneColumn = { ...column };
cloneColumn.title = renderColumnTitle(column.title, columnTitleProps);
if ('children' in cloneColumn) {
cloneColumn.children = fillTitle(cloneColumn.children, columnTitleProps);
}
return cloneColumn;
});
}
export default function useTitleColumns<RecordType>(
columnTitleProps: ColumnTitleProps<RecordType>,
): [TransformColumns<RecordType>] {
const filledColumns = React.useCallback(
(columns: ColumnsType<RecordType>) => fillTitle(columns, columnTitleProps),
[columnTitleProps],
);
return [filledColumns];
}

View File

@ -61,18 +61,11 @@ const columns = [
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| tableLayout | [table-layout](https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout) attribute of table element | - \| 'auto' \| 'fixed' | -<hr />`fixed` when header/columns are fixed, or using `column.ellipsis` | 3.24.0 | | tableLayout | [table-layout](https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout) attribute of table element | - \| 'auto' \| 'fixed' | -<hr />`fixed` when header/columns are fixed, or using `column.ellipsis` | 3.24.0 |
| bordered | Whether to show all table borders | boolean | `false` | | | bordered | Whether to show all table borders | boolean | `false` | |
| childrenColumnName | The column contains children to display | string\[] | children | 3.4.2 |
| columns | Columns of table | [ColumnProps](https://git.io/vMMXC)\[] | - | | | columns | Columns of table | [ColumnProps](https://git.io/vMMXC)\[] | - | |
| components | Override default table elements | [TableComponents](https://git.io/fANxz) | - | | | components | Override default table elements | [TableComponents](https://git.io/fANxz) | - | |
| dataSource | Data record array to be displayed | any\[] | - | | | dataSource | Data record array to be displayed | any\[] | - | |
| defaultExpandAllRows | Expand all rows initially | boolean | `false` | | | expandable | Config expandable content | [expandable](#expandable) | - | |
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - | |
| expandedRowKeys | Current expanded row keys | string\[] | - | |
| expandedRowRender | Expanded container render for each row | Function(record, index, indent, expanded):ReactNode | - | |
| expandIcon | Customize row expand Icon. Ref [example](http://react-component.github.io/table/examples/expandIcon.html) | Function(props):ReactNode | - | 3.11.3 |
| expandRowByClick | Whether to expand row by clicking anywhere in the whole row | boolean | `false` | 3.0.1 |
| footer | Table footer renderer | Function(currentPageData) | | | | footer | Table footer renderer | Function(currentPageData) | | |
| indentSize | Indent size in pixels of tree data | number | 15 | |
| loading | Loading status of table | boolean\|[object](https://ant.design/components/spin-cn/#API) ([more](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | `false` | | | loading | Loading status of table | boolean\|[object](https://ant.design/components/spin-cn/#API) ([more](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | `false` | |
| locale | i18n text including filter, sort, empty text, etc | object | filterConfirm: 'Ok' <br> filterReset: 'Reset' <br> emptyText: 'No Data' <br> [Default](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) | | | locale | i18n text including filter, sort, empty text, etc | object | filterConfirm: 'Ok' <br> filterReset: 'Reset' <br> emptyText: 'No Data' <br> [Default](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) | |
| pagination | Config of pagination. You can ref table pagination [config](#pagination) or full [`pagination`](/components/pagination/) document, hide it by setting it to `false` | object | | | | pagination | Config of pagination. You can ref table pagination [config](#pagination) or full [`pagination`](/components/pagination/) document, hide it by setting it to `false` | object | | |
@ -82,10 +75,9 @@ const columns = [
| scroll | Whether the table can be scrollable, [config](#scroll) | object | - | | | scroll | Whether the table can be scrollable, [config](#scroll) | object | - | |
| showHeader | Whether to show table header | boolean | `true` | | | showHeader | Whether to show table header | boolean | `true` | |
| size | Size of table | `default` \| `middle` \| `small` | `default` | | | size | Size of table | `default` \| `middle` \| `small` | `default` | |
| summary | Summary content | (currentData) => ReactNode | - | |
| title | Table title renderer | Function(currentPageData) | | | | title | Table title renderer | Function(currentPageData) | | |
| onChange | Callback executed when pagination, filters or sorter is changed | Function(pagination, filters, sorter, extra: { currentDataSource: [] }) | | | | onChange | Callback executed when pagination, filters or sorter is changed | Function(pagination, filters, sorter, extra: { currentDataSource: [] }) | | |
| onExpand | Callback executed when the row expand icon is clicked | Function(expanded, record) | | |
| onExpandedRowsChange | Callback executed when the expanded rows change | Function(expandedRows) | | |
| onHeaderRow | Set props on per header row | Function(column, index) | - | | | onHeaderRow | Set props on per header row | Function(column, index) | - | |
| onRow | Set props on per row | Function(record, index) | - | | | onRow | Set props on per row | Function(record, index) | - | |
| getPopupContainer | the render container of dropdowns in table | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 3.21.0 | | getPopupContainer | the render container of dropdowns in table | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 3.21.0 |
@ -120,10 +112,10 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| align | specify which way that column is aligned | 'left' \| 'right' \| 'center' | 'left' | 3.3.2 | | align | specify which way that column is aligned | 'left' \| 'right' \| 'center' | 'left' | 3.3.2 |
| ellipsis | ellipsize cell content, not working with sorter and filters for now.<br />tableLayout would be `fixed` when `ellipsis` is true. | boolean | false | 3.24.0 | | ellipsis | ellipsis cell content, not working with sorter and filters for now.<br />tableLayout would be `fixed` when `ellipsis` is true. | boolean | false | 3.24.0 |
| className | className of this column | string | - | | | className | className of this column | string | - | |
| colSpan | Span of this column's title | number | | | | colSpan | Span of this column's title | number | | |
| dataIndex | Display field of the data record, could be set like `a.b.c`, `a[0].b.c[1]` | string | - | | | dataIndex | Display field of the data record, support nest path by string array | string \| string\[] | - | |
| defaultSortOrder | Default order of sorted values | 'ascend' \| 'descend' | - | | | defaultSortOrder | Default order of sorted values | 'ascend' \| 'descend' | - | |
| filterDropdown | Customized filter overlay | React.ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => React.ReactNode | - | | filterDropdown | Customized filter overlay | React.ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => React.ReactNode | - |
| filterDropdownVisible | Whether `filterDropdown` is visible | boolean | - | | | filterDropdownVisible | Whether `filterDropdown` is visible | boolean | - | |
@ -161,6 +153,24 @@ Properties for pagination.
More about pagination, please check [`Pagination`](/components/pagination/). More about pagination, please check [`Pagination`](/components/pagination/).
### expandable
Properties for expandable.
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| childrenColumnName | The column contains children to display | string\[] | children |
| defaultExpandAllRows | Expand all rows initially | boolean | `false` |
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - |
| expandIcon | Customize row expand Icon. Ref [example](http://react-component.github.io/table/examples/expandIcon.html) | Function(props):ReactNode | - |
| expandedRowKeys | Current expanded row keys | string\[] | - |
| expandedRowRender | Expanded container render for each row | Function(record, index, indent, expanded):ReactNode | - |
| expandRowByClick | Whether to expand row by clicking anywhere in the whole row | boolean | `false` |
| indentSize | Indent size in pixels of tree data | number | 15 |
| rowExpandable | Enable row can be expandable | (record) => boolean | |
| onExpand | Callback executed when the row expand icon is clicked | Function(expanded, record) | |
| onExpandedRowsChange | Callback executed when the expanded rows change | Function(expandedRows) | |
### rowSelection ### rowSelection
Properties for row selection. Properties for row selection.
@ -251,3 +261,15 @@ return <Table rowKey="uid" />;
// or // or
return <Table rowKey={record => record.uid} />; return <Table rowKey={record => record.uid} />;
``` ```
## Migrate to v4
Table removes `onRowClick`, `onRowDoubleClick`, `onRowMouseEnter`, `onRowMouseLeave` and some other api which is already deprecated in v3. If you only use api listing in official document, that's OK.
Besides, the breaking change is changing `dataIndex` from nest string path like `user.age` to string array path like `['user', 'age']`. This help to resolve developer should additional work on the field which contains `.`.
## FAQ
### How to hide pagination when single page or not data
You can set `hideOnSinglePage` with `pagination` prop.

View File

@ -1,5 +1,3 @@
import Table from './Table'; import Table from './Table';
export * from './interface';
export default Table; export default Table;

View File

@ -66,18 +66,11 @@ const columns = [
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| tableLayout | 表格元素的 [table-layout](https://developer.mozilla.org/zh-CN/docs/Web/CSS/table-layout) 属性,设为 `fixed` 表示内容不会影响列的布局 | - \| 'auto' \| 'fixed' | 无<hr />固定表头/列或使用了 `column.ellipsis` 时,默认值为 `fixed` | 3.24.0 | | tableLayout | 表格元素的 [table-layout](https://developer.mozilla.org/zh-CN/docs/Web/CSS/table-layout) 属性,设为 `fixed` 表示内容不会影响列的布局 | - \| 'auto' \| 'fixed' | 无<hr />固定表头/列或使用了 `column.ellipsis` 时,默认值为 `fixed` | 3.24.0 |
| bordered | 是否展示外边框和列边框 | boolean | false | | | bordered | 是否展示外边框和列边框 | boolean | false | |
| childrenColumnName | 指定树形结构的列名 | string\[] | children | 3.4.2 |
| columns | 表格列的配置描述,具体项见下表 | [ColumnProps](https://git.io/vMMXC)\[] | - | | | columns | 表格列的配置描述,具体项见下表 | [ColumnProps](https://git.io/vMMXC)\[] | - | |
| components | 覆盖默认的 table 元素 | [TableComponents](https://git.io/fANxz) | - | | | components | 覆盖默认的 table 元素 | [TableComponents](https://git.io/fANxz) | - | |
| dataSource | 数据数组 | any\[] | | | | dataSource | 数据数组 | any\[] | | |
| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false | | | expandable | 配置展开属性 | [expandable](#expandable) | - | |
| defaultExpandedRowKeys | 默认展开的行 | string\[] | - | |
| expandedRowKeys | 展开的行,控制属性 | string\[] | - | |
| expandedRowRender | 额外的展开行 | Function(record, index, indent, expanded):ReactNode | - | |
| expandIcon | 自定义展开图标,参考[示例](http://react-component.github.io/table/examples/expandIcon.html) | Function(props):ReactNode | - | 3.11.3 |
| expandRowByClick | 通过点击行来展开子行 | boolean | `false` | 3.0.1 |
| footer | 表格尾部 | Function(currentPageData) | | | | footer | 表格尾部 | Function(currentPageData) | | |
| indentSize | 展示树形数据时,每层缩进的宽度,以 px 为单位 | number | 15 | |
| loading | 页面是否加载中 | boolean\|[object](https://ant.design/components/spin-cn/#API) ([更多](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | false | | | loading | 页面是否加载中 | boolean\|[object](https://ant.design/components/spin-cn/#API) ([更多](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | false | |
| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: '确定' <br> filterReset: '重置' <br> emptyText: '暂无数据' <br> [默认值](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) | | | locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: '确定' <br> filterReset: '重置' <br> emptyText: '暂无数据' <br> [默认值](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) | |
| pagination | 分页器,参考[配置项](#pagination)或 [pagination](/components/pagination/) 文档,设为 false 时不展示和进行分页 | object | | | | pagination | 分页器,参考[配置项](#pagination)或 [pagination](/components/pagination/) 文档,设为 false 时不展示和进行分页 | object | | |
@ -87,10 +80,9 @@ const columns = [
| scroll | 表格是否可滚动,[配置项](#scroll) | object | - | | | scroll | 表格是否可滚动,[配置项](#scroll) | object | - | |
| showHeader | 是否显示表头 | boolean | true | | | showHeader | 是否显示表头 | boolean | true | |
| size | 表格大小 | default \| middle \| small | default | | | size | 表格大小 | default \| middle \| small | default | |
| summary | 总结栏 | (currentData) => ReactNode | - | |
| title | 表格标题 | Function(currentPageData) | | | | title | 表格标题 | Function(currentPageData) | | |
| onChange | 分页、排序、筛选变化时触发 | Function(pagination, filters, sorter, extra: { currentDataSource: [] }) | | | | onChange | 分页、排序、筛选变化时触发 | Function(pagination, filters, sorter, extra: { currentDataSource: [] }) | | |
| onExpand | 点击展开图标时触发 | Function(expanded, record) | | |
| onExpandedRowsChange | 展开的行变化时触发 | Function(expandedRows) | | |
| onHeaderRow | 设置头部行属性 | Function(column, index) | - | | | onHeaderRow | 设置头部行属性 | Function(column, index) | - | |
| onRow | 设置行属性 | Function(record, index) | - | | | onRow | 设置行属性 | Function(record, index) | - | |
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 3.21.0 | | getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 3.21.0 |
@ -128,7 +120,7 @@ const columns = [
| ellipsis | 超过宽度将自动省略,暂不支持和排序筛选一起使用。<br />设置为 `true` 时,表格布局将变成 `tableLayout="fixed"`。 | boolean | false | 3.24.0 | | ellipsis | 超过宽度将自动省略,暂不支持和排序筛选一起使用。<br />设置为 `true` 时,表格布局将变成 `tableLayout="fixed"`。 | boolean | false | 3.24.0 |
| className | 列样式类名 | string | - | | | className | 列样式类名 | string | - | |
| colSpan | 表头列合并,设置为 0 时,不渲染 | number | | | | colSpan | 表头列合并,设置为 0 时,不渲染 | number | | |
| dataIndex | 列数据在数据项中对应的 key支持 `a.b.c`、`a[0].b.c[1]` 的嵌套写法 | string | - | | | dataIndex | 列数据在数据项中对应的路径,支持通过数组查询嵌套路径 | string \| string\[] | - | |
| defaultSortOrder | 默认排序顺序 | 'ascend' \| 'descend' | - | 3.9.3 | | defaultSortOrder | 默认排序顺序 | 'ascend' \| 'descend' | - | 3.9.3 |
| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | React.ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => React.ReactNode | - | | filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | React.ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => React.ReactNode | - |
| filterDropdownVisible | 用于控制自定义筛选菜单是否可见 | boolean | - | | | filterDropdownVisible | 用于控制自定义筛选菜单是否可见 | boolean | - | |
@ -166,6 +158,24 @@ const columns = [
更多配置项,请查看 [`Pagination`](/components/pagination/)。 更多配置项,请查看 [`Pagination`](/components/pagination/)。
### expandable
展开功能的配置。
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| childrenColumnName | 指定树形结构的列名 | string\[] | children |
| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false |
| defaultExpandedRowKeys | 默认展开的行 | string\[] | |
| expandIcon | 自定义展开图标,参考[示例](http://react-component.github.io/table/examples/expandIcon.html) | Function(props):ReactNode | |
| expandedRowKeys | 展开的行,控制属性 | string\[] | |
| expandedRowRender | 额外的展开行 | Function(record, index, indent, expanded):ReactNode | |
| expandRowByClick | 通过点击行来展开子行 | boolean | `false` |
| indentSize | 展示树形数据时,每层缩进的宽度,以 px 为单位 | number | 15 |
| rowExpandable | 设置是否允许行展开 | (record) => boolean | |
| onExpand | 点击展开图标时触发 | Function(expanded, record) | |
| onExpandedRowsChange | 展开的行变化时触发 | Function(expandedRows) | |
### rowSelection ### rowSelection
选择功能的配置。 选择功能的配置。
@ -255,3 +265,15 @@ return <Table rowKey="uid" />;
// 或 // 或
return <Table rowKey={record => record.uid} />; return <Table rowKey={record => record.uid} />;
``` ```
## 从 v3 升级到 v4
Table 移除了在 v3 中废弃的 `onRowClick`、`onRowDoubleClick`、`onRowMouseEnter`、`onRowMouseLeave` 等方法。如果你使用的 api 为文档中列举的 api那你不用担心会丢失功能。
此外,比较重大的改动为 `dataIndex` 从支持路径嵌套如 `user.age` 改成了数组路径如 `['user', 'age']`。以解决过去属性名带 `.` 需要额外的数据转化问题。
## FAQ
### 如何在没有数据或只有一页数据时隐藏分页栏
你可以设置 `pagination``hideOnSinglePage` 属性为 `true`

View File

@ -1,87 +1,23 @@
import * as React from 'react'; import {
import { SpinProps } from '../spin'; GetRowKey,
import { Store } from './createStore'; ColumnGroupType,
import { RadioChangeEvent } from '../radio'; ColumnType as RcColumnType,
import { CheckboxChangeEvent } from '../checkbox'; ExpandableConfig,
} from 'rc-table/lib/interface';
import { CheckboxProps } from '../checkbox';
import { PaginationConfig } from '../pagination'; import { PaginationConfig } from '../pagination';
import { tuple } from '../_util/type';
const ColumnFixedPlacements = tuple('left', 'right'); export { GetRowKey, ExpandableConfig };
// eslint-disable-next-line import/prefer-default-export export type Key = React.Key;
export { PaginationConfig } from '../pagination';
export type CompareFn<T> = (a: T, b: T, sortOrder?: SortOrder) => number; export type RowSelectionType = 'checkbox' | 'radio';
export type ColumnFilterItem = {
text: React.ReactNode;
value: string;
children?: ColumnFilterItem[];
};
export interface FilterDropdownProps { export type SelectionItemSelectFn = (currentRowKeys: Key[]) => void;
prefixCls?: string;
setSelectedKeys?: (selectedKeys: string[]) => void;
selectedKeys?: React.Key[];
confirm?: () => void;
clearFilters?: () => void;
filters?: ColumnFilterItem[];
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
visible?: boolean;
}
export interface ColumnProps<T> { export type TableSize = 'default' | 'middle' | 'small';
title?:
| React.ReactNode
| ((options: {
filters: TableStateFilters;
sortOrder?: SortOrder;
sortColumn?: ColumnProps<T> | null;
}) => React.ReactNode);
key?: React.Key;
dataIndex?: string; // Note: We can not use generic type here, since we need to support nested key, see #9393
render?: (text: any, record: T, index: number) => React.ReactNode;
align?: 'left' | 'right' | 'center';
ellipsis?: boolean;
filters?: ColumnFilterItem[];
onFilter?: (value: any, record: T) => boolean;
filterMultiple?: boolean;
filterDropdown?: React.ReactNode | ((props: FilterDropdownProps) => React.ReactNode);
filterDropdownVisible?: boolean;
onFilterDropdownVisibleChange?: (visible: boolean) => void;
sorter?: boolean | CompareFn<T>;
defaultSortOrder?: SortOrder;
colSpan?: number;
width?: string | number;
className?: string;
fixed?: boolean | typeof ColumnFixedPlacements[number];
filterIcon?: React.ReactNode | ((filtered: boolean) => React.ReactNode);
filteredValue?: any[];
sortOrder?: SortOrder | boolean;
children?: ColumnProps<T>[];
onCellClick?: (record: T, event: Event) => void;
onCell?: (record: T, rowIndex: number) => TableEventListeners;
onHeaderCell?: (props: ColumnProps<T>) => TableEventListeners;
sortDirections?: SortOrder[];
}
export interface AdditionalCellProps { export type ExpandType = null | 'row' | 'nest';
onClick?: React.MouseEventHandler<HTMLElement>;
[name: string]: any;
}
export interface TableComponents {
table?: React.ReactType;
header?: {
wrapper?: React.ReactType;
row?: React.ReactType;
cell?: React.ReactType;
};
body?: {
wrapper?: React.ReactType;
row?: React.ReactType;
cell?: React.ReactType;
};
}
export interface TableLocale { export interface TableLocale {
filterTitle?: string; filterTitle?: string;
@ -90,154 +26,75 @@ export interface TableLocale {
emptyText?: React.ReactNode | (() => React.ReactNode); emptyText?: React.ReactNode | (() => React.ReactNode);
selectAll?: React.ReactNode; selectAll?: React.ReactNode;
selectInvert?: React.ReactNode; selectInvert?: React.ReactNode;
selectionAll?: React.ReactNode;
sortTitle?: string; sortTitle?: string;
expand?: string; expand?: string;
collapse?: string; collapse?: string;
} }
export type RowSelectionType = 'checkbox' | 'radio'; export type SortOrder = 'descend' | 'ascend' | null;
export type SelectionSelectFn<T> = (
record: T,
selected: boolean,
selectedRows: Object[],
nativeEvent: Event,
) => void;
export type TableSelectWay = 'onSelect' | 'onSelectMultiple' | 'onSelectAll' | 'onSelectInvert'; export type CompareFn<T> = (a: T, b: T, sortOrder?: SortOrder) => number;
export interface TableRowSelection<T> { export interface ColumnFilterItem {
type?: RowSelectionType; text: React.ReactNode;
selectedRowKeys?: string[] | number[]; value: string;
onChange?: (selectedRowKeys: string[] | number[], selectedRows: T[]) => void; children?: ColumnFilterItem[];
getCheckboxProps?: (record: T) => Object;
onSelect?: SelectionSelectFn<T>;
onSelectMultiple?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
onSelectInvert?: (selectedRowKeys: string[] | number[]) => void;
selections?: SelectionItem[] | boolean;
hideDefaultSelections?: boolean;
fixed?: boolean;
columnWidth?: string | number;
selectWay?: TableSelectWay;
columnTitle?: string | React.ReactNode;
}
export type SortOrder = 'descend' | 'ascend';
export interface SorterResult<T> {
column: ColumnProps<T>;
order: SortOrder;
field: string;
columnKey: string;
}
export type TableSize = 'default' | 'middle' | 'small';
export interface ExpandIconProps<T> {
prefixCls: string;
expanded: boolean;
record: T;
needIndentSpaced: boolean;
expandable: boolean;
onExpand: (record: T, event?: React.MouseEvent) => void;
} }
export interface TableCurrentDataSource<T> { export interface ColumnTitleProps<RecordType> {
currentDataSource: T[]; /** @deprecated Please use `sorterColumns` instead. */
}
export interface TableEventListeners {
onClick?: (arg: React.MouseEvent) => void;
onDoubleClick?: (arg: React.MouseEvent) => void;
onContextMenu?: (arg: React.MouseEvent) => void;
onMouseEnter?: (arg: React.MouseEvent) => void;
onMouseLeave?: (arg: React.MouseEvent) => void;
[name: string]: any; // https://github.com/ant-design/ant-design/issues/17245#issuecomment-504807714
}
export interface CheckboxPropsCache {
[key: string]: any;
}
export interface WithStore {
store: Store;
checkboxPropsCache: CheckboxPropsCache;
setCheckboxPropsCache: (cache: CheckboxPropsCache) => void;
}
export interface TableProps<T> {
prefixCls?: string;
dropdownPrefixCls?: string;
rowSelection?: TableRowSelection<T>;
pagination?: PaginationConfig | false;
size?: TableSize;
dataSource?: T[];
components?: TableComponents;
columns?: ColumnProps<T>[];
rowKey?: string | ((record: T, index: number) => string);
rowClassName?: (record: T, index: number) => string;
expandedRowRender?: (
record: T,
index: number,
indent: number,
expanded: boolean,
) => React.ReactNode;
defaultExpandAllRows?: boolean;
defaultExpandedRowKeys?: string[] | number[];
expandedRowKeys?: string[] | number[];
expandIcon?: (props: ExpandIconProps<T>) => React.ReactNode;
expandIconAsCell?: boolean;
expandIconColumnIndex?: number;
expandRowByClick?: boolean;
onExpandedRowsChange?: (expandedRowKeys: string[] | number[]) => void;
onExpand?: (expanded: boolean, record: T) => void;
onChange?: (
pagination: PaginationConfig,
filters: Record<keyof T, string[]>,
sorter: SorterResult<T>,
extra: TableCurrentDataSource<T>,
) => void;
loading?: boolean | SpinProps;
locale?: TableLocale;
indentSize?: number;
onRowClick?: (record: T, index: number, event: Event) => void;
onRow?: (record: T, index: number) => TableEventListeners;
onHeaderRow?: (columns: ColumnProps<T>[]) => TableEventListeners;
useFixedHeader?: boolean;
bordered?: boolean;
showHeader?: boolean;
footer?: (currentPageData: T[]) => React.ReactNode;
title?: (currentPageData: T[]) => React.ReactNode;
scroll?: {
x?: boolean | number | string;
y?: boolean | number | string;
scrollToFirstRowOnChange?: boolean;
};
childrenColumnName?: string;
bodyStyle?: React.CSSProperties;
className?: string;
style?: React.CSSProperties;
tableLayout?: React.CSSProperties['tableLayout'];
children?: React.ReactNode;
sortDirections?: SortOrder[];
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
}
export type InternalTableProps<T> = TableProps<T> & WithStore;
export interface TableStateFilters {
[key: string]: string[];
}
export interface TableState<T> {
pagination: PaginationConfig;
filters: TableStateFilters;
sortColumn: ColumnProps<T> | null;
sortOrder?: SortOrder; sortOrder?: SortOrder;
pivot?: number; /** @deprecated Please use `sorterColumns` instead. */
prevProps: TableProps<T>; sortColumn?: ColumnType<RecordType>;
components: TableComponents; sortColumns?: { column: ColumnType<RecordType>; order: SortOrder }[];
columns: ColumnProps<T>[];
filters?: Record<string, string[]>;
} }
export type SelectionItemSelectFn = (key: string[]) => void; export type ColumnTitle<RecordType> =
type GetPopupContainer = (triggerNode?: HTMLElement) => HTMLElement; | React.ReactNode
| ((props: ColumnTitleProps<RecordType>) => React.ReactNode);
export interface FilterDropdownProps {
prefixCls: string;
setSelectedKeys: (selectedKeys: string[]) => void;
selectedKeys: React.Key[];
confirm: () => void;
clearFilters: (selectedKeys: string[]) => void;
filters?: ColumnFilterItem[];
visible: boolean;
}
export interface ColumnType<RecordType> extends RcColumnType<RecordType> {
title?: ColumnTitle<RecordType>;
// Sorter
// TODO: Doc this update
sorter?:
| boolean
| CompareFn<RecordType>
| {
compare: CompareFn<RecordType>;
/** Config multiple sorter order priority */
multiple: number;
};
sortOrder?: SortOrder;
defaultSortOrder?: SortOrder;
sortDirections?: SortOrder[];
// Filter
filters?: ColumnFilterItem[];
filterDropdown?: React.ReactNode | ((props: FilterDropdownProps) => React.ReactNode);
filterMultiple?: boolean;
filteredValue?: Key[];
filterIcon?: React.ReactNode | ((filtered: boolean) => React.ReactNode);
onFilter?: (value: any, record: RecordType) => boolean;
filterDropdownVisible?: boolean;
onFilterDropdownVisibleChange?: (visible: boolean) => void;
}
export type ColumnsType<RecordType> = (ColumnGroupType<RecordType> | ColumnType<RecordType>)[];
export interface SelectionItem { export interface SelectionItem {
key: string; key: string;
@ -245,70 +102,48 @@ export interface SelectionItem {
onSelect?: SelectionItemSelectFn; onSelect?: SelectionItemSelectFn;
} }
export interface SelectionCheckboxAllProps<T> { export type SelectionSelectFn<T> = (
store: Store; record: T,
locale: TableLocale; selected: boolean,
disabled: boolean; selectedRows: Object[],
getCheckboxPropsByItem: (item: T, index: number) => { defaultChecked: boolean }; nativeEvent: Event,
getRecordKey: (record: T, index?: number) => string; ) => void;
data: T[];
prefixCls: string | undefined;
onSelect: (key: string, index: number, selectFunc: any) => void;
hideDefaultSelections?: boolean;
selections?: SelectionItem[] | boolean;
getPopupContainer?: GetPopupContainer;
}
export interface SelectionCheckboxAllState { export interface TableRowSelection<T> {
checked?: boolean;
indeterminate?: boolean;
}
export interface SelectionBoxProps {
store: Store;
type?: RowSelectionType; type?: RowSelectionType;
defaultSelection: string[]; selectedRowKeys?: Key[];
rowIndex: string; onChange?: (selectedRowKeys: Key[], selectedRows: T[]) => void;
name?: string; getCheckboxProps?: (record: T) => Partial<CheckboxProps>;
disabled?: boolean; onSelect?: SelectionSelectFn<T>;
onChange: (e: RadioChangeEvent | CheckboxChangeEvent) => void; onSelectMultiple?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectInvert?: (selectedRowKeys: Key[]) => void;
selections?: SelectionItem[] | boolean;
hideDefaultSelections?: boolean;
fixed?: boolean;
columnWidth?: string | number;
columnTitle?: string | React.ReactNode;
} }
export interface SelectionBoxState { export type TransformColumns<RecordType> = (
checked?: boolean; columns: ColumnsType<RecordType>,
) => ColumnsType<RecordType>;
export interface TableCurrentDataSource<RecordType> {
currentDataSource: RecordType[];
} }
export interface SelectionInfo<T> { export interface SorterResult<RecordType> {
selectWay: TableSelectWay; column?: ColumnType<RecordType>;
record?: T; order?: SortOrder;
checked?: boolean; field?: Key | Key[];
changeRowKeys?: React.Key[]; columnKey?: Key;
nativeEvent?: Event;
} }
export interface FilterMenuProps<T> { export type GetPopupContainer = (triggerNode: HTMLElement) => HTMLElement;
locale: TableLocale;
selectedKeys: string[];
column: ColumnProps<T>;
confirmFilter: (column: ColumnProps<T>, selectedKeys: React.Key[]) => any;
prefixCls: string;
dropdownPrefixCls: string;
getPopupContainer?: GetPopupContainer;
}
export interface FilterMenuState<T> { export interface TablePaginationConfig extends PaginationConfig {
selectedKeys: React.Key[]; position?: 'top' | 'bottom' | 'both';
valueKeys: { [name: string]: string };
keyPathOfSelectedItem: { [key: string]: React.Key[] };
visible?: boolean;
prevProps: FilterMenuProps<T>;
} }
export type PrepareParamsArgumentsReturn<T> = [
any,
string[],
Object,
{
currentDataSource: T[];
},
];

View File

@ -0,0 +1,81 @@
@import './index';
@import './size';
@table-border: @border-width-base @border-style-base @border-color-split;
.@{table-prefix-cls}.@{table-prefix-cls}-bordered {
// ============================ Title =============================
.@{table-prefix-cls}-title {
border: @table-border;
border-bottom: 0;
}
// ============================= Cell =============================
thead > tr > th,
tbody > tr > td,
tfoot > tr > th,
tfoot > tr > td {
border-right: @table-border;
}
// Fixed right should provides additional border
.@{table-prefix-cls}-cell-fix-right-first::after {
border-right: @table-border;
}
// ============================ Header ============================
table > {
thead {
> tr:not(:last-child) > th {
border-bottom: @border-width-base @border-style-base @border-color-split;
}
}
}
// =========================== Content ============================
.@{table-prefix-cls}-container {
border: @table-border;
border-right: 0;
border-bottom: 0;
}
// ========================== Expandable ==========================
.@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical (-@table-padding-horizontal - @border-width-base);
&::after {
position: absolute;
top: 0;
right: @border-width-base;
bottom: 0;
border-right: @table-border;
content: '';
}
}
tr.@{table-prefix-cls}-expanded-row,
tr.@{table-prefix-cls}-placeholder {
> td {
border-right: 0;
}
}
// Size related
&.@{table-prefix-cls}-middle {
.@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical-md (-@table-padding-horizontal-md - @border-width-base);
}
}
&.@{table-prefix-cls}-small {
.@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical-sm (-@table-padding-horizontal-sm - @border-width-base);
}
}
// ============================ Footer ============================
.@{table-prefix-cls}-footer {
border: @table-border;
border-top: 0;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,172 +1,35 @@
@import './index';
@table-padding-vertical-md: @table-padding-vertical * 3 / 4; @table-padding-vertical-md: @table-padding-vertical * 3 / 4;
@table-padding-horizontal-md: @table-padding-horizontal / 2; @table-padding-horizontal-md: @table-padding-horizontal / 2;
@table-padding-vertical-sm: @table-padding-vertical / 2; @table-padding-vertical-sm: @table-padding-vertical / 2;
@table-padding-horizontal-sm: @table-padding-horizontal / 2; @table-padding-horizontal-sm: @table-padding-horizontal / 2;
.@{table-prefix-cls}-middle { .table-size(@size, @padding-vertical, @padding-horizontal) {
> .@{table-prefix-cls}-title, .@{table-prefix-cls}.@{table-prefix-cls}-@{size} {
> .@{table-prefix-cls}-footer { .@{table-prefix-cls}-title,
padding: @table-padding-vertical-md @table-padding-horizontal-md; .@{table-prefix-cls}-footer,
} thead > tr > th,
> .@{table-prefix-cls}-content { tbody > tr > td {
> .@{table-prefix-cls}-header > table, padding: @padding-vertical @padding-horizontal;
> .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-fixed-left > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-right > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-left
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table,
> .@{table-prefix-cls}-fixed-right
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table {
> .@{table-prefix-cls}-thead > tr > th,
> .@{table-prefix-cls}-tbody > tr > td {
padding: @table-padding-vertical-md @table-padding-horizontal-md;
}
} }
}
tr.@{table-prefix-cls}-expanded-row td > .@{table-prefix-cls}-wrapper { .@{table-prefix-cls}-filter-column {
margin: -@table-padding-vertical-md -@table-padding-horizontal / 2 -@table-padding-vertical-md - margin: -@padding-vertical -@padding-horizontal;
1px; }
.@{table-prefix-cls}-expanded-row-fixed {
margin: -@padding-vertical -@padding-horizontal;
}
} }
} }
.@{table-prefix-cls}-small { // ================================================================
border: @border-width-base @border-style-base @border-color-split; // = Middle =
border-radius: @table-border-radius-base; // ================================================================
.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md);
> .@{table-prefix-cls}-title, // ================================================================
> .@{table-prefix-cls}-footer { // = Small =
padding: @table-padding-vertical-sm @table-padding-horizontal-sm; // ================================================================
} .table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm);
> .@{table-prefix-cls}-title {
top: 0;
border-bottom: @border-width-base @border-style-base @border-color-split;
}
> .@{table-prefix-cls}-content {
> .@{table-prefix-cls}-body {
margin: 0 @table-padding-horizontal-sm;
}
> .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-fixed-left > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-right > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-left
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table,
> .@{table-prefix-cls}-fixed-right
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table {
border: 0;
> .@{table-prefix-cls}-thead > tr > th,
> .@{table-prefix-cls}-tbody > tr > td {
padding: @table-padding-vertical-sm @table-padding-horizontal-sm;
}
> .@{table-prefix-cls}-thead > tr > th {
background-color: transparent;
}
> .@{table-prefix-cls}-thead > tr {
border-bottom: @border-width-base @border-style-base @border-color-split;
}
> .@{table-prefix-cls}-thead > tr > th.@{table-prefix-cls}-column-sort {
background-color: @table-body-sort-bg;
}
}
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-fixed-left > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-right > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-left
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table,
> .@{table-prefix-cls}-fixed-right
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table {
padding: 0;
}
.@{table-prefix-cls}-header {
background-color: @component-background;
border-radius: @table-border-radius-base @table-border-radius-base 0 0;
}
.@{table-prefix-cls}-placeholder,
.@{table-prefix-cls}-row:last-child td {
border-bottom: 0;
}
}
&.@{table-prefix-cls}-bordered {
border-right: 0;
.@{table-prefix-cls}-title {
border: 0;
border-right: @border-width-base @border-style-base @border-color-split;
border-bottom: @border-width-base @border-style-base @border-color-split;
}
.@{table-prefix-cls}-content {
border-right: @border-width-base @border-style-base @border-color-split;
}
.@{table-prefix-cls}-footer {
border: 0;
border-top: @border-width-base @border-style-base @border-color-split;
border-right: @border-width-base @border-style-base @border-color-split;
&::before {
display: none;
}
}
.@{table-prefix-cls}-placeholder {
border-right: 0;
border-bottom: 0;
border-left: 0;
}
.@{table-prefix-cls}-thead > tr:only-child > th:last-child,
.@{table-prefix-cls}-tbody > tr > td:last-child {
border-right: none;
}
.@{table-prefix-cls}-fixed-left {
.@{table-prefix-cls}-thead > tr > th:last-child,
.@{table-prefix-cls}-tbody > tr > td:last-child {
border-right: @border-width-base @border-style-base @border-color-split;
}
}
.@{table-prefix-cls}-fixed-right {
border-right: @border-width-base @border-style-base @border-color-split;
border-left: @border-width-base @border-style-base @border-color-split;
}
}
tr.@{table-prefix-cls}-expanded-row td > .@{table-prefix-cls}-wrapper {
margin: -@table-padding-vertical-sm -@table-padding-horizontal / 2 -@table-padding-vertical-sm -
1px;
}
// https://github.com/ant-design/ant-design/issues/19287#issuecomment-544368967
&.@{table-prefix-cls}-fixed-header
> .@{table-prefix-cls}-content
> .@{table-prefix-cls}-scroll
> .@{table-prefix-cls}-body {
border-radius: 0 0 @table-border-radius-base @table-border-radius-base;
}
}

28
components/table/util.ts Normal file
View File

@ -0,0 +1,28 @@
/* eslint-disable import/prefer-default-export */
import { ColumnType, ColumnTitle, ColumnTitleProps, Key } from './interface';
export function getColumnKey<RecordType>(column: ColumnType<RecordType>, defaultKey: string): Key {
if ('key' in column && column.key !== undefined) {
return column.key;
}
if (column.dataIndex) {
return Array.isArray(column.dataIndex) ? column.dataIndex.join('.') : column.dataIndex;
}
return defaultKey;
}
export function getColumnPos(index: number, pos?: string) {
return pos ? `${pos}-${index}` : `${index}`;
}
export function renderColumnTitle<RecordType>(
title: ColumnTitle<RecordType>,
props: ColumnTitleProps<RecordType>,
) {
if (typeof title === 'function') {
return title(props);
}
return title;
}

View File

@ -1,80 +0,0 @@
import * as React from 'react';
import { ColumnFilterItem } from './interface';
export function flatArray(data: any[] = [], childrenName = 'children') {
const result: any[] = [];
const loop = (array: any[]) => {
array.forEach(item => {
if (item[childrenName]) {
const newItem = { ...item };
delete newItem[childrenName];
result.push(newItem);
if (item[childrenName].length > 0) {
loop(item[childrenName]);
}
} else {
result.push(item);
}
});
};
loop(data);
return result;
}
export function treeMap<Node>(
tree: Node[],
mapper: (node: Node, index: number) => any,
childrenName = 'children',
) {
return tree.map((node: any, index) => {
const extra: any = {};
if (node[childrenName]) {
extra[childrenName] = treeMap(node[childrenName], mapper, childrenName);
}
return {
...mapper(node as Node, index),
...extra,
};
});
}
export function flatFilter(tree: any[], callback: Function) {
return tree.reduce((acc, node) => {
if (callback(node)) {
acc.push(node);
}
if (node.children) {
const children = flatFilter(node.children, callback);
acc.push(...children);
}
return acc;
}, []);
}
export function normalizeColumns(elements: React.ReactChildren) {
const columns: any[] = [];
React.Children.forEach(elements, element => {
if (!React.isValidElement(element)) {
return;
}
const column: any = {
...(element.props as any),
};
if (element.key) {
column.key = element.key;
}
if (element.type && (element.type as any).__ANT_TABLE_COLUMN_GROUP) {
column.children = normalizeColumns(column.children);
}
columns.push(column);
});
return columns;
}
export function generateValueMaps(items?: ColumnFilterItem[], maps: { [name: string]: any } = {}) {
(items || []).forEach(({ value, children }) => {
maps[value.toString()] = value;
generateValueMaps(children, maps);
});
return maps;
}

File diff suppressed because it is too large Load Diff

View File

@ -117,7 +117,7 @@
"rc-field-form": "^0.0.0-alpha.17", "rc-field-form": "^0.0.0-alpha.17",
"rc-input-number": "~4.5.0", "rc-input-number": "~4.5.0",
"rc-mentions": "~0.4.0", "rc-mentions": "~0.4.0",
"rc-menu": "~8.0.0-alpha.3", "rc-menu": "~8.0.0-alpha.4",
"rc-notification": "~3.3.1", "rc-notification": "~3.3.1",
"rc-pagination": "~1.20.5", "rc-pagination": "~1.20.5",
"rc-progress": "~2.5.0", "rc-progress": "~2.5.0",
@ -127,7 +127,7 @@
"rc-slider": "~8.7.1", "rc-slider": "~8.7.1",
"rc-steps": "~3.5.0", "rc-steps": "~3.5.0",
"rc-switch": "~1.9.0", "rc-switch": "~1.9.0",
"rc-table": "~6.9.4", "rc-table": "~7.0.0-alpha.16",
"rc-tabs": "~9.6.4", "rc-tabs": "~9.6.4",
"rc-time-picker": "~4.0.0-alpha.2", "rc-time-picker": "~4.0.0-alpha.2",
"rc-tooltip": "~3.7.3", "rc-tooltip": "~3.7.3",
@ -230,6 +230,7 @@
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-test-renderer": "^16.8.6", "react-test-renderer": "^16.8.6",
"react-virtualized": "~9.21.1", "react-virtualized": "~9.21.1",
"react-window": "^1.8.5",
"reqwest": "^2.0.5", "reqwest": "^2.0.5",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"scrollama": "^2.0.0", "scrollama": "^2.0.0",

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { Edit } from '@ant-design/icons';
import Icon from '../Icon';
const branchUrl = 'https://github.com/ant-design/ant-design/edit/master/'; const branchUrl = 'https://github.com/ant-design/ant-design/edit/master/';
@ -14,7 +13,7 @@ export default function EditButton({ title, filename }) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Icon type="edit" /> <Edit />
</a> </a>
</Tooltip> </Tooltip>
); );

View File

@ -53,8 +53,6 @@ declare module 'rc-steps';
declare module 'rc-switch'; declare module 'rc-switch';
declare module 'rc-table';
declare module 'rc-upload'; declare module 'rc-upload';
declare module 'rc-form*'; declare module 'rc-form*';