Browse Source

Merge pull request #18324 from ant-design/master

chore: Merge master into feature
pull/18327/head
偏右 5 years ago
committed by GitHub
parent
commit
8345554cd7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      .github/main.workflow
  2. 8
      components/__tests__/util/domHook.js
  3. 53
      components/_util/__tests__/util.test.js
  4. 38
      components/_util/resizeObserver.tsx
  5. 7
      components/_util/warning.ts
  6. 14
      components/_util/warning.tsx
  7. 24
      components/affix/__tests__/Affix.test.js
  8. 8
      components/badge/ScrollNumber.tsx
  9. 6
      components/breadcrumb/Breadcrumb.tsx
  10. 3
      components/breadcrumb/style/index.less
  11. 68
      components/cascader/__tests__/__snapshots__/index.test.js.snap
  12. 12
      components/cascader/__tests__/index.test.js
  13. 6
      components/cascader/index.tsx
  14. 5
      components/menu/SubMenu.tsx
  15. 1
      components/menu/index.en-US.md
  16. 15
      components/menu/index.zh-CN.md
  17. 2
      components/popconfirm/index.en-US.md
  18. 19
      components/progress/style/index.less
  19. 1
      components/tag/CheckableTag.tsx
  20. 6
      components/tooltip/index.tsx
  21. 2
      components/upload/utils.tsx
  22. 6
      package.json

16
.github/main.workflow

@ -4,11 +4,13 @@ workflow "Deploy website" {
}
action "Deploy" {
uses = "docker://node:10"
runs = [
"sh",
"-c",
"git config user.name antd-actions-bot && git config --local user.email support+actions@github.com && git remote set-url origin https://${DEPLOY_TOKEN}@github.com/ant-design/ant-design.git && npm install && npm run deploy"
],
secrets = ["DEPLOY_TOKEN"]
uses = "JamesIves/github-pages-deploy-action@master"
secrets = [
"ACCESS_TOKEN",
]
env = {
BUILD_SCRIPT = "npm install && npm run predeploy"
BRANCH = "gh-pages"
FOLDER = "_site"
}
}

8
components/__tests__/util/domHook.js

@ -1,10 +1,12 @@
const __NULL__ = { notExist: true };
export function spyElementPrototypes(Element, properties) {
const propNames = Object.keys(properties);
const originDescriptors = {};
propNames.forEach(propName => {
const originDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, propName);
originDescriptors[propName] = originDescriptor;
originDescriptors[propName] = originDescriptor || __NULL__;
const spyProp = properties[propName];
@ -37,7 +39,9 @@ export function spyElementPrototypes(Element, properties) {
mockRestore() {
propNames.forEach(propName => {
const originDescriptor = originDescriptors[propName];
if (typeof originDescriptor === 'function') {
if (originDescriptor === __NULL__) {
delete Element.prototype[propName];
} else if (typeof originDescriptor === 'function') {
Element.prototype[propName] = originDescriptor;
} else {
Object.defineProperty(Element.prototype, propName, originDescriptor);

53
components/_util/__tests__/util.test.js

@ -9,6 +9,8 @@ import triggerEvent from '../triggerEvent';
import Wave from '../wave';
import TransButton from '../transButton';
import openAnimation from '../openAnimation';
import ResizeObserver from '../resizeObserver';
import { spyElementPrototype } from '../../__tests__/util/domHook';
describe('Test utils function', () => {
beforeAll(() => {
@ -143,7 +145,9 @@ describe('Test utils function', () => {
it('bindAnimationEvent should return when node is null', () => {
const wrapper = mount(
<Wave>
<button type="button" disabled>button</button>
<button type="button" disabled>
button
</button>
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
@ -152,7 +156,9 @@ describe('Test utils function', () => {
it('bindAnimationEvent.onClick should return when children is hidden', () => {
const wrapper = mount(
<Wave>
<button type="button" style={{ display: 'none' }}>button</button>
<button type="button" style={{ display: 'none' }}>
button
</button>
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
@ -220,4 +226,47 @@ describe('Test utils function', () => {
expect(done).toHaveBeenCalled();
});
});
describe('ResizeObserver', () => {
let domMock;
beforeAll(() => {
domMock = spyElementPrototype(HTMLDivElement, 'getBoundingClientRect', () => {
return {
width: 1128 + Math.random(),
height: 903 + Math.random(),
};
});
});
afterAll(() => {
domMock.mockRestore();
});
it('should not trigger `onResize` if size shaking', () => {
const onResize = jest.fn();
let divNode;
const wrapper = mount(
<ResizeObserver onResize={onResize}>
<div
ref={node => {
divNode = node;
}}
/>
</ResizeObserver>,
);
// First trigger
wrapper.instance().onResize([{ target: divNode }]);
onResize.mockReset();
// Repeat trigger should not trigger outer `onResize` with shaking
for (let i = 0; i < 10; i += 1) {
wrapper.instance().onResize([{ target: divNode }]);
}
expect(onResize).not.toHaveBeenCalled();
});
});
});

38
components/_util/resizeObserver.tsx

@ -10,9 +10,19 @@ interface ResizeObserverProps {
onResize?: () => void;
}
class ReactResizeObserver extends React.Component<ResizeObserverProps, {}> {
interface ResizeObserverState {
height: number;
width: number;
}
class ReactResizeObserver extends React.Component<ResizeObserverProps, ResizeObserverState> {
resizeObserver: ResizeObserver | null = null;
state = {
width: 0,
height: 0,
};
componentDidMount() {
this.onComponentUpdated();
}
@ -38,10 +48,30 @@ class ReactResizeObserver extends React.Component<ResizeObserverProps, {}> {
}
}
onResize = () => {
onResize: ResizeObserverCallback = (entries: ResizeObserverEntry[]) => {
const { onResize } = this.props;
if (onResize) {
onResize();
const { target } = entries[0];
const { width, height } = target.getBoundingClientRect();
/**
* Resize observer trigger when content size changed.
* In most case we just care about element size,
* let's use `boundary` instead of `contentRect` here to avoid shaking.
*/
const fixedWidth = Math.floor(width);
const fixedHeight = Math.floor(height);
if (this.state.width !== fixedWidth || this.state.height !== fixedHeight) {
this.setState({
width: fixedWidth,
height: fixedHeight,
});
if (onResize) {
onResize();
}
}
};

7
components/_util/warning.ts

@ -0,0 +1,7 @@
import warning, { resetWarned } from 'rc-util/lib/warning';
export { resetWarned };
export default (valid: boolean, component: string, message: string): void => {
warning(valid, `[antd: ${component}] ${message}`);
};

14
components/_util/warning.tsx

@ -1,14 +0,0 @@
import warning from 'warning';
let warned: Record<string, boolean> = {};
export function resetWarned() {
warned = {};
}
export default (valid: boolean, component: string, message: string): void => {
if (!valid && !warned[message]) {
warning(false, `[antd: ${component}] ${message}`);
warned[message] = true;
}
};

24
components/affix/__tests__/Affix.test.js

@ -3,6 +3,7 @@ import { mount } from 'enzyme';
import Affix from '..';
import { getObserverEntities } from '../utils';
import Button from '../../button';
import { spyElementPrototype } from '../../__tests__/util/domHook';
const events = {};
@ -40,6 +41,7 @@ class AffixMounter extends React.Component {
describe('Affix Render', () => {
let wrapper;
let domMock;
const classRect = {
container: {
@ -48,23 +50,21 @@ describe('Affix Render', () => {
},
};
const originGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
return (
classRect[this.className] || {
top: 0,
bottom: 0,
}
);
};
beforeAll(() => {
jest.useFakeTimers();
domMock = spyElementPrototype(HTMLElement, 'getBoundingClientRect', function mockBounding() {
return (
classRect[this.className] || {
top: 0,
bottom: 0,
}
);
});
});
afterAll(() => {
jest.useRealTimers();
HTMLElement.prototype.getBoundingClientRect = originGetBoundingClientRect;
domMock.mockRestore();
});
const movePlaceholder = top => {
classRect.fixed = {
@ -185,7 +185,7 @@ describe('Affix Render', () => {
.find('ReactResizeObserver')
.at(index)
.instance()
.onResize();
.onResize([{ target: { getBoundingClientRect: () => ({ width: 99, height: 99 }) } }]);
jest.runAllTimers();
expect(updateCalled).toHaveBeenCalled();

8
components/badge/ScrollNumber.tsx

@ -1,4 +1,4 @@
import React, { createElement, Component } from 'react';
import * as React from 'react';
import omit from 'omit.js';
import classNames from 'classnames';
import { polyfill } from 'react-lifecycles-compat';
@ -47,7 +47,7 @@ export interface ScrollNumberState {
count?: string | number | null;
}
class ScrollNumber extends Component<ScrollNumberProps, ScrollNumberState> {
class ScrollNumber extends React.Component<ScrollNumberProps, ScrollNumberState> {
static defaultProps = {
count: null,
onAnimated() {},
@ -126,7 +126,7 @@ class ScrollNumber extends Component<ScrollNumberProps, ScrollNumberState> {
const position = this.getPositionByNum(num, i);
const removeTransition =
this.state.animateStarted || getNumberArray(this.lastCount)[i] === undefined;
return createElement(
return React.createElement(
'span',
{
className: `${prefixCls}-only`,
@ -200,7 +200,7 @@ class ScrollNumber extends Component<ScrollNumberProps, ScrollNumberState> {
),
});
}
return createElement(component as any, newProps, this.renderNumberElement(prefixCls));
return React.createElement(component as any, newProps, this.renderNumberElement(prefixCls));
};
render() {

6
components/breadcrumb/Breadcrumb.tsx

@ -1,4 +1,4 @@
import React, { cloneElement } from 'react';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
import BreadcrumbItem from './BreadcrumbItem';
@ -151,9 +151,9 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
);
return cloneElement(element, {
return React.cloneElement(element, {
separator,
key: index,
key: index, // eslint-disable-line react/no-array-index-key
});
});
}

3
components/breadcrumb/style/index.less

@ -23,6 +23,9 @@
& > span:last-child {
color: @breadcrumb-last-item-color;
a {
color: @breadcrumb-last-item-color;
}
}
& > span:last-child &-separator {

68
components/cascader/__tests__/__snapshots__/index.test.js.snap

@ -349,6 +349,74 @@ exports[`Cascader can be selected 3`] = `
</div>
`;
exports[`Cascader have a notFoundContent that fit trigger input width 1`] = `
<div>
<div
class="ant-cascader-menus ant-cascader-menus-placement-bottomLeft slide-up-appear"
style="left: -999px; top: -995px;"
>
<div>
<ul
class="ant-cascader-menu"
style="height: auto;"
>
<li
class="ant-cascader-menu-item ant-cascader-menu-item-disabled"
role="menuitem"
title=""
>
<div
class="ant-empty ant-empty-normal ant-empty-small"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#F5F5F5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#D9D9D9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#FAFAFA"
/>
</g>
</g>
</svg>
</div>
<p
class="ant-empty-description"
>
No Data
</p>
</div>
</li>
</ul>
</div>
</div>
</div>
`;
exports[`Cascader popup correctly when panel is hidden 1`] = `
<div>
<div

12
components/cascader/__tests__/index.test.js

@ -471,4 +471,16 @@ describe('Cascader', () => {
mount(<Cascader options={optionsWithChildrenNull} />);
}).not.toThrow();
});
// https://github.com/ant-design/ant-design/issues/18176
it('have a notFoundContent that fit trigger input width', () => {
const wrapper = mount(<Cascader popupVisible options={[]} fieldNames={{ label: 'name', value: 'code', children: 'items' }} />);
const popupWrapper = mount(
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
expect(popupWrapper.render()).toMatchSnapshot();
});
});

6
components/cascader/index.tsx

@ -496,12 +496,12 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
]);
let { options } = props;
const names: FilledFieldNamesType = getFilledFieldNames(this.props);
if (options && options.length > 0) {
if (state.inputValue) {
options = this.generateFilteredOptions(prefixCls, renderEmpty);
}
} else {
const names: FilledFieldNamesType = getFilledFieldNames(this.props);
options = [
{
[names.label]: notFoundContent || renderEmpty('Cascader'),
@ -519,7 +519,7 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
const dropdownMenuColumnStyle: { width?: number; height?: string } = {};
const isNotFound =
(options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND';
(options || []).length === 1 && options[0][names.value] === 'ANT_CASCADER_NOT_FOUND';
if (isNotFound) {
dropdownMenuColumnStyle.height = 'auto'; // Height of one row.
}
@ -554,7 +554,7 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
value={state.inputValue}
disabled={disabled}
readOnly={!showSearch}
autoComplete="off"
autoComplete={inputProps.autoComplete || 'off'}
onClick={showSearch ? this.handleInputClick : undefined}
onBlur={showSearch ? this.handleInputBlur : undefined}
onKeyDown={this.handleKeyDown}

5
components/menu/SubMenu.tsx

@ -20,6 +20,7 @@ export interface SubMenuProps {
onTitleMouseEnter?: (e: TitleEventEntity) => void;
onTitleMouseLeave?: (e: TitleEventEntity) => void;
popupOffset?: [number, number];
popupClassName?: string;
}
class SubMenu extends React.Component<SubMenuProps, any> {
@ -41,14 +42,14 @@ class SubMenu extends React.Component<SubMenuProps, any> {
};
render() {
const { rootPrefixCls, className } = this.props;
const { rootPrefixCls, popupClassName } = this.props;
return (
<MenuContext.Consumer>
{({ antdMenuTheme }: MenuContextProps) => (
<RcSubMenu
{...this.props}
ref={this.saveSubMenu}
popupClassName={classNames(`${rootPrefixCls}-${antdMenuTheme}`, className)}
popupClassName={classNames(`${rootPrefixCls}-${antdMenuTheme}`, popupClassName)}
/>
)}
</MenuContext.Consumer>

1
components/menu/index.en-US.md

@ -62,6 +62,7 @@ More layouts with navigation: [layout](/components/layout).
| Param | Description | Type | Default value | Version |
| --- | --- | --- | --- | --- |
| popupClassName | sub menu class name | string | | 3.21.5 |
| children | sub menus or sub menu items | Array&lt;MenuItem\|SubMenu> | | |
| disabled | whether sub menu is disabled or not | boolean | false | |
| key | unique id of the sub menu | string | | |

15
components/menu/index.zh-CN.md

@ -61,13 +61,14 @@ subtitle: 导航菜单
### Menu.SubMenu
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ------------ | -------------- | --------------------------- | ------ | ---- |
| children | 子菜单的菜单项 | Array&lt;MenuItem\|SubMenu> | | |
| disabled | 是否禁用 | boolean | false | |
| key | 唯一标志 | string | | |
| title | 子菜单项值 | string\|ReactNode | | |
| onTitleClick | 点击子菜单标题 | function({ key, domEvent }) | | |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| -------------- | -------------- | --------------------------- | ------ | ------ |
| popupClassName | 子菜单样式 | string | | 3.21.5 |
| children | 子菜单的菜单项 | Array&lt;MenuItem\|SubMenu> | | |
| disabled | 是否禁用 | boolean | false | |
| key | 唯一标志 | string | | |
| title | 子菜单项值 | string\|ReactNode | | |
| onTitleClick | 点击子菜单标题 | function({ key, domEvent }) | | |
### Menu.ItemGroup

2
components/popconfirm/index.en-US.md

@ -17,7 +17,7 @@ The difference with the `confirm` modal dialog is that it's more lightweight tha
| Param | Description | Type | Default value | Version |
| --- | --- | --- | --- | --- |
| cancelText | text of the Cancel button | string | `Cancel` | |
| okText | text of the Confirm button | string | `Confirm` | |
| okText | text of the Confirm button | string | `OK` | |
| okType | Button `type` of the Confirm button | string | `primary` | |
| title | title of the confirmation box | string\|ReactNode | - | |
| onCancel | callback of cancel | function(e) | - | |

19
components/progress/style/index.less

@ -46,7 +46,12 @@
&-circle-path {
animation: ~'@{ant-prefix}-progress-appear' 0.3s;
stroke: @progress-default-color;
}
&-inner:not(.@{ant-prefix}-progress-circle-gradient) {
.@{ant-prefix}-progress-circle-path {
stroke: @progress-default-color;
}
}
&-success-bg,
@ -102,6 +107,9 @@
.@{progress-prefix-cls}-text {
color: @error-color;
}
}
&-status-exception &-inner:not(.@{progress-prefix-cls}-circle-gradient) {
.@{progress-prefix-cls}-circle-path {
stroke: @error-color;
}
@ -114,6 +122,9 @@
.@{progress-prefix-cls}-text {
color: @success-color;
}
}
&-status-success &-inner:not(.@{progress-prefix-cls}-circle-gradient) {
.@{progress-prefix-cls}-circle-path {
stroke: @success-color;
}
@ -153,12 +164,6 @@
color: @success-color;
}
}
&-circle-gradient {
.@{progress-prefix-cls}-circle-path {
stroke: url(#gradient);
}
}
}
@keyframes ~"@{ant-prefix}-progress-active" {

1
components/tag/CheckableTag.tsx

@ -5,6 +5,7 @@ import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
export interface CheckableTagProps {
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
checked: boolean;
onChange?: (checked: boolean) => void;
}

6
components/tooltip/index.tsx

@ -1,4 +1,4 @@
import React, { cloneElement } from 'react';
import * as React from 'react';
import { polyfill } from 'react-lifecycles-compat';
import RcTooltip from 'rc-tooltip';
import classNames from 'classnames';
@ -115,7 +115,7 @@ function getDisabledCompatibleChildren(element: React.ReactElement<any>) {
...omitted,
pointerEvents: 'none',
};
const child = cloneElement(element, {
const child = React.cloneElement(element, {
style: buttonStyle,
className: null,
});
@ -263,7 +263,7 @@ class Tooltip extends React.Component<TooltipProps, any> {
onVisibleChange={this.onVisibleChange}
onPopupAlign={this.onPopupAlign}
>
{visible ? cloneElement(child, { className: childCls }) : child}
{visible ? React.cloneElement(child, { className: childCls }) : child}
</RcTooltip>
);
};

2
components/upload/utils.tsx

@ -74,7 +74,7 @@ export const isImageUrl = (file: UploadFile): boolean => {
}
const url: string = (file.thumbUrl || file.url) as string;
const extension = extname(url);
if (/^data:image\//.test(url) || /(webp|svg|png|gif|jpg|jpeg|bmp|dpg)$/i.test(extension)) {
if (/^data:image\//.test(url) || /(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg)$/i.test(extension)) {
return true;
}
if (/^data:/.test(url)) {

6
package.json

@ -87,7 +87,7 @@
"rc-tree-select": "~2.9.1",
"rc-trigger": "^2.6.2",
"rc-upload": "~2.7.0",
"rc-util": "^4.6.0",
"rc-util": "^4.10.0",
"react-lazy-load": "^3.0.13",
"react-lifecycles-compat": "^3.0.4",
"react-slick": "~0.25.2",
@ -136,6 +136,7 @@
"eslint-plugin-react": "^7.14.2",
"eslint-tinker": "^0.5.0",
"fetch-jsonp": "^1.1.3",
"full-icu": "^1.3.0",
"glob": "^7.1.4",
"husky": "^3.0.2",
"immutability-helper": "^3.0.0",
@ -143,6 +144,7 @@
"jest": "^24.8.0",
"jsdom": "^15.1.1",
"jsonml.js": "^0.1.0",
"less": "~3.9.0",
"logrocket": "^1.0.0",
"logrocket-react": "^3.0.0",
"lz-string": "^1.4.4",
@ -210,7 +212,7 @@
"tsc": "tsc",
"start": "rimraf _site && mkdir _site && node ./scripts/generateColorLess.js && cross-env NODE_ENV=development bisheng start -c ./site/bisheng.config.js",
"start:preact": "node ./scripts/generateColorLess.js && cross-env NODE_ENV=development REACT_ENV=preact bisheng start -c ./site/bisheng.config.js",
"site": "bisheng build --ssr -c ./site/bisheng.config.js && node ./scripts/generateColorLess.js",
"site": "cross-env NODE_ICU_DATA=node_modules/full-icu bisheng build --ssr -c ./site/bisheng.config.js && node ./scripts/generateColorLess.js",
"predeploy": "antd-tools run clean && npm run site && cp netlify.toml CNAME _site && cp .circleci/config.yml _site",
"deploy": "bisheng gh-pages --push-only",
"deploy:china-mirror": "git checkout gh-pages && git pull origin gh-pages && git push git@gitee.com:ant-design/ant-design.git gh-pages",

Loading…
Cancel
Save