diff --git a/components/__tests__/__snapshots__/index.test.js.snap b/components/__tests__/__snapshots__/index.test.js.snap index eb455ba167..079920cab0 100644 --- a/components/__tests__/__snapshots__/index.test.js.snap +++ b/components/__tests__/__snapshots__/index.test.js.snap @@ -21,6 +21,7 @@ Array [ "Comment", "ConfigProvider", "DatePicker", + "Descriptions", "Divider", "Dropdown", "Drawer", diff --git a/components/_util/responsiveObserve.ts b/components/_util/responsiveObserve.ts new file mode 100644 index 0000000000..6ee73cddd2 --- /dev/null +++ b/components/_util/responsiveObserve.ts @@ -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>; + +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; diff --git a/components/descriptions/__tests__/__snapshots__/demo.test.js.snap b/components/descriptions/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 0000000000..0eeb89fdfa --- /dev/null +++ b/components/descriptions/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,611 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/descriptions/demo/basic.md correctly 1`] = ` +
+
+ User Info +
+
+ + + + + + + + + + + + +
+ + UserName + + + Zhou Maomao + + + + Telephone + + + 1810000000 + + + + Live + + + Hangzhou, Zhejiang + +
+ + Remark + + + empty + + + + Address + + + No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China + +
+
+
+`; + +exports[`renders ./components/descriptions/demo/border.md correctly 1`] = ` +
+
+ User Info +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Product + + Cloud Database + + Billing Mode + + Prepaid + + Automatic Renewal + + YES +
+ Order time + + 2018-04-24 18:00:00 + + Usage Time + + 2019-04-24 18:00:00 +
+ Status + + + + + Running + + +
+ Negotiated Amount + + $80.00 + + Discount + + $20.00 + + Official Receipts + + $60.00 +
+ Config Info + + Data disk type: MongoDB +
+ Database version: 3.4 +
+ Package: dds.mongo.mid +
+ Storage space: 10 GB +
+ Replication_factor:3 +
+ Region: East China 1 +
+
+
+
+`; + +exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = ` +
+
+
+ Responsive Descriptions +
+
+ + + + + + + + + + + + + + + + +
+ + Product + + + Cloud Database + + + + Billing + + + Prepaid + + + + time + + + 18:00:00 + +
+ + Amount + + + $80.00 + + + + Discount + + + $20.00 + + + + Official + + + $60.00 + +
+ + Config Info + + + Data disk type: MongoDB +
+ Database version: 3.4 +
+ Package: dds.mongo.mid +
+ Storage space: 10 GB +
+ Replication_factor:3 +
+ Region: East China 1 +
+
+
+
+
+`; + +exports[`renders ./components/descriptions/demo/size.md correctly 1`] = ` +
+
+ + + +
+
+
+
+
+ Custom Size +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ Product + + Cloud Database + + Billing + + Prepaid + + time + + 18:00:00 +
+ Amount + + $80.00 + + Discount + + $20.00 + + Official + + $60.00 +
+ Config Info + + Data disk type: MongoDB +
+ Database version: 3.4 +
+ Package: dds.mongo.mid +
+ Storage space: 10 GB +
+ Replication_factor:3 +
+ Region: East China 1 +
+
+
+
+
+`; diff --git a/components/descriptions/__tests__/__snapshots__/index.test.js.snap b/components/descriptions/__tests__/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..1d3ad5bcfc --- /dev/null +++ b/components/descriptions/__tests__/__snapshots__/index.test.js.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Descriptions column is number 1`] = ` + +
+
+ + + + + + + + + + + +
+ + Product + + + Cloud Database + + + + Billing + + + Prepaid + + + + time + + + 18:00:00 + +
+ + Amount + + + $80.00 + +
+
+
+
+`; diff --git a/components/descriptions/__tests__/demo.test.js b/components/descriptions/__tests__/demo.test.js new file mode 100644 index 0000000000..966d453bea --- /dev/null +++ b/components/descriptions/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('descriptions'); diff --git a/components/descriptions/__tests__/index.test.js b/components/descriptions/__tests__/index.test.js new file mode 100644 index 0000000000..2c0752d96f --- /dev/null +++ b/components/descriptions/__tests__/index.test.js @@ -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( + + Cloud Database + Prepaid + 18:00:00 + $80.00 + , + ); + 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( + + Cloud Database + Prepaid + 18:00:00 + $80.00 + , + ); + expect(wrapper.find('tr')).toHaveLength(2); + + enquire.callunmatch(); + wrapper.unmount(); + }); + + it('column is number', () => { + // eslint-disable-next-line global-require + const wrapper = mount( + + Cloud Database + Prepaid + 18:00:00 + $80.00 + , + ); + expect(wrapper).toMatchSnapshot(); + wrapper.unmount(); + }); + + it('when typeof column is object', () => { + const wrapper = mount( + + Cloud Database + Prepaid + 18:00:00 + $80.00 + , + ); + expect(wrapper.instance().getColumn()).toBe(8); + wrapper.unmount(); + }); +}); diff --git a/components/descriptions/demo/basic.md b/components/descriptions/demo/basic.md new file mode 100644 index 0000000000..94b76a31cd --- /dev/null +++ b/components/descriptions/demo/basic.md @@ -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( + + Zhou Maomao + 1810000000 + Hangzhou, Zhejiang + empty + + No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China + + , + mountNode, +); +``` diff --git a/components/descriptions/demo/border.md b/components/descriptions/demo/border.md new file mode 100644 index 0000000000..3879270669 --- /dev/null +++ b/components/descriptions/demo/border.md @@ -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( + + Cloud Database + Prepaid + YES + 2018-04-24 18:00:00 + + 2019-04-24 18:00:00 + + + + + $80.00 + $20.00 + $60.00 + + Data disk type: MongoDB +
+ Database version: 3.4 +
+ Package: dds.mongo.mid +
+ Storage space: 10 GB +
+ Replication_factor:3 +
+ Region: East China 1
+
+
, + mountNode, +); +``` diff --git a/components/descriptions/demo/responsive.md b/components/descriptions/demo/responsive.md new file mode 100644 index 0000000000..3f4283e872 --- /dev/null +++ b/components/descriptions/demo/responsive.md @@ -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 ( +
+ + Cloud Database + Prepaid + 18:00:00 + $80.00 + $20.00 + $60.00 + + Data disk type: MongoDB +
+ Database version: 3.4 +
+ Package: dds.mongo.mid +
+ Storage space: 10 GB +
+ Replication_factor:3 +
+ Region: East China 1 +
+
+
+ ); +}; + +ReactDOM.render(, mountNode); +``` diff --git a/components/descriptions/demo/size.md b/components/descriptions/demo/size.md new file mode 100644 index 0000000000..124ad19d84 --- /dev/null +++ b/components/descriptions/demo/size.md @@ -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 ( +
+ + default + middle + small + +
+
+ + Cloud Database + Prepaid + 18:00:00 + $80.00 + $20.00 + $60.00 + + Data disk type: MongoDB +
+ Database version: 3.4 +
+ Package: dds.mongo.mid +
+ Storage space: 10 GB +
+ Replication_factor:3 +
+ Region: East China 1
+
+
+
+ ); + } +} + +ReactDOM.render(, mountNode); +``` diff --git a/components/descriptions/index.en-US.md b/components/descriptions/index.en-US.md new file mode 100644 index 0000000000..cd500afd8d --- /dev/null +++ b/components/descriptions/index.en-US.md @@ -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 | diff --git a/components/descriptions/index.tsx b/components/descriptions/index.tsx new file mode 100644 index 0000000000..35c87bc731 --- /dev/null +++ b/components/descriptions/index.tsx @@ -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 = ({ 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>; +} + +/** + * Convert children into `column` groups. + * @param cloneChildren: DescriptionsItem + * @param column: number + */ +const generateChildrenRows = ( + cloneChildren: React.ReactNode, + column: number, +): React.ReactElement[][] => { + const childrenArray: React.ReactElement[][] = []; + let columnArray: React.ReactElement[] = []; + let totalRowSpan = 0; + React.Children.forEach(cloneChildren, (node: React.ReactElement) => { + 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 + * <> + * {DescriptionsItem.label} + * {DescriptionsItem.children} + * + */ +const renderCol = (child: React.ReactElement, bordered: boolean) => { + const { prefixCls, label, children, span = 1 } = child.props; + if (bordered) { + return [ + + {label} + , + + {children} + , + ]; + } + return ( + + + {label} + + + {children} + + + ); +}; + +const renderRow = ( + children: React.ReactElement[], + 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; + const span = column - childrenArray.length; + if (isLast) { + lastChildren = React.cloneElement(lastChildren as React.ReactElement, { + span, + }); + } + const cloneChildren = React.Children.map( + childrenArray, + (childrenItem: React.ReactElement) => { + return renderCol(childrenItem, bordered); + }, + ); + return ( + + {cloneChildren} + {renderCol(lastChildren, bordered)} + + ); +}; + +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 ( + + {({ 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) => { + return React.cloneElement(child, { + prefixCls, + }); + }, + ); + + const childrenArray: Array< + React.ReactElement[] + > = generateChildrenRows(cloneChildren, column); + return ( +
+ {title &&
{title}
} +
+ + + {childrenArray.map((child, index) => + renderRow( + child, + index, + { + prefixCls, + column, + isLast: index + 1 === childrenArray.length, + }, + bordered, + ), + )} + +
+
+
+ ); + }} +
+ ); + } +} + +Descriptions.Item = DescriptionsItem; + +export default Descriptions; diff --git a/components/descriptions/index.zh-CN.md b/components/descriptions/index.zh-CN.md new file mode 100644 index 0000000000..ba8c034907 --- /dev/null +++ b/components/descriptions/index.zh-CN.md @@ -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 | diff --git a/components/descriptions/style/index.less b/components/descriptions/style/index.less new file mode 100644 index 0000000000..fcc29e539b --- /dev/null +++ b/components/descriptions/style/index.less @@ -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; + } + } + } +} diff --git a/components/descriptions/style/index.tsx b/components/descriptions/style/index.tsx new file mode 100644 index 0000000000..3a3ab0de59 --- /dev/null +++ b/components/descriptions/style/index.tsx @@ -0,0 +1,2 @@ +import '../../style/index.less'; +import './index.less'; diff --git a/components/grid/__tests__/index.test.js b/components/grid/__tests__/index.test.js index f0d42d6b67..3aeadcf53d 100644 --- a/components/grid/__tests__/index.test.js +++ b/components/grid/__tests__/index.test.js @@ -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(); - 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(); + 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().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(); + 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(); }); }); diff --git a/components/grid/row.tsx b/components/grid/row.tsx index d1c341416c..b6932b6a54 100644 --- a/components/grid/row.tsx +++ b/components/grid/row.tsx @@ -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>; const RowAligns = tuple('top', 'middle', 'bottom'); const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between'); export interface RowProps extends React.HTMLAttributes { @@ -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 { static defaultProps = { gutter: 0, @@ -67,41 +42,16 @@ export default class Row extends React.Component { 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; diff --git a/components/index.tsx b/components/index.tsx index 5e7c24b188..4130c5c7fb 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -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'; diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index e3e57e3501..f3ea00c029 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -21,6 +21,7 @@ Array [ "Comment", "ConfigProvider", "DatePicker", + "Descriptions", "Divider", "Dropdown", "Drawer",