From 1fad2b0277ec4eae386478bdb78ce2b0410b5636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sun, 23 Feb 2020 20:48:16 +0800 Subject: [PATCH] fix: Lots of Descriptions (#21542) * refactor * class name * update demo snapshot * clean up * update snapshot * fix test case * more test case * adjust logic --- components/descriptions/Cell.tsx | 67 +++ components/descriptions/Col.tsx | 72 ---- components/descriptions/Item.tsx | 15 + components/descriptions/Row.tsx | 118 ++++++ .../__tests__/__snapshots__/demo.test.js.snap | 124 +++--- .../__snapshots__/index.test.js.snap | 19 +- .../descriptions/__tests__/index.test.js | 44 +- components/descriptions/index.tsx | 386 ++++++------------ package.json | 2 +- 9 files changed, 459 insertions(+), 388 deletions(-) create mode 100644 components/descriptions/Cell.tsx delete mode 100644 components/descriptions/Col.tsx create mode 100644 components/descriptions/Item.tsx create mode 100644 components/descriptions/Row.tsx diff --git a/components/descriptions/Cell.tsx b/components/descriptions/Cell.tsx new file mode 100644 index 0000000000..0c40f1233a --- /dev/null +++ b/components/descriptions/Cell.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import classNames from 'classnames'; + +export interface CellProps { + itemPrefixCls: string; + span: number; + className?: string; + component: string; + style?: React.CSSProperties; + bordered?: boolean; + label?: React.ReactNode; + content?: React.ReactNode; + colon?: boolean; +} + +const Cell: React.FC = ({ + itemPrefixCls, + component, + span, + className, + style, + bordered, + label, + content, + colon, +}) => { + const Component = component as any; + + if (bordered) { + return ( + + {label || content} + + ); + } + + return ( + + {label && ( + + {label} + + )} + {content && {content}} + + ); +}; + +export default Cell; diff --git a/components/descriptions/Col.tsx b/components/descriptions/Col.tsx deleted file mode 100644 index 73932a6ce6..0000000000 --- a/components/descriptions/Col.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import * as React from 'react'; -import classNames from 'classnames'; -import { DescriptionsItemProps } from './index'; - -interface ColProps { - child: React.ReactElement; - bordered: boolean; - colon: boolean; - type?: 'label' | 'content'; - layout?: 'horizontal' | 'vertical'; -} - -const Col: React.SFC = props => { - const { child, bordered, colon, type, layout } = props; - const { prefixCls, label, className, children, span = 1 } = child.props; - const labelProps: any = { - className: classNames(`${prefixCls}-item-label`, { - [`${prefixCls}-item-colon`]: colon, - [`${prefixCls}-item-no-label`]: !label, - }), - key: 'label', - }; - if (layout === 'vertical') { - labelProps.colSpan = span * 2 - 1; - } - - if (bordered) { - if (type === 'label') { - return {label}; - } - return ( - - {children} - - ); - } - if (layout === 'vertical') { - if (type === 'content') { - return ( - - - {children} - - - ); - } - return ( - - - {label} - - - ); - } - return ( - - {label} - - {children} - - - ); -}; - -export default Col; diff --git a/components/descriptions/Item.tsx b/components/descriptions/Item.tsx new file mode 100644 index 0000000000..5b8095789b --- /dev/null +++ b/components/descriptions/Item.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export interface DescriptionsItemProps { + prefixCls?: string; + className?: string; + style?: React.CSSProperties; + label?: React.ReactNode; + children: React.ReactNode; + span?: number; +} + +const DescriptionsItem: React.SFC = ({ children }) => + children as JSX.Element; + +export default DescriptionsItem; diff --git a/components/descriptions/Row.tsx b/components/descriptions/Row.tsx new file mode 100644 index 0000000000..3e9891031c --- /dev/null +++ b/components/descriptions/Row.tsx @@ -0,0 +1,118 @@ +import * as React from 'react'; +import { DescriptionsItemProps } from './Item'; +import Cell from './Cell'; + +interface CellConfig { + component: string | [string, string]; + type: string; + showLabel?: boolean; + showContent?: boolean; +} + +function renderCells( + items: React.ReactElement[], + { colon, prefixCls, bordered }: RowProps, + { component, type, showLabel, showContent }: CellConfig, +) { + return items.map( + ( + { + props: { + label, + children, + prefixCls: itemPrefixCls = prefixCls, + className, + style, + span = 1, + }, + key, + }, + index, + ) => { + if (typeof component === 'string') { + return ( + + ); + } + + return ( + <> + + + + ); + }, + ); +} + +export interface RowProps { + prefixCls: string; + vertical: boolean; + children: React.ReactElement[]; + bordered?: boolean; + colon: boolean; + index: number; +} + +const Row: React.FC = props => { + const { prefixCls, vertical, children, index, bordered } = props; + if (vertical) { + return ( + <> + + {renderCells(children, props, { component: 'th', type: 'label', showLabel: true })} + + + {renderCells(children, props, { + component: 'td', + type: 'content', + showContent: true, + })} + + + ); + } + + return ( + + {renderCells(children, props, { + component: bordered ? ['th', 'td'] : 'td', + type: 'item', + showLabel: true, + showContent: true, + })} + + ); +}; + +export default Row; diff --git a/components/descriptions/__tests__/__snapshots__/demo.test.js.snap b/components/descriptions/__tests__/__snapshots__/demo.test.js.snap index b39d2e4422..8bfdf67582 100644 --- a/components/descriptions/__tests__/__snapshots__/demo.test.js.snap +++ b/components/descriptions/__tests__/__snapshots__/demo.test.js.snap @@ -121,7 +121,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` class="ant-descriptions-row" > Product @@ -132,7 +133,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` Cloud Database Billing Mode @@ -143,7 +145,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` Prepaid Automatic Renewal @@ -158,7 +161,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` class="ant-descriptions-row" > Order time @@ -169,7 +173,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` 2018-04-24 18:00:00 Usage Time @@ -184,7 +189,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` class="ant-descriptions-row" > Status @@ -210,7 +216,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` class="ant-descriptions-row" > Negotiated Amount @@ -221,7 +228,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` $80.00 Discount @@ -232,7 +240,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` $20.00 Official Receipts @@ -247,7 +256,8 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` class="ant-descriptions-row" > Config Info @@ -294,7 +304,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` class="ant-descriptions-row" > Product @@ -305,7 +316,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` Cloud Database Billing @@ -316,7 +328,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` Prepaid time @@ -331,7 +344,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` class="ant-descriptions-row" > Amount @@ -342,7 +356,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` $80.00 Discount @@ -353,7 +368,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` $20.00 Official @@ -368,7 +384,8 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` class="ant-descriptions-row" > Config Info @@ -479,7 +496,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` class="ant-descriptions-row" > Product @@ -490,7 +508,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` Cloud Database Billing @@ -501,7 +520,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` Prepaid time @@ -516,7 +536,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` class="ant-descriptions-row" > Amount @@ -527,7 +548,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` $80.00 Discount @@ -538,7 +560,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` $20.00 Official @@ -553,7 +576,8 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` class="ant-descriptions-row" > Config Info @@ -716,7 +740,7 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = ` - @@ -725,8 +749,8 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = ` > UserName - - + @@ -735,8 +759,8 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = ` > Telephone - - + @@ -745,7 +769,7 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = ` > Live - + - @@ -793,8 +817,8 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = ` > Address - - + @@ -803,7 +827,7 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = ` > Remark - + Product Billing Mode Automatic Renewal @@ -897,14 +921,14 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] class="ant-descriptions-row" > Order time Usage Time @@ -920,7 +944,7 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] 2019-04-24 18:00:00 @@ -929,8 +953,8 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] class="ant-descriptions-row" > Status @@ -940,7 +964,7 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] > Negotiated Amount Discount Official Receipts @@ -1004,8 +1028,8 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] class="ant-descriptions-row" > Config Info @@ -1015,7 +1039,7 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] > Data disk type: MongoDB
diff --git a/components/descriptions/__tests__/__snapshots__/index.test.js.snap b/components/descriptions/__tests__/__snapshots__/index.test.js.snap index bfc68fb187..1d680b8ffa 100644 --- a/components/descriptions/__tests__/__snapshots__/index.test.js.snap +++ b/components/descriptions/__tests__/__snapshots__/index.test.js.snap @@ -51,9 +51,6 @@ exports[`Descriptions Descriptions support style 1`] = ` class="ant-descriptions-item" colspan="1" > - @@ -268,7 +265,7 @@ exports[`Descriptions vertical layout 1`] = ` - @@ -277,7 +274,7 @@ exports[`Descriptions vertical layout 1`] = ` > Product - + - @@ -305,7 +302,7 @@ exports[`Descriptions vertical layout 1`] = ` > Billing - + - @@ -333,7 +330,7 @@ exports[`Descriptions vertical layout 1`] = ` > time
- + - @@ -361,7 +358,7 @@ exports[`Descriptions vertical layout 1`] = ` > Amount - + { , ); expect(wrapper.find('tr')).toHaveLength(5); - expect(wrapper.find('.ant-descriptions-item-no-label')).toHaveLength(1); + expect(wrapper.find('.ant-descriptions-item-label')).toHaveLength(4); wrapper.unmount(); }); @@ -71,7 +71,7 @@ describe('Descriptions', () => { $80.00 , ); - expect(wrapper.instance().getColumn()).toBe(8); + expect(wrapper.find('td').reduce((total, td) => total + td.props().colSpan, 0)).toBe(8); wrapper.unmount(); }); @@ -158,7 +158,7 @@ describe('Descriptions', () => { , ); - expect(wrapper.find('Col').key()).toBe('label-bamboo'); + expect(wrapper.find('Cell').key()).toBe('item-bamboo'); }); // https://github.com/ant-design/ant-design/issues/19887 @@ -178,4 +178,42 @@ describe('Descriptions', () => { expect(wrapper.render()).toMatchSnapshot(); }); + + // https://github.com/ant-design/ant-design/issues/20255 + it('columns 5 with customize', () => { + const wrapper = mount( + + {/* 1 1 1 1 */} + bamboo + bamboo + bamboo + bamboo + {/* 2 2 */} + + bamboo + + + bamboo + + {/* 3 1 */} + + bamboo + + bamboo + , + ); + + function matchSpan(rowIndex, spans) { + const tr = wrapper.find('tr').at(rowIndex); + const tds = tr.find('th'); + expect(tds).toHaveLength(spans.length); + tds.forEach((td, index) => { + expect(td.props().colSpan).toEqual(spans[index]); + }); + } + + matchSpan(0, [1, 1, 1, 1]); + matchSpan(2, [2, 2]); + matchSpan(4, [3, 1]); + }); }); diff --git a/components/descriptions/index.tsx b/components/descriptions/index.tsx index ffcdfba9a5..7c3b042f7f 100644 --- a/components/descriptions/index.tsx +++ b/components/descriptions/index.tsx @@ -1,39 +1,95 @@ +/* eslint-disable react/no-array-index-key */ import * as React from 'react'; import classNames from 'classnames'; import toArray from 'rc-util/lib/Children/toArray'; -import warning from '../_util/warning'; import ResponsiveObserve, { Breakpoint, ScreenMap, responsiveArray, } from '../_util/responsiveObserve'; -import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; -import Col from './Col'; +import warning from '../_util/warning'; +import { ConfigContext } from '../config-provider'; +import Row from './Row'; +import DescriptionsItem from './Item'; -// https://github.com/smooth-code/react-flatten-children/ -function flattenChildren(children: React.ReactNode): JSX.Element[] { - if (!children) { - return []; +const DEFAULT_COLUMN_MAP: Record = { + xxl: 3, + xl: 3, + lg: 3, + md: 3, + sm: 2, + xs: 1, +}; + +function getColumn(column: DescriptionsProps['column'], screens: ScreenMap): number { + if (typeof column === 'number') { + return column; } - return toArray(children).reduce((flatChildren: JSX.Element[], child: JSX.Element) => { - if (child && child.type === React.Fragment) { - return flatChildren.concat(flattenChildren(child.props.children)); + + if (typeof column === 'object') { + for (let i = 0; i < responsiveArray.length; i++) { + const breakpoint: Breakpoint = responsiveArray[i]; + if (screens[breakpoint] && column[breakpoint] !== undefined) { + return column[breakpoint] || DEFAULT_COLUMN_MAP[breakpoint]; + } } - flatChildren.push(child); - return flatChildren; - }, []); + } + + return 3; } -export interface DescriptionsItemProps { - prefixCls?: string; - className?: string; - label?: React.ReactNode; - children: React.ReactNode; - span?: number; +function getFilledItem( + node: React.ReactElement, + span: number | undefined, + rowRestCol: number, +): React.ReactElement { + let clone = node; + + if (span === undefined || span > rowRestCol) { + clone = React.cloneElement(node, { + span: rowRestCol, + }); + warning( + span === undefined, + 'Descriptions', + 'Sum of column `span` in a line not match `column` of Descriptions.', + ); + } + + return clone; } -const DescriptionsItem: React.SFC = ({ children }) => - children as JSX.Element; +function getRows(children: React.ReactNode, column: number) { + const childNodes = toArray(children).filter(n => n); + const rows: React.ReactElement[][] = []; + + let tmpRow: React.ReactElement[] = []; + let rowRestCol = column; + + childNodes.forEach((node, index) => { + const span: number | undefined = node.props?.span; + const mergedSpan = span || 1; + + // Additional handle last one + if (index === childNodes.length - 1) { + tmpRow.push(getFilledItem(node, span, rowRestCol)); + rows.push(tmpRow); + return; + } + + if (mergedSpan < rowRestCol) { + rowRestCol -= mergedSpan; + tmpRow.push(node); + } else { + tmpRow.push(getFilledItem(node, mergedSpan, rowRestCol)); + rows.push(tmpRow); + rowRestCol = column; + tmpRow = []; + } + }); + + return rows; +} export interface DescriptionsProps { prefixCls?: string; @@ -48,245 +104,73 @@ export interface DescriptionsProps { colon?: boolean; } -/** - * Convert children into `column` groups. - * @param children: DescriptionsItem - * @param column: number - */ -const generateChildrenRows = ( - children: React.ReactNode, - column: number, -): React.ReactElement[][] => { - const rows: React.ReactElement[][] = []; - let columns: React.ReactElement[] | null = null; - let leftSpans: number; +function Descriptions({ + prefixCls: customizePrefixCls, + title, + column = DEFAULT_COLUMN_MAP, + colon = true, + bordered, + layout, + children, + className, + style, + size, +}: DescriptionsProps) { + const { getPrefixCls, direction } = React.useContext(ConfigContext); + const prefixCls = getPrefixCls('descriptions', customizePrefixCls); + const [screens, setScreens] = React.useState({}); + const mergedColumn = getColumn(column, screens); - const itemNodes = flattenChildren(children); - itemNodes.forEach((node: React.ReactElement, index: number) => { - let itemNode = node; - - if (!columns) { - leftSpans = column; - columns = []; - rows.push(columns); - } - - // Always set last span to align the end of Descriptions - const lastItem = index === itemNodes.length - 1; - let lastSpanSame = true; - if (lastItem) { - lastSpanSame = !itemNode.props.span || itemNode.props.span === leftSpans; - itemNode = React.cloneElement(itemNode, { - span: leftSpans, - }); - } - - // Calculate left fill span - const { span = 1 } = itemNode.props; - columns.push(itemNode); - leftSpans -= span; - - if (leftSpans <= 0) { - columns = null; - - warning( - leftSpans === 0 && lastSpanSame, - 'Descriptions', - 'Sum of column `span` in a line not match `column` of Descriptions.', - ); - } - }); - - return rows; -}; - -const renderRow = ( - children: React.ReactElement[], - index: number, - { prefixCls }: { prefixCls: string }, - bordered: boolean, - layout: 'horizontal' | 'vertical', - colon: boolean, -) => { - const renderCol = ( - colItem: React.ReactElement, - type: 'label' | 'content', - idx: number, - ) => { - return ( - - ); - }; - - const cloneChildren: JSX.Element[] = []; - const cloneContentChildren: JSX.Element[] = []; - flattenChildren(children).forEach( - (childrenItem: React.ReactElement, idx: number) => { - cloneChildren.push(renderCol(childrenItem, 'label', idx)); - if (layout === 'vertical') { - cloneContentChildren.push(renderCol(childrenItem, 'content', idx)); - } else if (bordered) { - cloneChildren.push(renderCol(childrenItem, 'content', idx)); - } - }, - ); - - if (layout === 'vertical') { - return [ - - {cloneChildren} - , - - {cloneContentChildren} - , - ]; - } - - return ( - - {cloneChildren} - - ); -}; - -const defaultColumnMap = { - xxl: 3, - xl: 3, - lg: 3, - md: 3, - sm: 2, - xs: 1, -}; - -class Descriptions extends React.Component< - DescriptionsProps, - { - screens: ScreenMap; - } -> { - static defaultProps: DescriptionsProps = { - size: 'default', - column: defaultColumnMap, - }; - - static Item: typeof DescriptionsItem = DescriptionsItem; - - state: { - screens: ScreenMap; - } = { - screens: {}, - }; - - token: string; - - componentDidMount() { - const { column } = this.props; - this.token = ResponsiveObserve.subscribe(screens => { + // Responsive + React.useEffect(() => { + const token = ResponsiveObserve.subscribe(newScreens => { if (typeof column !== 'object') { return; } - this.setState({ - screens, - }); + setScreens(newScreens); }); - } - componentWillUnmount() { - ResponsiveObserve.unsubscribe(this.token); - } + return () => { + ResponsiveObserve.unsubscribe(token); + }; + }, []); - getColumn(): number { - const { column } = this.props; - if (typeof column === 'object') { - for (let i = 0; i < responsiveArray.length; i++) { - const breakpoint: Breakpoint = responsiveArray[i]; - if (this.state.screens[breakpoint] && column[breakpoint] !== undefined) { - return column[breakpoint] || defaultColumnMap[breakpoint]; - } - } - } - // If the configuration is not an object, it is a number, return number - if (typeof column === 'number') { - return column as number; - } - // If it is an object, but no response is found, this happens only in the test. - // Maybe there are some strange environments - return 3; - } + // Children + const rows = getRows(children, mergedColumn); - render() { - return ( - - {({ getPrefixCls, direction }: ConfigConsumerProps) => { - const { - className, - prefixCls: customizePrefixCls, - title, - size, - children, - bordered = false, - layout = 'horizontal', - colon = true, - style, - } = this.props; - const prefixCls = getPrefixCls('descriptions', customizePrefixCls); + return ( +
+ {title &&
{title}
} - const column = this.getColumn(); - const cloneChildren = flattenChildren(children) - .map((child: React.ReactElement) => { - if (React.isValidElement(child)) { - return React.cloneElement(child, { - prefixCls, - }); - } - return null; - }) - .filter((node: React.ReactElement) => node); - - const childrenArray: Array[]> = generateChildrenRows(cloneChildren, column); - return ( -
- {title &&
{title}
} -
- - - {childrenArray.map((child, index) => - renderRow( - child, - index, - { - prefixCls, - }, - bordered, - layout, - colon, - ), - )} - -
-
-
- ); - }} - - ); - } +
+ + + {rows.map((row, index) => ( + + {row} + + ))} + +
+
+
+ ); } +Descriptions.Item = DescriptionsItem; + export default Descriptions; diff --git a/package.json b/package.json index c99f6b07ea..2de2efb6a0 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "rc-tree-select": "~3.0.0-alpha.5", "rc-trigger": "~4.0.0-rc.0", "rc-upload": "~3.0.0-alpha.0", - "rc-util": "^4.17.0", + "rc-util": "^4.20.0", "rc-virtual-list": "^0.0.0-alpha.25", "resize-observer-polyfill": "^1.5.1", "scroll-into-view-if-needed": "^2.2.20",