New component Descriptions (#14645)

* add new component: DescriptionList

* add warning message

* docs: fix doc typo

* feat: implement the size attribute

* docs: fix doc typo

* refactor: use new name Descriptions

* test: snapshots updated

* feat: support react15

* style: fix code style warring

* style: better var name

* style: better code style

* style: merge css class

* feat: add responsive config

* fix: fix error title

* style: use @border-radius-base

* update snapshot

* feat: set default column

* test: add test script

* style: fix property defaultProps is useless error

* style: more robust code

* style: fix codereview warning

* style: fix review warning

* use responsiveObserveserve

* fix review warning

* bug: add childrenArray copy,prevent changes to incoming parameters

* fix dom error

* fix typo

* fix test

* don't use this

* snapshot updated

* prettier md

* remove descriptions md text

* new rendering method

* doc :add dot

* style: add right border
This commit is contained in:
陈帅 2019-05-22 23:22:09 +08:00 committed by GitHub
parent 633fd7142d
commit 3e32364dc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1587 additions and 83 deletions

View File

@ -21,6 +21,7 @@ Array [
"Comment",
"ConfigProvider",
"DatePicker",
"Descriptions",
"Divider",
"Dropdown",
"Drawer",

View File

@ -0,0 +1,101 @@
// matchMedia polyfill for
// https://github.com/WickyNilliams/enquire.js/issues/82
let enquire: any;
if (typeof window !== 'undefined') {
const matchMediaPolyfill = (mediaQuery: string) => {
return {
media: mediaQuery,
matches: false,
addListener() {},
removeListener() {},
};
};
window.matchMedia = window.matchMedia || matchMediaPolyfill;
enquire = require('enquire.js');
}
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Partial<Record<Breakpoint, string>>;
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
export const responsiveMap: BreakpointMap = {
xs: '(max-width: 575px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
xl: '(min-width: 1200px)',
xxl: '(min-width: 1600px)',
};
type SubscribeFunc = (screens: BreakpointMap) => void;
let subscribers: Array<{
token: string;
func: SubscribeFunc;
}> = [];
let subUid = -1;
let screens = {};
const responsiveObserve = {
dispatch(pointMap: BreakpointMap) {
screens = pointMap;
if (subscribers.length < 1) {
return false;
}
subscribers.forEach(item => {
item.func(screens);
});
return true;
},
subscribe(func: SubscribeFunc) {
if (subscribers.length === 0) {
this.register();
}
const token = (++subUid).toString();
subscribers.push({
token: token,
func: func,
});
func(screens);
return token;
},
unsubscribe(token: string) {
subscribers = subscribers.filter(item => item.token !== token);
if (subscribers.length === 0) {
this.unregister();
}
},
unregister() {
Object.keys(responsiveMap).map((screen: Breakpoint) =>
enquire.unregister(responsiveMap[screen]),
);
},
register() {
Object.keys(responsiveMap).map((screen: Breakpoint) =>
enquire.register(responsiveMap[screen], {
match: () => {
const pointMap = {
...screens,
[screen]: true,
};
this.dispatch(pointMap);
},
unmatch: () => {
const pointMap = {
...screens,
[screen]: false,
};
this.dispatch(pointMap);
},
// Keep a empty destory to avoid triggering unmatch when unregister
destroy() {},
}),
);
},
};
export default responsiveObserve;

View File

@ -0,0 +1,611 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/descriptions/demo/basic.md correctly 1`] = `
<div
class="ant-descriptions"
>
<div
class="ant-descriptions-title"
>
User Info
</div>
<div
class="ant-descriptions-view"
>
<table>
<tbody>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
UserName
</span>
<span
class="ant-descriptions-item-content"
>
Zhou Maomao
</span>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Telephone
</span>
<span
class="ant-descriptions-item-content"
>
1810000000
</span>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Live
</span>
<span
class="ant-descriptions-item-content"
>
Hangzhou, Zhejiang
</span>
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Remark
</span>
<span
class="ant-descriptions-item-content"
>
empty
</span>
</td>
<td
class="ant-descriptions-item"
colspan="2"
>
<span
class="ant-descriptions-item-label"
>
Address
</span>
<span
class="ant-descriptions-item-content"
>
No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
exports[`renders ./components/descriptions/demo/border.md correctly 1`] = `
<div
class="ant-descriptions bordered"
>
<div
class="ant-descriptions-title"
>
User Info
</div>
<div
class="ant-descriptions-view"
>
<table>
<tbody>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Product
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
Cloud Database
</td>
<td
class="ant-descriptions-item-label"
>
Billing Mode
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
Prepaid
</td>
<td
class="ant-descriptions-item-label"
>
Automatic Renewal
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
YES
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Order time
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
2018-04-24 18:00:00
</td>
<td
class="ant-descriptions-item-label"
>
Usage Time
</td>
<td
class="ant-descriptions-item-content"
colspan="5"
>
2019-04-24 18:00:00
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Status
</td>
<td
class="ant-descriptions-item-content"
colspan="5"
>
<span
class="ant-badge ant-badge-status ant-badge-not-a-wrapper"
>
<span
class="ant-badge-status-dot ant-badge-status-processing"
/>
<span
class="ant-badge-status-text"
>
Running
</span>
</span>
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Negotiated Amount
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
$80.00
</td>
<td
class="ant-descriptions-item-label"
>
Discount
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
$20.00
</td>
<td
class="ant-descriptions-item-label"
>
Official Receipts
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
$60.00
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Config Info
</td>
<td
class="ant-descriptions-item-content"
colspan="5"
>
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication_factor:3
<br />
Region: East China 1
<br />
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = `
<div>
<div
class="ant-descriptions"
>
<div
class="ant-descriptions-title"
>
Responsive Descriptions
</div>
<div
class="ant-descriptions-view"
>
<table>
<tbody>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Product
</span>
<span
class="ant-descriptions-item-content"
>
Cloud Database
</span>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Billing
</span>
<span
class="ant-descriptions-item-content"
>
Prepaid
</span>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
time
</span>
<span
class="ant-descriptions-item-content"
>
18:00:00
</span>
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Amount
</span>
<span
class="ant-descriptions-item-content"
>
$80.00
</span>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Discount
</span>
<span
class="ant-descriptions-item-content"
>
$20.00
</span>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<span
class="ant-descriptions-item-label"
>
Official
</span>
<span
class="ant-descriptions-item-content"
>
$60.00
</span>
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item"
colspan="3"
>
<span
class="ant-descriptions-item-label"
>
Config Info
</span>
<span
class="ant-descriptions-item-content"
>
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication_factor:3
<br />
Region: East China 1
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
exports[`renders ./components/descriptions/demo/size.md correctly 1`] = `
<div>
<div
class="ant-radio-group ant-radio-group-outline"
>
<label
class="ant-radio-wrapper ant-radio-wrapper-checked"
>
<span
class="ant-radio ant-radio-checked"
>
<input
checked=""
class="ant-radio-input"
type="radio"
value="default"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
default
</span>
</label>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="middle"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
middle
</span>
</label>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="small"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
small
</span>
</label>
</div>
<br />
<br />
<div
class="ant-descriptions bordered"
>
<div
class="ant-descriptions-title"
>
Custom Size
</div>
<div
class="ant-descriptions-view"
>
<table>
<tbody>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Product
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
Cloud Database
</td>
<td
class="ant-descriptions-item-label"
>
Billing
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
Prepaid
</td>
<td
class="ant-descriptions-item-label"
>
time
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
18:00:00
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Amount
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
$80.00
</td>
<td
class="ant-descriptions-item-label"
>
Discount
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
$20.00
</td>
<td
class="ant-descriptions-item-label"
>
Official
</td>
<td
class="ant-descriptions-item-content"
colspan="1"
>
$60.00
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item-label"
>
Config Info
</td>
<td
class="ant-descriptions-item-content"
colspan="5"
>
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication_factor:3
<br />
Region: East China 1
<br />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Descriptions column is number 1`] = `
<Descriptions
column="3"
size="default"
>
<div
className="ant-descriptions"
>
<div
className="ant-descriptions-view"
>
<table>
<tbody>
<tr
className="ant-descriptions-row"
key="0"
>
<td
className="ant-descriptions-item"
colSpan={1}
key=".$.0"
>
<span
className="ant-descriptions-item-label"
key="label"
>
Product
</span>
<span
className="ant-descriptions-item-content"
key="content"
>
Cloud Database
</span>
</td>
<td
className="ant-descriptions-item"
colSpan={1}
key=".$.1"
>
<span
className="ant-descriptions-item-label"
key="label"
>
Billing
</span>
<span
className="ant-descriptions-item-content"
key="content"
>
Prepaid
</span>
</td>
<td
className="ant-descriptions-item"
colSpan={1}
>
<span
className="ant-descriptions-item-label"
key="label"
>
time
</span>
<span
className="ant-descriptions-item-content"
key="content"
>
18:00:00
</span>
</td>
</tr>
<tr
className="ant-descriptions-row"
key="1"
>
<td
className="ant-descriptions-item"
colSpan={3}
>
<span
className="ant-descriptions-item-label"
key="label"
>
Amount
</span>
<span
className="ant-descriptions-item-content"
key="content"
>
$80.00
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</Descriptions>
`;

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('descriptions');

View File

@ -0,0 +1,86 @@
import React from 'react';
import { mount } from 'enzyme';
import Descriptions from '..';
const DescriptionsItem = Descriptions.Item;
jest.mock('enquire.js', () => {
let that;
let unmatchFun;
return {
unregister: jest.fn(),
register: (media, options) => {
if (media === '(max-width: 575px)') {
that = this;
options.match.call(that);
unmatchFun = options.unmatch;
}
},
callunmatch() {
unmatchFun.call(that);
},
};
});
describe('Descriptions', () => {
it('when max-width: 575pxcolumn=1', () => {
// eslint-disable-next-line global-require
const enquire = require('enquire.js');
const wrapper = mount(
<Descriptions>
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
<DescriptionsItem label="time">18:00:00</DescriptionsItem>
<DescriptionsItem label="Amount">$80.00</DescriptionsItem>
</Descriptions>,
);
expect(wrapper.find('tr')).toHaveLength(4);
enquire.callunmatch();
wrapper.unmount();
});
it('when max-width: 575pxcolumn=2', () => {
// eslint-disable-next-line global-require
const enquire = require('enquire.js');
const wrapper = mount(
<Descriptions column={{ xs: 2 }}>
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
<DescriptionsItem label="time">18:00:00</DescriptionsItem>
<DescriptionsItem label="Amount">$80.00</DescriptionsItem>
</Descriptions>,
);
expect(wrapper.find('tr')).toHaveLength(2);
enquire.callunmatch();
wrapper.unmount();
});
it('column is number', () => {
// eslint-disable-next-line global-require
const wrapper = mount(
<Descriptions column="3">
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
<DescriptionsItem label="time">18:00:00</DescriptionsItem>
<DescriptionsItem label="Amount">$80.00</DescriptionsItem>
</Descriptions>,
);
expect(wrapper).toMatchSnapshot();
wrapper.unmount();
});
it('when typeof column is object', () => {
const wrapper = mount(
<Descriptions column={{ xs: 8, sm: 16, md: 24 }}>
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
<DescriptionsItem label="time">18:00:00</DescriptionsItem>
<DescriptionsItem label="Amount">$80.00</DescriptionsItem>
</Descriptions>,
);
expect(wrapper.instance().getColumn()).toBe(8);
wrapper.unmount();
});
});

View File

@ -0,0 +1,33 @@
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---
## zh-CN
简单的展示。
## en-US
Simplest Usage.
```jsx
import { Descriptions } from 'antd';
const DescriptionsItem = Descriptions.Item;
ReactDOM.render(
<Descriptions title="User Info">
<DescriptionsItem label="UserName">Zhou Maomao</DescriptionsItem>
<DescriptionsItem label="Telephone">1810000000</DescriptionsItem>
<DescriptionsItem label="Live">Hangzhou, Zhejiang</DescriptionsItem>
<DescriptionsItem label="Remark">empty</DescriptionsItem>
<DescriptionsItem label="Address">
No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
</DescriptionsItem>
</Descriptions>,
mountNode,
);
```

View File

@ -0,0 +1,52 @@
---
order: 1
title:
zh-CN: 带边框的
en-US: border
---
## zh-CN
带边框和背景颜色列表。
## en-US
Descriptions with border and background color.
```jsx
import { Descriptions, Badge } from 'antd';
const DescriptionsItem = Descriptions.Item;
ReactDOM.render(
<Descriptions title="User Info" bordered>
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing Mode">Prepaid</DescriptionsItem>
<DescriptionsItem label="Automatic Renewal">YES</DescriptionsItem>
<DescriptionsItem label="Order time">2018-04-24 18:00:00</DescriptionsItem>
<DescriptionsItem label="Usage Time" span={3}>
2019-04-24 18:00:00
</DescriptionsItem>
<DescriptionsItem label="Status" span={3}>
<Badge status="processing" text="Running" />
</DescriptionsItem>
<DescriptionsItem label="Negotiated Amount">$80.00</DescriptionsItem>
<DescriptionsItem label="Discount">$20.00</DescriptionsItem>
<DescriptionsItem label="Official Receipts">$60.00</DescriptionsItem>
<DescriptionsItem label="Config Info">
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication_factor:3
<br />
Region: East China 1<br />
</DescriptionsItem>
</Descriptions>,
mountNode,
);
```

View File

@ -0,0 +1,54 @@
---
order: 3
title:
zh-CN: 响应式
en-US: responsive
---
## zh-CN
通过响应式的配置可以实现在小屏幕设备上的完美呈现。
## en-US
Responsive configuration enables perfect presentation on small screen devices.
```jsx
import { Descriptions } from 'antd';
const DescriptionsItem = Descriptions.Item;
const Demo = () => {
return (
<div>
<Descriptions
title="Responsive Descriptions"
border
column={{ xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 }}
>
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
<DescriptionsItem label="time">18:00:00</DescriptionsItem>
<DescriptionsItem label="Amount">$80.00</DescriptionsItem>
<DescriptionsItem label="Discount">$20.00</DescriptionsItem>
<DescriptionsItem label="Official">$60.00</DescriptionsItem>
<DescriptionsItem label="Config Info">
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication_factor:3
<br />
Region: East China 1
</DescriptionsItem>
</Descriptions>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,72 @@
---
order: 2
title:
zh-CN: 自定义尺寸
en-US: Custom size
---
## zh-CN
自定义尺寸,适应在各种容器中展示。
## en-US
Custom sizes to fit in a variety of containers.
```jsx
import { Descriptions, Radio } from 'antd';
const RadioGroup = Radio.Group;
const DescriptionsItem = Descriptions.Item;
class Demo extends React.Component {
state = {
size: 'default',
};
onChange = e => {
console.log('size checked', e.target.value);
this.setState({
size: e.target.value,
});
};
render() {
return (
<div>
<RadioGroup onChange={this.onChange} value={this.state.size}>
<Radio value="default">default</Radio>
<Radio value="middle">middle</Radio>
<Radio value="small">small</Radio>
</RadioGroup>
<br />
<br />
<Descriptions bordered title="Custom Size" border size={this.state.size}>
<DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
<DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
<DescriptionsItem label="time">18:00:00</DescriptionsItem>
<DescriptionsItem label="Amount">$80.00</DescriptionsItem>
<DescriptionsItem label="Discount">$20.00</DescriptionsItem>
<DescriptionsItem label="Official">$60.00</DescriptionsItem>
<DescriptionsItem label="Config Info">
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication_factor:3
<br />
Region: East China 1<br />
</DescriptionsItem>
</Descriptions>
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,30 @@
---
category: Components
type: Data Display
title: Description List
cols: 1
---
Display multiple read-only fields in groups.
## When To Use
Commonly displayed on the details page.
## API
### Descriptions
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| title | The title of the description list, placed at the top | ReactNode | - |
| bordered | whether to display the border | boolean | false |
| column | the number of `DescriptionItems` in a row,could be a number or a object like `{ xs: 8, sm: 16, md: 24}`,(Only set `bordered={true}` to take effect) | number | 3 |
| size | set the size of the list. Can be set to `middle`,`small`, or not filled | `default | middle | small` | false |
### DescriptionItem
| Property | Description | Type | Default |
| -------- | ------------------------------ | --------- | ------- |
| label | description of the content | ReactNode | - |
| span | The number of columns included | number | 1 |

View File

@ -0,0 +1,255 @@
import * as React from 'react';
import classNames from 'classnames';
import warning from '../_util/warning';
import ResponsiveObserve, {
Breakpoint,
BreakpointMap,
responsiveArray,
} from '../_util/responsiveObserve';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
export interface DescriptionsItemProps {
prefixCls?: string;
label: React.ReactNode;
children: JSX.Element;
span?: number;
}
const DescriptionsItem: React.SFC<DescriptionsItemProps> = ({ children }) => children;
export interface DescriptionsProps {
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
bordered?: boolean;
size?: 'middle' | 'small' | 'default';
children?: React.ReactNode;
title?: string;
column?: number | Partial<Record<Breakpoint, number>>;
}
/**
* Convert children into `column` groups.
* @param cloneChildren: DescriptionsItem
* @param column: number
*/
const generateChildrenRows = (
cloneChildren: React.ReactNode,
column: number,
): React.ReactElement<DescriptionsItemProps>[][] => {
const childrenArray: React.ReactElement<DescriptionsItemProps>[][] = [];
let columnArray: React.ReactElement<DescriptionsItemProps>[] = [];
let totalRowSpan = 0;
React.Children.forEach(cloneChildren, (node: React.ReactElement<DescriptionsItemProps>) => {
columnArray.push(node);
if (node.props.span) {
totalRowSpan += node.props.span;
} else {
totalRowSpan += 1;
}
if (totalRowSpan >= column) {
childrenArray.push(columnArray);
columnArray = [];
totalRowSpan = 0;
warning(
totalRowSpan > column,
'Descriptions',
'Sum of column `span` in a line exceeds `column` of Descriptions.',
);
}
});
if (columnArray.length > 0) {
childrenArray.push(columnArray);
columnArray = [];
}
return childrenArray;
};
/**
* This code is for handling react15 does not support returning an array,
* It can convert a children into two td
* @param child DescriptionsItem
* @returns
* <>
* <td>{DescriptionsItem.label}</td>
* <td>{DescriptionsItem.children}</td>
* </>
*/
const renderCol = (child: React.ReactElement<DescriptionsItemProps>, bordered: boolean) => {
const { prefixCls, label, children, span = 1 } = child.props;
if (bordered) {
return [
<td className={`${prefixCls}-item-label`} key="label">
{label}
</td>,
<td className={`${prefixCls}-item-content`} key="content" colSpan={span * 2 - 1}>
{children}
</td>,
];
}
return (
<td colSpan={span} className={`${prefixCls}-item`}>
<span className={`${prefixCls}-item-label`} key="label">
{label}
</span>
<span className={`${prefixCls}-item-content`} key="content">
{children}
</span>
</td>
);
};
const renderRow = (
children: React.ReactElement<DescriptionsItemProps>[],
index: number,
{ prefixCls, column, isLast }: { prefixCls: string; column: number; isLast: boolean },
bordered: boolean,
) => {
// copy children,prevent changes to incoming parameters
const childrenArray = [...children];
let lastChildren = childrenArray.pop() as React.ReactElement<DescriptionsItemProps>;
const span = column - childrenArray.length;
if (isLast) {
lastChildren = React.cloneElement(lastChildren as React.ReactElement<DescriptionsItemProps>, {
span,
});
}
const cloneChildren = React.Children.map(
childrenArray,
(childrenItem: React.ReactElement<DescriptionsItemProps>) => {
return renderCol(childrenItem, bordered);
},
);
return (
<tr className={`${prefixCls}-row`} key={index}>
{cloneChildren}
{renderCol(lastChildren, bordered)}
</tr>
);
};
const defaultColumnMap = {
xxl: 3,
xl: 3,
lg: 3,
md: 3,
sm: 2,
xs: 1,
};
class Descriptions extends React.Component<
DescriptionsProps,
{
screens: BreakpointMap;
}
> {
static defaultProps: DescriptionsProps = {
size: 'default',
column: defaultColumnMap,
};
static Item: typeof DescriptionsItem;
state: {
screens: BreakpointMap;
} = {
screens: {},
};
token: string;
componentDidMount() {
const { column } = this.props;
this.token = ResponsiveObserve.subscribe(screens => {
if (typeof column !== 'object') {
return;
}
this.setState({
screens,
});
});
}
componentWillUnmount() {
ResponsiveObserve.unsubscribe(this.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;
}
render() {
return (
<ConfigConsumer>
{({ getPrefixCls }: ConfigConsumerProps) => {
const {
className,
prefixCls: customizePrefixCls,
title,
size,
children,
bordered = false,
} = this.props;
const prefixCls = getPrefixCls('descriptions', customizePrefixCls);
const column = this.getColumn();
const cloneChildren = React.Children.map(
children,
(child: React.ReactElement<DescriptionsItemProps>) => {
return React.cloneElement(child, {
prefixCls,
});
},
);
const childrenArray: Array<
React.ReactElement<DescriptionsItemProps>[]
> = generateChildrenRows(cloneChildren, column);
return (
<div
className={classNames(prefixCls, className, {
[size as string]: size !== 'default',
bordered,
})}
>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
<div className={`${prefixCls}-view`}>
<table>
<tbody>
{childrenArray.map((child, index) =>
renderRow(
child,
index,
{
prefixCls,
column,
isLast: index + 1 === childrenArray.length,
},
bordered,
),
)}
</tbody>
</table>
</div>
</div>
);
}}
</ConfigConsumer>
);
}
}
Descriptions.Item = DescriptionsItem;
export default Descriptions;

View File

@ -0,0 +1,31 @@
---
category: Components
subtitle: 描述列表
type: 数据展示
title: Descriptions
cols: 1
---
成组展示多个只读字段。
## 何时使用
常见于详情页的信息展示。
## API
### Descriptions
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| title | 描述列表的标题,显示在最顶部 | ReactNode | - |
| bordered | 是否展示边框 | boolean | false |
| column | 一行的 `DescriptionItems` 数量,可以写成像素值或支持响应式的对象写法 `{ xs: 8, sm: 16, md: 24}` | number | 3 |
| size | 设置列表的大小。可以设置为 `middle` 、`small`, 或不填(只有设置 `bordered={true}` 生效) | `default | middle | small` | false |
### DescriptionItem
| 参数 | 说明 | 类型 | 默认值 |
| ----- | ------------ | --------- | ------ |
| label | 内容的描述 | ReactNode | - |
| span | 包含列的数量 | number | 1 |

View File

@ -0,0 +1,118 @@
@import '../../style/themes/default';
@import '../../style/mixins/index';
@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions';
@descriptions-default-padding: 16px 24px;
@descriptions-middle-padding: 12px 24px;
@descriptions-small-padding: 8px 16px;
.@{descriptions-prefix-cls} {
&-title {
margin-bottom: 20px;
color: @heading-color;
font-weight: bold;
font-size: @font-size-lg;
line-height: @line-height-base;
}
&-view {
width: 100%;
overflow: hidden;
border-radius: @border-radius-base;
table {
width: 100%;
}
}
&-row {
> td {
padding-bottom: 16px;
}
&:last-child {
border-bottom: none;
}
}
&-item-label {
color: @heading-color;
font-size: @font-size-base;
line-height: @line-height-base;
white-space: nowrap;
&::after {
position: relative;
top: -0.5px;
margin: 0 8px 0 2px;
content: ':';
}
}
&-item-content {
display: table-cell;
color: @text-color;
font-size: @font-size-base;
line-height: @line-height-base;
}
&-item {
padding-bottom: 0;
> span {
display: inline-block;
}
.@{descriptions-prefix-cls}-item-label {
float: left;
padding: 0 !important;
}
.@{descriptions-prefix-cls}-item-content {
float: left;
padding: 0 !important;
}
}
// padding setting
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
padding: @descriptions-default-padding;
}
&.bordered.middle {
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
padding: @descriptions-middle-padding;
}
}
&.bordered.small {
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
padding: @descriptions-small-padding;
}
}
&.bordered {
.@{descriptions-prefix-cls}-view {
border: 1px solid @border-color-split;
}
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
border-right: 1px solid @border-color-split;
}
.@{descriptions-prefix-cls}-item-label:last-child,
.@{descriptions-prefix-cls}-item-content:last-child {
border-right: none;
}
.@{descriptions-prefix-cls}-row {
border-bottom: 1px solid @border-color-split;
&:last-child {
border-bottom: none;
}
}
.@{descriptions-prefix-cls}-item-label {
background-color: #fafafa;
&::after {
display: none;
}
}
}
}

View File

@ -0,0 +1,2 @@
import '../../style/index.less';
import './index.less';

View File

@ -7,8 +7,8 @@ jest.mock('enquire.js', () => {
let unmatchFun;
return {
unregister: jest.fn(),
register: (meidia, options) => {
if (meidia === '(max-width: 575px)') {
register: (media, options) => {
if (media === '(max-width: 575px)') {
that = this;
options.match.call(that);
unmatchFun = options.unmatch;
@ -31,23 +31,10 @@ describe('Grid', () => {
expect(wrapper).toMatchSnapshot();
});
it('should work correct when gutter is object', () => {
// eslint-disable-next-line global-require
const enquire = require('enquire.js');
const wrapper = mount(<Row gutter={{ xs: 20 }} />);
expect(wrapper.find('div').prop('style')).toEqual({
marginLeft: -10,
marginRight: -10,
});
enquire.callunmatch();
expect(
wrapper
.update()
.find('div')
.prop('style'),
).toEqual(undefined);
it('when typeof getGutter is object', () => {
const wrapper = mount(<Row gutter={{ xs: 8, sm: 16, md: 24 }} />);
expect(wrapper.instance().getGutter()).toBe(8);
wrapper.unmount();
expect(enquire.unregister).toHaveBeenCalledTimes(6);
});
it('renders wrapped Col correctly', () => {
@ -70,8 +57,22 @@ describe('Grid', () => {
expect(willUnmount).toHaveBeenCalled();
});
it('when typeof getGutter is object', () => {
const wrapper = mount(<Row gutter={{ xs: 8, sm: 16, md: 24 }} />).instance();
expect(wrapper.getGutter()).toBe(8);
it('should work correct when gutter is object', () => {
// eslint-disable-next-line global-require
const enquire = require('enquire.js');
const wrapper = mount(<Row gutter={{ xs: 20 }} />);
expect(wrapper.find('div').prop('style')).toEqual({
marginLeft: -10,
marginRight: -10,
});
enquire.callunmatch();
expect(
wrapper
.update()
.find('div')
.prop('style'),
).toEqual(undefined);
wrapper.unmount();
expect(enquire.unregister).toHaveBeenCalled();
});
});

View File

@ -1,29 +1,15 @@
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
// matchMedia polyfill for
// https://github.com/WickyNilliams/enquire.js/issues/82
let enquire: any;
if (typeof window !== 'undefined') {
const matchMediaPolyfill = (mediaQuery: string) => {
return {
media: mediaQuery,
matches: false,
addListener() {},
removeListener() {},
};
};
window.matchMedia = window.matchMedia || matchMediaPolyfill;
enquire = require('enquire.js');
}
import * as React from 'react';
import classNames from 'classnames';
import * as PropTypes from 'prop-types';
import RowContext from './RowContext';
import { tuple } from '../_util/type';
import ResponsiveObserve, {
Breakpoint,
BreakpointMap,
responsiveArray,
} from '../_util/responsiveObserve';
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Partial<Record<Breakpoint, string>>;
const RowAligns = tuple('top', 'middle', 'bottom');
const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between');
export interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
@ -38,17 +24,6 @@ export interface RowState {
screens: BreakpointMap;
}
const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
const responsiveMap: BreakpointMap = {
xs: '(max-width: 575px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
xl: '(min-width: 1200px)',
xxl: '(min-width: 1600px)',
};
export default class Row extends React.Component<RowProps, RowState> {
static defaultProps = {
gutter: 0,
@ -67,41 +42,16 @@ export default class Row extends React.Component<RowProps, RowState> {
state: RowState = {
screens: {},
};
token: string;
componentDidMount() {
Object.keys(responsiveMap).map((screen: Breakpoint) =>
enquire.register(responsiveMap[screen], {
match: () => {
if (typeof this.props.gutter !== 'object') {
return;
}
this.setState(prevState => ({
screens: {
...prevState.screens,
[screen]: true,
},
}));
},
unmatch: () => {
if (typeof this.props.gutter !== 'object') {
return;
}
this.setState(prevState => ({
screens: {
...prevState.screens,
[screen]: false,
},
}));
},
// Keep a empty destory to avoid triggering unmatch when unregister
destroy() {},
}),
);
this.token = ResponsiveObserve.subscribe(screens => {
if (typeof this.props.gutter === 'object') {
this.setState({ screens });
}
});
}
componentWillUnmount() {
Object.keys(responsiveMap).map((screen: Breakpoint) =>
enquire.unregister(responsiveMap[screen]),
);
ResponsiveObserve.unsubscribe(this.token);
}
getGutter(): number | undefined {
const { gutter } = this.props;

View File

@ -53,6 +53,8 @@ export { default as ConfigProvider } from './config-provider';
export { default as DatePicker } from './date-picker';
export { default as Descriptions } from './descriptions';
export { default as Divider } from './divider';
export { default as Dropdown } from './dropdown';

View File

@ -21,6 +21,7 @@ Array [
"Comment",
"ConfigProvider",
"DatePicker",
"Descriptions",
"Divider",
"Dropdown",
"Drawer",