mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 17:44:35 +08:00
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:
parent
633fd7142d
commit
3e32364dc0
@ -21,6 +21,7 @@ Array [
|
|||||||
"Comment",
|
"Comment",
|
||||||
"ConfigProvider",
|
"ConfigProvider",
|
||||||
"DatePicker",
|
"DatePicker",
|
||||||
|
"Descriptions",
|
||||||
"Divider",
|
"Divider",
|
||||||
"Dropdown",
|
"Dropdown",
|
||||||
"Drawer",
|
"Drawer",
|
||||||
|
101
components/_util/responsiveObserve.ts
Normal file
101
components/_util/responsiveObserve.ts
Normal 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;
|
@ -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>
|
||||||
|
`;
|
@ -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>
|
||||||
|
`;
|
3
components/descriptions/__tests__/demo.test.js
Normal file
3
components/descriptions/__tests__/demo.test.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import demoTest from '../../../tests/shared/demoTest';
|
||||||
|
|
||||||
|
demoTest('descriptions');
|
86
components/descriptions/__tests__/index.test.js
Normal file
86
components/descriptions/__tests__/index.test.js
Normal 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: 575px,column=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: 575px,column=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();
|
||||||
|
});
|
||||||
|
});
|
33
components/descriptions/demo/basic.md
Normal file
33
components/descriptions/demo/basic.md
Normal 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,
|
||||||
|
);
|
||||||
|
```
|
52
components/descriptions/demo/border.md
Normal file
52
components/descriptions/demo/border.md
Normal 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,
|
||||||
|
);
|
||||||
|
```
|
54
components/descriptions/demo/responsive.md
Normal file
54
components/descriptions/demo/responsive.md
Normal 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);
|
||||||
|
```
|
72
components/descriptions/demo/size.md
Normal file
72
components/descriptions/demo/size.md
Normal 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);
|
||||||
|
```
|
30
components/descriptions/index.en-US.md
Normal file
30
components/descriptions/index.en-US.md
Normal 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 |
|
255
components/descriptions/index.tsx
Normal file
255
components/descriptions/index.tsx
Normal 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;
|
31
components/descriptions/index.zh-CN.md
Normal file
31
components/descriptions/index.zh-CN.md
Normal 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 |
|
118
components/descriptions/style/index.less
Normal file
118
components/descriptions/style/index.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
components/descriptions/style/index.tsx
Normal file
2
components/descriptions/style/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import '../../style/index.less';
|
||||||
|
import './index.less';
|
@ -7,8 +7,8 @@ jest.mock('enquire.js', () => {
|
|||||||
let unmatchFun;
|
let unmatchFun;
|
||||||
return {
|
return {
|
||||||
unregister: jest.fn(),
|
unregister: jest.fn(),
|
||||||
register: (meidia, options) => {
|
register: (media, options) => {
|
||||||
if (meidia === '(max-width: 575px)') {
|
if (media === '(max-width: 575px)') {
|
||||||
that = this;
|
that = this;
|
||||||
options.match.call(that);
|
options.match.call(that);
|
||||||
unmatchFun = options.unmatch;
|
unmatchFun = options.unmatch;
|
||||||
@ -31,23 +31,10 @@ describe('Grid', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work correct when gutter is object', () => {
|
it('when typeof getGutter is object', () => {
|
||||||
// eslint-disable-next-line global-require
|
const wrapper = mount(<Row gutter={{ xs: 8, sm: 16, md: 24 }} />);
|
||||||
const enquire = require('enquire.js');
|
expect(wrapper.instance().getGutter()).toBe(8);
|
||||||
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();
|
wrapper.unmount();
|
||||||
expect(enquire.unregister).toHaveBeenCalledTimes(6);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders wrapped Col correctly', () => {
|
it('renders wrapped Col correctly', () => {
|
||||||
@ -70,8 +57,22 @@ describe('Grid', () => {
|
|||||||
expect(willUnmount).toHaveBeenCalled();
|
expect(willUnmount).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when typeof getGutter is object', () => {
|
it('should work correct when gutter is object', () => {
|
||||||
const wrapper = mount(<Row gutter={{ xs: 8, sm: 16, md: 24 }} />).instance();
|
// eslint-disable-next-line global-require
|
||||||
expect(wrapper.getGutter()).toBe(8);
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,29 +1,15 @@
|
|||||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
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 * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as PropTypes from 'prop-types';
|
import * as PropTypes from 'prop-types';
|
||||||
import RowContext from './RowContext';
|
import RowContext from './RowContext';
|
||||||
import { tuple } from '../_util/type';
|
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 RowAligns = tuple('top', 'middle', 'bottom');
|
||||||
const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between');
|
const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between');
|
||||||
export interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@ -38,17 +24,6 @@ export interface RowState {
|
|||||||
screens: BreakpointMap;
|
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> {
|
export default class Row extends React.Component<RowProps, RowState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
gutter: 0,
|
gutter: 0,
|
||||||
@ -67,41 +42,16 @@ export default class Row extends React.Component<RowProps, RowState> {
|
|||||||
state: RowState = {
|
state: RowState = {
|
||||||
screens: {},
|
screens: {},
|
||||||
};
|
};
|
||||||
|
token: string;
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
Object.keys(responsiveMap).map((screen: Breakpoint) =>
|
this.token = ResponsiveObserve.subscribe(screens => {
|
||||||
enquire.register(responsiveMap[screen], {
|
if (typeof this.props.gutter === 'object') {
|
||||||
match: () => {
|
this.setState({ screens });
|
||||||
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() {},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
Object.keys(responsiveMap).map((screen: Breakpoint) =>
|
ResponsiveObserve.unsubscribe(this.token);
|
||||||
enquire.unregister(responsiveMap[screen]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
getGutter(): number | undefined {
|
getGutter(): number | undefined {
|
||||||
const { gutter } = this.props;
|
const { gutter } = this.props;
|
||||||
|
@ -53,6 +53,8 @@ export { default as ConfigProvider } from './config-provider';
|
|||||||
|
|
||||||
export { default as DatePicker } from './date-picker';
|
export { default as DatePicker } from './date-picker';
|
||||||
|
|
||||||
|
export { default as Descriptions } from './descriptions';
|
||||||
|
|
||||||
export { default as Divider } from './divider';
|
export { default as Divider } from './divider';
|
||||||
|
|
||||||
export { default as Dropdown } from './dropdown';
|
export { default as Dropdown } from './dropdown';
|
||||||
|
@ -21,6 +21,7 @@ Array [
|
|||||||
"Comment",
|
"Comment",
|
||||||
"ConfigProvider",
|
"ConfigProvider",
|
||||||
"DatePicker",
|
"DatePicker",
|
||||||
|
"Descriptions",
|
||||||
"Divider",
|
"Divider",
|
||||||
"Dropdown",
|
"Dropdown",
|
||||||
"Drawer",
|
"Drawer",
|
||||||
|
Loading…
Reference in New Issue
Block a user