From 54463c908ba21eed3da6086470c6abdaaed4e305 Mon Sep 17 00:00:00 2001 From: zombieJ Date: Wed, 9 Jan 2019 20:15:37 +0800 Subject: [PATCH 01/18] ConfigProvider support `csp` prop (#14222) close #12438 --- components/_util/wave.tsx | 16 +++++++++++++++- .../config-provider/__tests__/csp.test.js | 17 +++++++++++++++++ components/config-provider/index.en-US.md | 10 ++++++++++ components/config-provider/index.tsx | 9 ++++++++- components/config-provider/index.zh-CN.md | 10 ++++++++++ docs/react/faq.en-US.md | 4 ++++ docs/react/faq.zh-CN.md | 4 ++++ 7 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 components/config-provider/__tests__/csp.test.js diff --git a/components/_util/wave.tsx b/components/_util/wave.tsx index 63d284ba7e..8203385b5a 100644 --- a/components/_util/wave.tsx +++ b/components/_util/wave.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { findDOMNode } from 'react-dom'; import TransitionEvents from 'css-animation/lib/Event'; import raf from '../_util/raf'; +import { ConfigConsumer, ConfigConsumerProps, CSPConfig } from '../config-provider'; let styleForPesudo: HTMLStyleElement | null; @@ -23,6 +24,7 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }> private animationStartId: number; private animationStart: boolean = false; private destroy: boolean = false; + private csp?: CSPConfig; isNotGrey(color: string) { const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/); @@ -53,6 +55,11 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }> !/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color waveColor !== 'transparent' ) { + // Add nonce if CSP exist + if (this.csp && this.csp.nonce) { + styleForPesudo.nonce = this.csp.nonce; + } + extraNode.style.borderColor = waveColor; styleForPesudo.innerHTML = `[ant-click-animating-without-extra-node]:after { border-color: ${waveColor}; }`; if (!document.body.contains(styleForPesudo)) { @@ -169,7 +176,14 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }> this.destroy = true; } + renderWave = ({ csp }: ConfigConsumerProps) => { + const { children } = this.props; + this.csp = csp; + + return children; + }; + render() { - return this.props.children; + return {this.renderWave}; } } diff --git a/components/config-provider/__tests__/csp.test.js b/components/config-provider/__tests__/csp.test.js new file mode 100644 index 0000000000..a5aaa4eae0 --- /dev/null +++ b/components/config-provider/__tests__/csp.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import ConfigProvider from '..'; +import Button from '../../button'; + +describe('ConfigProvider', () => { + it('Content Security Policy', () => { + const csp = { nonce: 'test-antd' }; + const wrapper = mount( + + + +``` + ## API | Property | Description | Type | Default | diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 12108c6ece..73aaebbb88 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -5,11 +5,16 @@ import defaultRenderEmpty, { RenderEmptyHandler } from './renderEmpty'; export { RenderEmptyHandler }; +export interface CSPConfig { + nonce?: string; +} + export interface ConfigConsumerProps { getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement; rootPrefixCls?: string; getPrefixCls: (suffixCls: string, customizePrefixCls?: string) => string; renderEmpty: RenderEmptyHandler; + csp?: CSPConfig; } interface ConfigProviderProps { @@ -17,6 +22,7 @@ interface ConfigProviderProps { prefixCls?: string; children?: React.ReactNode; renderEmpty?: RenderEmptyHandler; + csp?: CSPConfig; } const ConfigContext: Context = createReactContext({ @@ -42,11 +48,12 @@ class ConfigProvider extends React.Component { }; renderProvider = (context: ConfigConsumerProps) => { - const { children, getPopupContainer, renderEmpty } = this.props; + const { children, getPopupContainer, renderEmpty, csp } = this.props; const config: ConfigConsumerProps = { ...context, getPrefixCls: this.getPrefixCls, + csp, }; if (getPopupContainer) { diff --git a/components/config-provider/index.zh-CN.md b/components/config-provider/index.zh-CN.md index a9c9a66f5e..03cdf51b38 100644 --- a/components/config-provider/index.zh-CN.md +++ b/components/config-provider/index.zh-CN.md @@ -24,6 +24,16 @@ return ( ); ``` +### Content Security Policy + +部分组件为了支持波纹效果,使用了动态样式。如果开启了 Content Security Policy (CSP),你可以通过 `csp` 属性来进行配置: + +```jsx + + + +``` + ## API | 参数 | 说明 | 类型 | 默认值 | diff --git a/docs/react/faq.en-US.md b/docs/react/faq.en-US.md index f65755544d..68b3f9042a 100644 --- a/docs/react/faq.en-US.md +++ b/docs/react/faq.en-US.md @@ -106,6 +106,10 @@ After 3.9.x [we are using svg icon](/components/icon#svg-icons), so you don't ne If you need some features which should not be included in antd, try to extend antd's component with [HOC](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775). [more](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.eeu8q01s1) +### How to fix dynamic style when open Content Security Policy (CSP)? + +You can config `nonce` by [ConfigProvider](/components/config-provider/#Content-Security-Policy). + ### How to spell Ant Design correctly? - ✅ **Ant Design**: Capitalized with space, for the design language. diff --git a/docs/react/faq.zh-CN.md b/docs/react/faq.zh-CN.md index a047578613..e7291354d1 100644 --- a/docs/react/faq.zh-CN.md +++ b/docs/react/faq.zh-CN.md @@ -104,6 +104,10 @@ import { Menu, Breadcrumb, Icon } from 'antd'; 如果你需要一些 antd 没有包含的功能,你可以尝试通过 [HOC](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775) 拓展 antd 的组件。 [更多](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.eeu8q01s1) +### 开启了 Content Security Policy (CSP) 如何处理动态样式? + +你可以通过 [ConfigProvider](/components/config-provider/#Content-Security-Policy) 来配置 `nonce` 属性。 + ### 如何正确的拼写 Ant Design? - ✅ **Ant Design**:用空格分隔的首字母大写单词,指代设计语言。 From ff13ac0f5ed0552d226dc8836f1fb809cbe5efba Mon Sep 17 00:00:00 2001 From: zombieJ Date: Wed, 9 Jan 2019 20:38:09 +0800 Subject: [PATCH 02/18] New component Statistic / Countdown (#14154) * init * number format * add Countdown * :zap: Try something ts stuff * move out Countdown * add format * support countdown * add prefix & suffix * Move Number to Statistic * adjust ts * clean up * roll back of lodash * update doc & style * variable of the less style * add demo test * full coverage * hide title if not need * update snapshot * update accessiblity * update color * stop countdown when time is out * formatTimeStr adjust * use reset class * add miss tab index * update doc * update title prop & snapshot * rm additional aria. It's over design * use card sample on unit demo * sfc * adjust demo --- .../__snapshots__/index.test.js.snap | 1 + .../__snapshots__/index.test.js.snap | 60 +++ .../config-provider/__tests__/index.test.js | 4 + components/config-provider/index.tsx | 6 +- components/index.tsx | 2 + components/statistic/Countdown.tsx | 78 ++++ components/statistic/Number.tsx | 55 +++ components/statistic/Statistic.tsx | 64 +++ .../__tests__/__snapshots__/demo.test.js.snap | 388 ++++++++++++++++++ components/statistic/__tests__/demo.test.js | 3 + components/statistic/__tests__/index.test.js | 65 +++ components/statistic/demo/basic.md | 31 ++ components/statistic/demo/card.md | 50 +++ components/statistic/demo/countdown.md | 36 ++ components/statistic/demo/unit.md | 30 ++ components/statistic/index.en-US.md | 38 ++ components/statistic/index.tsx | 6 + components/statistic/index.zh-CN.md | 39 ++ components/statistic/style/index.less | 38 ++ components/statistic/style/index.tsx | 2 + components/statistic/utils.tsx | 60 +++ components/style/themes/default.less | 7 + tests/__snapshots__/index.test.js.snap | 1 + typings/custom-typings.d.ts | 4 + 24 files changed, 1065 insertions(+), 3 deletions(-) create mode 100644 components/statistic/Countdown.tsx create mode 100644 components/statistic/Number.tsx create mode 100644 components/statistic/Statistic.tsx create mode 100644 components/statistic/__tests__/__snapshots__/demo.test.js.snap create mode 100644 components/statistic/__tests__/demo.test.js create mode 100644 components/statistic/__tests__/index.test.js create mode 100644 components/statistic/demo/basic.md create mode 100644 components/statistic/demo/card.md create mode 100644 components/statistic/demo/countdown.md create mode 100644 components/statistic/demo/unit.md create mode 100644 components/statistic/index.en-US.md create mode 100644 components/statistic/index.tsx create mode 100644 components/statistic/index.zh-CN.md create mode 100644 components/statistic/style/index.less create mode 100644 components/statistic/style/index.tsx create mode 100644 components/statistic/utils.tsx diff --git a/components/__tests__/__snapshots__/index.test.js.snap b/components/__tests__/__snapshots__/index.test.js.snap index 6bfaa07821..211cf437e4 100644 --- a/components/__tests__/__snapshots__/index.test.js.snap +++ b/components/__tests__/__snapshots__/index.test.js.snap @@ -35,6 +35,7 @@ Array [ "message", "Menu", "Modal", + "Statistic", "notification", "Pagination", "Popconfirm", diff --git a/components/config-provider/__tests__/__snapshots__/index.test.js.snap b/components/config-provider/__tests__/__snapshots__/index.test.js.snap index 4c52148da1..b0aed66e56 100644 --- a/components/config-provider/__tests__/__snapshots__/index.test.js.snap +++ b/components/config-provider/__tests__/__snapshots__/index.test.js.snap @@ -11043,6 +11043,66 @@ exports[`ConfigProvider components Spin prefixCls 1`] = ` `; +exports[`ConfigProvider components Statistic configProvider 1`] = ` +
+
+ + + 0 + + +
+
+`; + +exports[`ConfigProvider components Statistic normal 1`] = ` +
+
+ + + 0 + + +
+
+`; + +exports[`ConfigProvider components Statistic prefixCls 1`] = ` +
+
+ + + 0 + + +
+
+`; + exports[`ConfigProvider components Steps configProvider 1`] = `
{ // Spin testPair('Spin', props => ); + // Statistic + testPair('Statistic', props => ); + // Steps testPair('Steps', props => { const myProps = { ...props }; diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 73aaebbb88..45d9ec7067 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -87,9 +87,9 @@ interface ConsumerConfig { } export function withConfigConsumer(config: ConsumerConfig) { - return function(Component: IReactComponent): React.SFC { + return function (Component: IReactComponent): React.SFC & ComponentDef { // Wrap with ConfigConsumer. Since we need compatible with react 15, be care when using ref methods - return (props: ExportProps) => ( + return ((props: ExportProps) => ( {(configProps: ConfigConsumerProps) => { const { prefixCls: basicPrefixCls } = config; @@ -99,7 +99,7 @@ export function withConfigConsumer(config: return ; }} - ); + )) as React.SFC & ComponentDef; }; } diff --git a/components/index.tsx b/components/index.tsx index 6b176d30c2..69d5726d1e 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -81,6 +81,8 @@ export { default as Menu } from './menu'; export { default as Modal } from './modal'; +export { default as Statistic } from './statistic'; + export { default as notification } from './notification'; export { default as Pagination } from './pagination'; diff --git a/components/statistic/Countdown.tsx b/components/statistic/Countdown.tsx new file mode 100644 index 0000000000..0c48fc0959 --- /dev/null +++ b/components/statistic/Countdown.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { polyfill } from 'react-lifecycles-compat'; +import * as moment from 'moment'; +import interopDefault from '../_util/interopDefault'; +import Statistic, { StatisticProps } from './Statistic'; +import { formatCountdown, countdownValueType, FormatConfig } from './utils'; + +const REFRESH_INTERVAL = 1000 / 30; + +interface CountdownProps extends StatisticProps { + value?: countdownValueType; + format?: string; +} + +class Countdown extends React.Component { + static defaultProps: Partial = { + format: 'HH:mm:ss', + }; + + countdownId?: number = undefined; + + componentDidMount() { + this.syncTimer(); + } + + componentDidUpdate() { + this.syncTimer(); + } + + componentWillUnmount() { + this.stopTimer(); + } + + syncTimer = () => { + const { value } = this.props; + + const timestamp = interopDefault(moment)(value).valueOf(); + if (timestamp >= Date.now()) { + this.startTimer(); + } else { + this.stopTimer(); + } + }; + + startTimer = () => { + if (this.countdownId !== undefined) return; + + this.countdownId = window.setInterval(() => { + this.forceUpdate(); + }, REFRESH_INTERVAL); + }; + + stopTimer = () => { + clearInterval(this.countdownId); + this.countdownId = undefined; + }; + + formatCountdown = (value: countdownValueType, config: FormatConfig) => { + const { format } = this.props; + return formatCountdown(value, { ...config, format }); + }; + + // Countdown do not need display the timestamp + valueRender = (node: React.ReactElement) => + React.cloneElement(node, { + title: undefined, + }); + + render() { + return ( + + ); + } +} + +polyfill(Countdown); + +export default Countdown; diff --git a/components/statistic/Number.tsx b/components/statistic/Number.tsx new file mode 100644 index 0000000000..2e8c61f0bb --- /dev/null +++ b/components/statistic/Number.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import padEnd from 'lodash/padEnd'; +import { valueType, FormatConfig } from './utils'; + +interface NumberProps extends FormatConfig { + value: valueType; +} + +const StatisticNumber: React.SFC = props => { + const { value, formatter, precision, decimalSeparator, prefixCls } = props; + + let valueNode: React.ReactNode; + + if (typeof formatter === 'function') { + // Customize formatter + valueNode = formatter(value); + } else { + // Internal formatter + const val: string = String(value); + const cells = val.match(/^(\d*)(\.(\d+))?$/); + + // Process if illegal number + if (!cells) { + valueNode = val; + } else { + let int = cells[1] || '0'; + let decimal = cells[3] || ''; + + int = int.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + + if (typeof precision === 'number') { + decimal = padEnd(decimal, precision, '0').slice(0, precision); + } + + if (decimal) { + decimal = `${decimalSeparator}${decimal}`; + } + + valueNode = [ + + {int} + , + decimal && ( + + {decimal} + + ), + ]; + } + } + + return {valueNode}; +}; + +export default StatisticNumber; diff --git a/components/statistic/Statistic.tsx b/components/statistic/Statistic.tsx new file mode 100644 index 0000000000..c425f1e1f8 --- /dev/null +++ b/components/statistic/Statistic.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import classNames from 'classnames'; + +import { withConfigConsumer, ConfigConsumerProps } from '../config-provider'; +import StatisticNumber from './Number'; +import Countdown from './Countdown'; +import { valueType, FormatConfig } from './utils'; + +interface StatisticComponent { + Countdown: typeof Countdown; +} + +export interface StatisticProps extends FormatConfig { + prefixCls?: string; + className?: string; + style?: React.CSSProperties; + value?: valueType; + valueStyle?: React.CSSProperties; + valueRender?: (node: React.ReactNode) => React.ReactNode; + title?: React.ReactNode; + prefix?: React.ReactNode; + suffix?: React.ReactNode; +} + +const Statistic: React.SFC = props => { + const { + prefixCls, + className, + style, + valueStyle, + value = 0, + title, + valueRender, + prefix, + suffix, + } = props; + + let valueNode: React.ReactNode = ; + + if (valueRender) { + valueNode = valueRender(valueNode); + } + + return ( +
+ {title &&
{title}
} +
+ {prefix && {prefix}} + {valueNode} + {suffix && {suffix}} +
+
+ ); +}; + +Statistic.defaultProps = { + decimalSeparator: '.', +}; + +const WrapperStatistic = withConfigConsumer({ + prefixCls: 'statistic', +})(Statistic); + +export default WrapperStatistic; diff --git a/components/statistic/__tests__/__snapshots__/demo.test.js.snap b/components/statistic/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 0000000000..0977607393 --- /dev/null +++ b/components/statistic/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,388 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/statistic/demo/basic.md correctly 1`] = ` +
+
+
+
+ Active Users +
+
+ + + 112,893 + + +
+
+
+
+
+
+ Account Balance (CNY) +
+
+ + + 112,893 + + + .00 + + +
+
+ +
+
+`; + +exports[`renders ./components/statistic/demo/card.md correctly 1`] = ` +
+
+
+
+
+
+
+ Active +
+
+ + + + + + + + 11 + + + .28 + + + + % + +
+
+
+
+
+
+
+
+
+
+ Idle +
+
+ + + + + + + + 9 + + + .30 + + + + % + +
+
+
+
+
+
+
+`; + +exports[`renders ./components/statistic/demo/countdown.md correctly 1`] = ` +
+
+
+
+ Countdown +
+
+ + 48:00:30 + +
+
+
+
+
+
+ Million Seconds +
+
+ + 48:00:30:000 + +
+
+
+
+
+
+ Day Level +
+
+ + 2 天 0 时 0 分 30 秒 + +
+
+
+
+`; + +exports[`renders ./components/statistic/demo/unit.md correctly 1`] = ` +
+
+
+
+ Feedback +
+
+ + + + + + + + 1,128 + + +
+
+
+
+
+
+ Unmerged +
+
+ + + 93 + + + + / 100 + +
+
+
+
+`; diff --git a/components/statistic/__tests__/demo.test.js b/components/statistic/__tests__/demo.test.js new file mode 100644 index 0000000000..2cc48661ee --- /dev/null +++ b/components/statistic/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('statistic'); diff --git a/components/statistic/__tests__/index.test.js b/components/statistic/__tests__/index.test.js new file mode 100644 index 0000000000..e7e5a09f5f --- /dev/null +++ b/components/statistic/__tests__/index.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import MockDate from 'mockdate'; +import moment from 'moment'; +import { mount } from 'enzyme'; +import Statistic from '..'; + +const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout)); + +describe('Statistic', () => { + beforeAll(() => { + MockDate.set(moment('2018-11-28 00:00:00')); + }); + + afterAll(() => { + MockDate.reset(); + }); + + it('customize formatter', () => { + const formatter = jest.fn(() => 93); + const wrapper = mount(); + expect(formatter).toBeCalledWith(1128); + expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('93'); + }); + + it('not a number', () => { + const wrapper = mount(); + expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('bamboo'); + }); + + describe('Countdown', () => { + it('render correctly', () => { + const now = moment() + .add(2, 'd') + .add(11, 'h') + .add(28, 'm') + .add(9, 's') + .add(3, 'ms'); + + [ + ['H:m:s', '59:28:9'], + ['HH:mm:ss', '59:28:09'], + ['HH:mm:ss:SSS', '59:28:09:003'], + ['DD-HH:mm:ss', '02-11:28:09'], + ].forEach(([format, value]) => { + const wrapper = mount(); + expect(wrapper.find('.ant-statistic-content-value').text()).toEqual(value); + }); + }); + + it('time going', async () => { + const now = Date.now() + 1000; + const wrapper = mount(); + wrapper.update(); + + // setInterval should work + const instance = wrapper.instance(); + expect(instance.countdownId).not.toBe(undefined); + + await delay(50); + + wrapper.unmount(); + expect(instance.countdownId).toBe(undefined); + }); + }); +}); diff --git a/components/statistic/demo/basic.md b/components/statistic/demo/basic.md new file mode 100644 index 0000000000..ca1a29e122 --- /dev/null +++ b/components/statistic/demo/basic.md @@ -0,0 +1,31 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +简单的展示。 + +## en-US + +Simplest Usage. + +```jsx +import { Statistic, Row, Col, Button } from 'antd'; + +ReactDOM.render( + + + + + + + + + , + mountNode +); +``` diff --git a/components/statistic/demo/card.md b/components/statistic/demo/card.md new file mode 100644 index 0000000000..66768824d0 --- /dev/null +++ b/components/statistic/demo/card.md @@ -0,0 +1,50 @@ +--- +order: 2 +title: + zh-CN: 在卡片中使用 + en-US: In Card +--- + +## zh-CN + +在卡片中展示统计数值。 + +## en-US + +Display statistic data in Card. + +```jsx +import { Statistic, Card, Row, Col, Icon } from 'antd'; + +ReactDOM.render( +
+ + + + } + suffix="%" + /> + + + + + } + suffix="%" + /> + + + +
, + mountNode +); +``` diff --git a/components/statistic/demo/countdown.md b/components/statistic/demo/countdown.md new file mode 100644 index 0000000000..0bfc1feb12 --- /dev/null +++ b/components/statistic/demo/countdown.md @@ -0,0 +1,36 @@ +--- +order: 3 +title: + zh-CN: 倒计时 + en-US: Countdown +--- + +## zh-CN + +倒计时组件。 + +## en-US + +Countdown component. + +```jsx +import { Statistic, Row, Col } from 'antd'; + +const Countdown = Statistic.Countdown; +const deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30; // Moment is also OK + +ReactDOM.render( + + + + + + + + + + + , + mountNode +); +``` diff --git a/components/statistic/demo/unit.md b/components/statistic/demo/unit.md new file mode 100644 index 0000000000..eb94db72dc --- /dev/null +++ b/components/statistic/demo/unit.md @@ -0,0 +1,30 @@ +--- +order: 1 +title: + zh-CN: 单位 + en-US: Unit +--- + +## zh-CN + +通过前缀和后缀添加单位。 + +## en-US + +Add unit through `prefix` and `suffix`. + +```jsx +import { Statistic, Row, Col, Icon } from 'antd'; + +ReactDOM.render( + + + } /> + + + + + , + mountNode +); +``` diff --git a/components/statistic/index.en-US.md b/components/statistic/index.en-US.md new file mode 100644 index 0000000000..97b41ea7dc --- /dev/null +++ b/components/statistic/index.en-US.md @@ -0,0 +1,38 @@ +--- +category: Components +type: Data Display +title: Statistic +--- + +Display statistic number. + +## When To Use + +* When want to highlight some data. +* When want to display statistic data with description. + +## API + +#### Statistic + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| decimalSeparator | decimal separator | string | - | +| formatter | customize value display logic | (value) => ReactNode | - | +| precision | precision of input value | number | - | +| prefix | prefix node of value | string \| ReactNode | - | +| suffix | suffix node of value | string \| ReactNode | - | +| title | Display title | string \| ReactNode | - | +| value | Display value | string \| number | - | +| valueStyle | Set value css style | style | - | + +#### Statistic.Countdown + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| format | Format as [moment](http://momentjs.com/) | string | 'HH:mm:ss' | +| prefix | prefix node of value | string \| ReactNode | - | +| suffix | suffix node of value | string \| ReactNode | - | +| title | Display title | string \| ReactNode | - | +| value | Set target countdown time | number \| moment | - | +| valueStyle | Set value css style | style | - | diff --git a/components/statistic/index.tsx b/components/statistic/index.tsx new file mode 100644 index 0000000000..3f60608f64 --- /dev/null +++ b/components/statistic/index.tsx @@ -0,0 +1,6 @@ +import Statistic from './Statistic'; +import Countdown from './Countdown'; + +Statistic.Countdown = Countdown; + +export default Statistic; diff --git a/components/statistic/index.zh-CN.md b/components/statistic/index.zh-CN.md new file mode 100644 index 0000000000..2525621cd8 --- /dev/null +++ b/components/statistic/index.zh-CN.md @@ -0,0 +1,39 @@ +--- +category: Components +subtitle: 统计数值 +type: 数据展示 +title: Statistic +--- + +展示统计数值。 + +## 何时使用 + +* 当需要突出某个或某组数字时。 +* 当需要展示带描述的统计类数据时使用。 + +## API + +#### Statistic + +| 参数 | 说明 | 类型 | 默认值 | +| -------- | ----------- | ---- | ------- | +| decimalSeparator | 设置小数点 | string | - | +| formatter | 自定义数值展示 | (value) => ReactNode | - | +| precision | 数值精度 | number | - | +| prefix | 设置数值的前缀 | string \| ReactNode | - | +| suffix | 设置数值的后缀 | string \| ReactNode | - | +| title | 数值的标题 | string \| ReactNode | - | +| value | 数值内容 | string \| number | - | +| valueStyle | 设置数值的样式 | style | - | + +#### Statistic.Countdown + +| 参数 | 说明 | 类型 | 默认值 | +| -------- | ----------- | ---- | ------- | +| format | 格式化倒计时展示,参考 [moment](http://momentjs.com/) | string | 'HH:mm:ss' | +| prefix | 设置数值的前缀 | string \| ReactNode | - | +| suffix | 设置数值的后缀 | string \| ReactNode | - | +| title | 数值的标题 | string \| ReactNode | - | +| value | 数值内容 | number \| moment | - | +| valueStyle | 设置数值的样式 | style | - | diff --git a/components/statistic/style/index.less b/components/statistic/style/index.less new file mode 100644 index 0000000000..541457a852 --- /dev/null +++ b/components/statistic/style/index.less @@ -0,0 +1,38 @@ +@import '../../style/themes/default'; +@import '../../style/mixins/index'; + +@statistic-prefix-cls: ~'@{ant-prefix}-statistic'; + +.@{statistic-prefix-cls} { + .reset-component; + + &-title { + font-size: @statistic-title-font-size; + margin-bottom: 4px; + } + + &-content { + font-size: @statistic-content-font-size; + font-family: @statistic-font-family; + + &-value { + &-decimal { + font-size: @statistic-unit-font-size; + } + } + + &-prefix, + &-suffix { + display: inline-block; + } + + &-prefix { + margin-right: 4px; + } + + &-suffix { + margin-left: 4px; + font-size: @statistic-unit-font-size; + } + } +} diff --git a/components/statistic/style/index.tsx b/components/statistic/style/index.tsx new file mode 100644 index 0000000000..3a3ab0de59 --- /dev/null +++ b/components/statistic/style/index.tsx @@ -0,0 +1,2 @@ +import '../../style/index.less'; +import './index.less'; diff --git a/components/statistic/utils.tsx b/components/statistic/utils.tsx new file mode 100644 index 0000000000..eb0f8d55c4 --- /dev/null +++ b/components/statistic/utils.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import * as moment from 'moment'; +import padStart from 'lodash/padStart'; +import interopDefault from '../_util/interopDefault'; + +export type valueType = number | string; +export type countdownValueType = valueType | string; + +export type Formatter = + | false + | 'number' + | 'countdown' + | ((value: valueType, config?: FormatConfig) => React.ReactNode); + +export interface FormatConfig { + formatter?: Formatter; + decimalSeparator?: string; + precision?: number; + prefixCls?: string; +} + +export interface CountdownFormatConfig extends FormatConfig { + format?: string; +} + +// Countdown +const timeUnits: [string, number][] = [ + ['Y', 1000 * 60 * 60 * 24 * 365], // years + ['M', 1000 * 60 * 60 * 24 * 30], // months + ['D', 1000 * 60 * 60 * 24], // days + ['H', 1000 * 60 * 60], // hours + ['m', 1000 * 60], // minutes + ['s', 1000], // seconds + ['S', 1], // million seconds +]; + +function formatTimeStr(duration: number, format: string) { + let leftDuration: number = duration; + + return timeUnits.reduce((current, [name, unit]) => { + if (current.indexOf(name) !== -1) { + const value = Math.floor(leftDuration / unit); + leftDuration -= value * unit; + return current.replace(new RegExp(`${name}+`, 'g'), (match: string) => { + const len = match.length; + return padStart(value.toString(), len, '0'); + }); + } + return current; + }, format); +} + +export function formatCountdown(value: countdownValueType, config: CountdownFormatConfig) { + const { format = '' } = config; + const target = interopDefault(moment)(value).valueOf(); + const current = interopDefault(moment)().valueOf(); + const diff = Math.max(target - current, 0); + + return formatTimeStr(diff, format); +} diff --git a/components/style/themes/default.less b/components/style/themes/default.less index c06dfb2dd8..bb2ea0b755 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -562,3 +562,10 @@ @list-item-meta-margin-bottom: @padding-md; @list-item-meta-avatar-margin-right: @padding-md; @list-item-meta-title-margin-bottom: @padding-sm; + +// Statistic +// --- +@statistic-title-font-size: @font-size-base; +@statistic-content-font-size: 24px; +@statistic-unit-font-size: 16px; +@statistic-font-family: Tahoma, 'Helvetica Neue', @font-family; diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index b0dc6f3b84..83abea77f6 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -35,6 +35,7 @@ Array [ "message", "Menu", "Modal", + "Statistic", "notification", "Pagination", "Popconfirm", diff --git a/typings/custom-typings.d.ts b/typings/custom-typings.d.ts index 8449ff9c8f..0af56db4d0 100644 --- a/typings/custom-typings.d.ts +++ b/typings/custom-typings.d.ts @@ -93,6 +93,10 @@ declare module '*.json' { declare module 'lodash/debounce'; +declare module 'lodash/padStart'; + +declare module 'lodash/padEnd'; + declare module 'lodash/uniqBy'; declare module "raf"; From 21f0419ace9d256240516f953d33b335068b9706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=B8=E5=B2=B3?= Date: Thu, 10 Jan 2019 01:21:51 +0800 Subject: [PATCH 03/18] feat: Add mask prop for Modal method, close #14177 (#14197) --- components/modal/Modal.tsx | 1 + components/modal/__tests__/confirm.test.js | 5 +++++ components/modal/confirm.tsx | 2 ++ components/modal/index.en-US.md | 1 + components/modal/index.zh-CN.md | 1 + 5 files changed, 10 insertions(+) diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index 484a659171..dffc222496 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -79,6 +79,7 @@ export interface ModalFuncProps { icon?: React.ReactNode; /* Deperated */ iconType?: string; + mask?: boolean; maskClosable?: boolean; zIndex?: number; okCancel?: boolean; diff --git a/components/modal/__tests__/confirm.test.js b/components/modal/__tests__/confirm.test.js index ca12e90d3a..86c9430d1f 100644 --- a/components/modal/__tests__/confirm.test.js +++ b/components/modal/__tests__/confirm.test.js @@ -168,4 +168,9 @@ describe('Modal.confirm triggers callbacks correctly', () => { expect($$('.custom-modal-confirm')).toHaveLength(1); expect($$('.custom-modal-confirm-body-wrapper')).toHaveLength(1); }); + + it('should be Modal.confirm without mask', () => { + open({ mask: false }); + expect($$('.ant-modal-mask')).toHaveLength(0); + }); }); diff --git a/components/modal/confirm.tsx b/components/modal/confirm.tsx index 914308b78c..4fc42b3ca9 100644 --- a/components/modal/confirm.tsx +++ b/components/modal/confirm.tsx @@ -43,6 +43,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => { const okCancel = 'okCancel' in props ? props.okCancel! : true; const width = props.width || 416; const style = props.style || {}; + const mask = props.mask === undefined ? true : props.mask; // 默认为 false,保持旧版默认行为 const maskClosable = props.maskClosable === undefined ? false : props.maskClosable; const runtimeLocale = getConfirmLocale(); @@ -80,6 +81,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => { transitionName="zoom" footer="" maskTransitionName="fade" + mask={mask} maskClosable={maskClosable} maskStyle={maskStyle} style={style} diff --git a/components/modal/index.en-US.md b/components/modal/index.en-US.md index ad97b9e868..d60d0a1096 100644 --- a/components/modal/index.en-US.md +++ b/components/modal/index.en-US.md @@ -71,6 +71,7 @@ The properties of the object are follows: | icon | custom icon (`Added in 3.12.0`) | string\|ReactNode | `` | | iconType | Icon `type` of the Icon component (deperated after `3.12.0`) | string | `question-circle` | | keyboard | Whether support press esc to close | Boolean | true | +| mask | Whether show mask or not. | Boolean | true | | maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | Boolean | `false` | | okText | Text of the OK button | string | `OK` | | okType | Button `type` of the OK button | string | `primary` | diff --git a/components/modal/index.zh-CN.md b/components/modal/index.zh-CN.md index ad64c41a3e..69993b8dd3 100644 --- a/components/modal/index.zh-CN.md +++ b/components/modal/index.zh-CN.md @@ -69,6 +69,7 @@ title: Modal | content | 内容 | string\|ReactNode | 无 | | icon | 自定义图标(3.12.0 新增) | string\|ReactNode | `` | | iconType | 图标类型(3.12.0 后废弃,请使用 `icon`) | string | `question-circle` | +| mask | 是否展示遮罩 | Boolean | true | | maskClosable | 点击蒙层是否允许关闭 | Boolean | `false` | | okText | 确认按钮文字 | string | 确定 | | okType | 确认按钮类型 | string | primary | From f524b693ed0537ebbfc9719f47b228f8a5e1b077 Mon Sep 17 00:00:00 2001 From: zombieJ Date: Thu, 10 Jan 2019 11:15:43 +0800 Subject: [PATCH 04/18] Button support `round` shape (#14236) close #13700 --- .../__tests__/__snapshots__/demo.test.js.snap | 25 +++++++++++++++++++ components/button/button.tsx | 2 +- components/button/demo/size.md | 1 + components/button/index.en-US.md | 2 +- components/button/index.zh-CN.md | 2 +- components/button/style/index.less | 4 +++ components/button/style/mixin.less | 14 +++++++++++ 7 files changed, 47 insertions(+), 3 deletions(-) diff --git a/components/button/__tests__/__snapshots__/demo.test.js.snap b/components/button/__tests__/__snapshots__/demo.test.js.snap index 87461309a7..798be4a5e6 100644 --- a/components/button/__tests__/__snapshots__/demo.test.js.snap +++ b/components/button/__tests__/__snapshots__/demo.test.js.snap @@ -882,6 +882,31 @@ exports[`renders ./components/button/demo/size.md correctly 1`] = ` +

diff --git a/components/button/index.en-US.md b/components/button/index.en-US.md index 115ca2375e..40ed480660 100644 --- a/components/button/index.en-US.md +++ b/components/button/index.en-US.md @@ -22,7 +22,7 @@ To get a customized button, just set `type`/`shape`/`size`/`loading`/`disabled`. | htmlType | set the original html `type` of `button`, see: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type) | string | `button` | | icon | set the icon of button, see: Icon component | string | - | | loading | set the loading status of button | boolean \| { delay: number } | false | -| shape | can be set to `circle` or omitted | string | - | +| shape | can be set to `circle`, `round` or omitted | string | - | | size | can be set to `small` `large` or omitted | string | `default` | | target | same as target attribute of a, works when href is specified | string | - | | type | can be set to `primary` `ghost` `dashed` `danger`(added in 2.7) or omitted (meaning `default`) | string | `default` | diff --git a/components/button/index.zh-CN.md b/components/button/index.zh-CN.md index a425cfe07b..bb772ea78d 100644 --- a/components/button/index.zh-CN.md +++ b/components/button/index.zh-CN.md @@ -25,7 +25,7 @@ subtitle: 按钮 | htmlType | 设置 `button` 原生的 `type` 值,可选值请参考 [HTML 标准](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type) | string | `button` | | icon | 设置按钮的图标类型 | string | - | | loading | 设置按钮载入状态 | boolean \| { delay: number } | `false` | -| shape | 设置按钮形状,可选值为 `circle` 或者不设 | string | - | +| shape | 设置按钮形状,可选值为 `circle`、 `round` 或者不设 | string | - | | size | 设置按钮大小,可选值为 `small` `large` 或者不设 | string | `default` | | target | 相当于 a 链接的 target 属性,href 存在时生效 | string | - | | type | 设置按钮类型,可选值为 `primary` `dashed` `danger`(版本 2.7 中增加) 或者不设 | string | - | diff --git a/components/button/style/index.less b/components/button/style/index.less index 0f150d8d91..06724615da 100644 --- a/components/button/style/index.less +++ b/components/button/style/index.less @@ -69,6 +69,10 @@ .btn-danger; } + &-round { + .btn-round(@btn-prefix-cls); + } + &-circle, &-circle-outline { .btn-circle(@btn-prefix-cls); diff --git a/components/button/style/mixin.less b/components/button/style/mixin.less index 3eee51e936..b9adfb8b2c 100644 --- a/components/button/style/mixin.less +++ b/components/button/style/mixin.less @@ -220,6 +220,20 @@ .btn-danger() { .button-variant-danger(@btn-danger-color, @btn-danger-bg, @btn-danger-border); } +// round button +.btn-round(@btnClassName: btn) { + .button-size(@btn-circle-size; 0 @btn-circle-size / 2; @font-size-base + 2px; @btn-circle-size); + &.@{btnClassName}-lg { + .button-size( + @btn-circle-size-lg; 0 @btn-circle-size-lg / 2; @btn-font-size-lg + 2px; @btn-circle-size-lg + ); + } + &.@{btnClassName}-sm { + .button-size( + @btn-circle-size-sm; 0 @btn-circle-size-sm / 2; @font-size-base; @btn-circle-size-sm + ); + } +} // circle button: the content only contains icon .btn-circle(@btnClassName: btn) { .square(@btn-circle-size); From 4e54aa8a46ee732a9d2f950a4301cd6534b2ef79 Mon Sep 17 00:00:00 2001 From: zombieJ Date: Thu, 10 Jan 2019 11:47:11 +0800 Subject: [PATCH 05/18] ConfigProvider support `autoInsertSpaceInButton` (#14230) close #14229 --- components/button/button.tsx | 9 +- components/button/index.en-US.md | 6 + components/button/index.zh-CN.md | 7 + ...x.test.js.snap => components.test.js.snap} | 0 .../__tests__/components.test.js | 559 +++++++++++++++++ .../config-provider/__tests__/csp.test.js | 17 - .../config-provider/__tests__/index.test.js | 572 +----------------- components/config-provider/index.en-US.md | 1 + components/config-provider/index.tsx | 5 +- components/config-provider/index.zh-CN.md | 1 + 10 files changed, 604 insertions(+), 573 deletions(-) rename components/config-provider/__tests__/__snapshots__/{index.test.js.snap => components.test.js.snap} (100%) create mode 100644 components/config-provider/__tests__/components.test.js delete mode 100644 components/config-provider/__tests__/csp.test.js diff --git a/components/button/button.tsx b/components/button/button.tsx index fcc49d653b..15998222e8 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -191,7 +191,7 @@ class Button extends React.Component { return React.Children.count(children) === 1 && !icon; } - renderButton = ({ getPrefixCls }: ConfigConsumerProps) => { + renderButton = ({ getPrefixCls, autoInsertSpaceInButton }: ConfigConsumerProps) => { const { prefixCls: customizePrefixCls, type, @@ -208,6 +208,7 @@ class Button extends React.Component { const { loading, hasTwoCNChar } = this.state; const prefixCls = getPrefixCls('btn', customizePrefixCls); + const autoInsertSpace = autoInsertSpaceInButton !== false; // large => lg // small => sm @@ -229,7 +230,7 @@ class Button extends React.Component { [`${prefixCls}-icon-only`]: !children && children !== 0 && icon, [`${prefixCls}-loading`]: loading, [`${prefixCls}-background-ghost`]: ghost, - [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar, + [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace, [`${prefixCls}-block`]: block, }); @@ -237,7 +238,9 @@ class Button extends React.Component { const iconNode = iconType ? : null; const kids = children || children === 0 - ? React.Children.map(children, child => insertSpace(child, this.isNeedInserted())) + ? React.Children.map(children, child => + insertSpace(child, this.isNeedInserted() && autoInsertSpace), + ) : null; const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType']); diff --git a/components/button/index.en-US.md b/components/button/index.en-US.md index 40ed480660..efadc924aa 100644 --- a/components/button/index.en-US.md +++ b/components/button/index.en-US.md @@ -35,6 +35,12 @@ It accepts all props which native button support. `` will be rendered into `Hello world!`. +## FAQ + +### How to remove space between 2 chinese characters + +Use [ConfigProvider](/components/config-provider/#API) to set `autoInsertSpaceInButton` as `false`. +