Merge pull request #24597 from ant-design/master

chore: Merge master into feature
This commit is contained in:
偏右 2020-05-30 15:19:49 +08:00 committed by GitHub
commit 75d84b2041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 1841 additions and 1351 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -5,11 +5,10 @@ on: [push]
jobs:
to_gitee:
runs-on: ubuntu-latest
if: github.repository == 'ant-design/ant-design'
steps:
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitee.com:ant-design/ant-design.git
ssh_private_key:
${{ secrets.GITEE_SSH_PRIVATE_KEY }}
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitee.com:ant-design/ant-design.git
ssh_private_key: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}

View File

@ -1,31 +0,0 @@
name: UI-TEST
on:
issue_comment:
types: [created]
jobs:
ui:
runs-on: ubuntu-latest
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/ui')
steps:
- name: checkout
uses: actions/checkout@master
- name: install
run: npm install
- name: dist
run: npm run dist
- name: test
run: npm run test:image
- name: VERCEL Now Deployment
uses: amondnet/now-deployment@v2.0.3
with:
zeit-token: ${{ secrets.VERCEL_TOKEN }}
now-project-id: ${{ secrets.VERCEL_PROJECT_ID}}
now-org-id: ${{ secrets.VERCEL_ORG_ID}}
working-directory: ./jest-stare
if: failure()

7
Dockerfile.ui-test Normal file
View File

@ -0,0 +1,7 @@
FROM buildkite/puppeteer:latest
RUN mkdir /app
WORKDIR /app
COPY package.json ./
RUN npm install
ENV PATH="${PATH}:/app/node_modules/.bin"
COPY . .

View File

@ -112,6 +112,7 @@ Veja [i18n](https://ant.design/docs/react/i18n).
- [Página inicial](https://ant.design/)
- [Componentes](https://ant.design/components/button)
- [Ant Design Pro](http://pro.ant.design/)
- [Ant Design Charts](https://charts.ant.design)
- [Change Log](CHANGELOG.en-US.md)
- [rc-components](http://react-component.github.io/)
- [Mobile UI](http://mobile.ant.design)

View File

@ -116,6 +116,7 @@ import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
- [首页](https://ant.design/)
- [组件库](https://ant.design/docs/react/introduce)
- [Ant Design Pro](http://pro.ant.design/)
- [Ant Design Charts](https://charts.ant.design)
- [更新日志](CHANGELOG.en-US.md)
- [React 底层基础组件](http://react-component.github.io/)
- [移动端组件](http://mobile.ant.design)

View File

@ -112,6 +112,7 @@ Dozens of languages supported in `antd`, see [i18n](https://ant.design/docs/reac
- [Home page](https://ant.design/)
- [Components](https://ant.design/components/button/)
- [Ant Design Pro](http://pro.ant.design/)
- [Ant Design Charts](https://charts.ant.design)
- [Change Log](CHANGELOG.en-US.md)
- [rc-components](http://react-component.github.io/)
- [Mobile UI](http://mobile.ant.design)

View File

@ -12,50 +12,106 @@ pool:
vmImage: 'ubuntu-latest'
stages:
- stage: site
jobs:
- job: Build_Site
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '12.13.1'
- script: npm install
displayName: 'Install modules'
- script: |
node ./scripts/azure-github-comment.js "[![Prepare preview](https://user-images.githubusercontent.com/5378891/72351368-2c979e00-371b-11ea-9652-eb4e825d745e.gif)](https://dev.azure.com/ant-design/ant-design/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
- script: npm run site
displayName: 'Build sites'
- script: ls -al _site/
displayName: 'List build'
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ant-design.surge.sh
echo "Deploy to $DEPLOY_DOMAIN"
npx surge --project ./_site --domain $DEPLOY_DOMAIN
displayName: 'Deploy Site'
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ant-design.surge.sh
node ./scripts/azure-github-comment.js "[<img width="306" src="https://user-images.githubusercontent.com/5378891/72400743-23dbb200-3785-11ea-9d13-1a2d92743846.png">]($DEPLOY_DOMAIN)"
displayName: 'Update comment on github'
- job: Build_Site_Failed
dependsOn: Build_Site
condition: failed()
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '12.13.1'
- script: npm install
displayName: 'Install modules'
- script: |
node ./scripts/azure-github-comment.js "[<img width="534" src="https://user-images.githubusercontent.com/5378891/75333447-1e63a280-58c1-11ea-975d-235367fd1522.png">](https://dev.azure.com/ant-design/ant-design/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
- stage: site
jobs:
- job: Build_Site
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '12.13.1'
- script: npm install
displayName: 'Install modules'
- script: |
node ./scripts/azure-github-comment.js "[![Prepare preview](https://user-images.githubusercontent.com/5378891/72351368-2c979e00-371b-11ea-9652-eb4e825d745e.gif)](https://dev.azure.com/ant-design/ant-design/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
- script: npm run site
displayName: 'Build sites'
- script: ls -al _site/
displayName: 'List build'
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ant-design.surge.sh
echo "Deploy to $DEPLOY_DOMAIN"
npx surge --project ./_site --domain $DEPLOY_DOMAIN
displayName: 'Deploy Site'
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ant-design.surge.sh
node ./scripts/azure-github-comment.js "[<img width="306" src="https://user-images.githubusercontent.com/5378891/72400743-23dbb200-3785-11ea-9d13-1a2d92743846.png">]($DEPLOY_DOMAIN)"
displayName: 'Update comment on github'
- job: Build_Site_Failed
dependsOn: Build_Site
condition: failed()
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '12.13.1'
- script: npm install
displayName: 'Install modules'
- script: |
node ./scripts/azure-github-comment.js "[<img width="534" src="https://user-images.githubusercontent.com/5378891/75333447-1e63a280-58c1-11ea-975d-235367fd1522.png">](https://dev.azure.com/ant-design/ant-design/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
- stage: ui
dependsOn: []
jobs:
- job: UI_Test
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '12.16.3'
- script: npm install
displayName: 'Install modules'
- script: |
node ./scripts/azure-github-comment.js -ui "[![UI Testing](https://user-images.githubusercontent.com/14831261/82744255-6bfc8800-9da8-11ea-9017-857933e8433b.gif)](https://dev.azure.com/ant-design/ant-design/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
- script: npm run test-image
displayName: 'UI Test'
- task: PublishPipelineArtifact@1
inputs:
targetPath: $(System.DefaultWorkingDirectory)/jest-stare
artifactName: jestStare
condition: failed()
- script: |
node ./scripts/azure-github-comment.js -ui "[<img width="306" src="https://user-images.githubusercontent.com/14831261/82744259-6e5ee200-9da8-11ea-8479-685f6e280b77.jpg">](https://dev.azure.com/ant-design/ant-design/_build/results?buildId=$(Build.BuildId))"
displayName: 'Update comment on github'
- job: UI_Test_Failed
dependsOn: UI_Test
condition: failed()
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '12.13.1'
- script: npm install
displayName: 'Install modules'
- task: DownloadPipelineArtifact@2
inputs:
artifact: jestStare
path: './jest-stare'
- script: ls -al ./jest-stare
displayName: 'List report'
- script: |
export DEPLOY_DOMAIN=https://ui-test-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ant-design.surge.sh
echo "Deploy to $DEPLOY_DOMAIN"
npx surge --project ./jest-stare --domain $DEPLOY_DOMAIN
displayName: 'Deploy Report Site'
- script: |
node ./scripts/azure-github-comment.js -ui "[<img width="306" src="https://user-images.githubusercontent.com/14831261/82744257-6dc64b80-9da8-11ea-80cf-05b2279a5602.jpg">](https://ui-test-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ant-design.surge.sh)"
displayName: 'Update comment on github'

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { findDOMNode } from 'react-dom';
import TransitionEvents from '@ant-design/css-animation/lib/Event';
import raf from './raf';
import { ConfigConsumer, ConfigConsumerProps, CSPConfig } from '../config-provider';
import { ConfigConsumer, ConfigConsumerProps, CSPConfig, ConfigContext } from '../config-provider';
let styleForPesudo: HTMLStyleElement | null;
@ -24,6 +24,8 @@ function isNotGrey(color: string) {
}
export default class Wave extends React.Component<{ insertExtraNode?: boolean }> {
static contextType = ConfigContext;
private instance?: {
cancel: () => void;
};
@ -40,6 +42,8 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }>
private csp?: CSPConfig;
context: ConfigConsumerProps;
componentDidMount() {
const node = findDOMNode(this) as HTMLElement;
if (!node || node.nodeType !== 1) {
@ -66,7 +70,8 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }>
const { insertExtraNode } = this.props;
this.extraNode = document.createElement('div');
const { extraNode } = this;
extraNode.className = 'ant-click-animating-node';
const { getPrefixCls } = this.context;
extraNode.className = `${getPrefixCls('')}-click-animating-node`;
const attributeName = this.getAttributeName();
node.setAttribute(attributeName, 'true');
// Not white or transparnt or grey
@ -86,7 +91,9 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }>
extraNode.style.borderColor = waveColor;
styleForPesudo.innerHTML = `
[ant-click-animating-without-extra-node='true']::after, .ant-click-animating-node {
[${getPrefixCls('')}-click-animating-without-extra-node='true']::after, .${getPrefixCls(
'',
)}-click-animating-node {
--antd-wave-shadow-color: ${waveColor};
}`;
if (!document.body.contains(styleForPesudo)) {
@ -121,8 +128,11 @@ export default class Wave extends React.Component<{ insertExtraNode?: boolean }>
};
getAttributeName() {
const { getPrefixCls } = this.context;
const { insertExtraNode } = this.props;
return insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node';
return insertExtraNode
? `${getPrefixCls('')}-click-animating`
: `${getPrefixCls('')}-click-animating-without-extra-node`;
}
bindAnimationEvent = (node: HTMLElement) => {

View File

@ -76,7 +76,7 @@
&-close-icon {
position: absolute;
top: 8px + @font-size-base * @line-height-base / 2 - @font-size-base / 2;
right: 16px;
right: @padding-md;
padding: 0;
overflow: hidden;
font-size: @font-size-sm;
@ -124,8 +124,8 @@
&-with-description &-close-icon {
position: absolute;
top: @padding-xs;
right: @padding-xs;
top: @padding-md;
right: @padding-md;
font-size: @font-size-base;
cursor: pointer;
}

View File

@ -31,7 +31,7 @@
&-icon {
.@{alert-prefix-cls}-rtl & {
right: 16px;
right: @padding-md;
left: auto;
}
}
@ -39,14 +39,16 @@
&-close-icon {
.@{alert-prefix-cls}-rtl & {
right: auto;
left: 16px;
left: @padding-md;
}
}
&-with-description,
&-with-description&-closable {
.@{alert-prefix-cls}.@{alert-prefix-cls}-rtl& {
padding: 15px 64px 15px 15px;
padding: @alert-with-description-padding-vertical @alert-with-description-icon-size * 2 +
@alert-with-description-padding-vertical @alert-with-description-no-icon-padding-vertical
15px;
}
}
@ -58,7 +60,7 @@
&-with-description &-icon {
.@{alert-prefix-cls}-rtl& {
right: 24px;
right: @alert-with-description-icon-size;
left: auto;
}
}
@ -66,7 +68,7 @@
&-with-description &-close-icon {
.@{alert-prefix-cls}-rtl& {
right: auto;
left: 16px;
left: @padding-md;
}
}
}

View File

@ -142,4 +142,16 @@ describe('Avatar Render', () => {
);
warnSpy.mockRestore();
});
it('support size is number', () => {
const wrapper = mount(<Avatar size={100}>TestString</Avatar>);
expect(wrapper).toMatchRenderedSnapshot();
});
it('support onMouseEnter', () => {
const onMouseEnter = jest.fn();
const wrapper = mount(<Avatar onMouseEnter={onMouseEnter}>TestString</Avatar>);
wrapper.simulate('mouseenter');
expect(onMouseEnter).toHaveBeenCalled();
});
});

View File

@ -10,3 +10,17 @@ exports[`Avatar Render rtl render component should be rendered correctly in RTL
/>
</span>
`;
exports[`Avatar Render support size is number 1`] = `
<span
class="ant-avatar ant-avatar-circle"
style="width:100px;height:100px;line-height:100px;font-size:18px"
>
<span
class="ant-avatar-string"
style="opacity:0"
>
TestString
</span>
</span>
`;

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/avatar/demo/badge.md correctly 1`] = `
<div>
Array [
<span
class="avatar-item"
>
@ -194,7 +194,7 @@ exports[`renders ./components/avatar/demo/badge.md correctly 1`] = `
</span>
</sup>
</span>
</span>
</span>,
<span>
<span
class="ant-badge"
@ -228,12 +228,12 @@ exports[`renders ./components/avatar/demo/badge.md correctly 1`] = `
data-show="true"
/>
</span>
</span>
</div>
</span>,
]
`;
exports[`renders ./components/avatar/demo/basic.md correctly 1`] = `
<div>
Array [
<div>
<span
class="ant-avatar ant-avatar-circle ant-avatar-icon"
@ -332,7 +332,7 @@ exports[`renders ./components/avatar/demo/basic.md correctly 1`] = `
</svg>
</span>
</span>
</div>
</div>,
<div>
<span
class="ant-avatar ant-avatar-square ant-avatar-icon"
@ -431,12 +431,12 @@ exports[`renders ./components/avatar/demo/basic.md correctly 1`] = `
</svg>
</span>
</span>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/avatar/demo/dynamic.md correctly 1`] = `
<div>
Array [
<span
class="ant-avatar ant-avatar-lg ant-avatar-circle"
style="background-color:#f56a00;vertical-align:middle"
@ -447,7 +447,7 @@ exports[`renders ./components/avatar/demo/dynamic.md correctly 1`] = `
>
U
</span>
</span>
</span>,
<button
class="ant-btn ant-btn-sm"
style="margin:0 16px;vertical-align:middle"
@ -456,7 +456,7 @@ exports[`renders ./components/avatar/demo/dynamic.md correctly 1`] = `
<span>
ChangeUser
</span>
</button>
</button>,
<button
class="ant-btn ant-btn-sm"
style="vertical-align:middle"
@ -465,12 +465,12 @@ exports[`renders ./components/avatar/demo/dynamic.md correctly 1`] = `
<span>
changeGap
</span>
</button>
</div>
</button>,
]
`;
exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = `
<div>
Array [
<button
class="ant-btn"
type="button"
@ -478,7 +478,7 @@ exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = `
<span>
Toggle Avatar visibility
</span>
</button>
</button>,
<button
class="ant-btn"
type="button"
@ -486,7 +486,7 @@ exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = `
<span>
Toggle Avatar size
</span>
</button>
</button>,
<button
class="ant-btn"
type="button"
@ -494,9 +494,9 @@ exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = `
<span>
Change Avatar scale
</span>
</button>
<br />
<br />
</button>,
<br />,
<br />,
<div
style="text-align:center;transform:scale(1);margin-top:24px"
>
@ -542,12 +542,12 @@ exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = `
/>
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/avatar/demo/type.md correctly 1`] = `
<div>
Array [
<span
class="ant-avatar ant-avatar-circle ant-avatar-icon"
>
@ -571,7 +571,7 @@ exports[`renders ./components/avatar/demo/type.md correctly 1`] = `
/>
</svg>
</span>
</span>
</span>,
<span
class="ant-avatar ant-avatar-circle"
>
@ -581,9 +581,10 @@ exports[`renders ./components/avatar/demo/type.md correctly 1`] = `
>
U
</span>
</span>
</span>,
<span
class="ant-avatar ant-avatar-circle"
style="width:40px;height:40px;line-height:40px;font-size:18px"
>
<span
class="ant-avatar-string"
@ -591,14 +592,14 @@ exports[`renders ./components/avatar/demo/type.md correctly 1`] = `
>
USER
</span>
</span>
</span>,
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</span>
</span>,
<span
class="ant-avatar ant-avatar-circle"
style="color:#f56a00;background-color:#fde3cf"
@ -609,7 +610,7 @@ exports[`renders ./components/avatar/demo/type.md correctly 1`] = `
>
U
</span>
</span>
</span>,
<span
class="ant-avatar ant-avatar-circle ant-avatar-icon"
style="background-color:#87d068"
@ -634,6 +635,6 @@ exports[`renders ./components/avatar/demo/type.md correctly 1`] = `
/>
</svg>
</span>
</span>
</div>
</span>,
]
`;

View File

@ -18,7 +18,7 @@ import { Avatar, Badge } from 'antd';
import { UserOutlined } from '@ant-design/icons';
ReactDOM.render(
<div>
<>
<span className="avatar-item">
<Badge count={1}>
<Avatar shape="square" icon={<UserOutlined />} />
@ -29,7 +29,7 @@ ReactDOM.render(
<Avatar shape="square" icon={<UserOutlined />} />
</Badge>
</span>
</div>,
</>,
mountNode,
);
```

View File

@ -18,7 +18,7 @@ import { Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons';
ReactDOM.render(
<div>
<>
<div>
<Avatar size={64} icon={<UserOutlined />} />
<Avatar size="large" icon={<UserOutlined />} />
@ -31,7 +31,7 @@ ReactDOM.render(
<Avatar shape="square" icon={<UserOutlined />} />
<Avatar shape="square" size="small" icon={<UserOutlined />} />
</div>
</div>,
</>,
mountNode,
);
```

View File

@ -35,7 +35,7 @@ const Autoset: React.FC = () => {
setGap(index < GapList.length - 1 ? GapList[index + 1] : GapList[0]);
};
return (
<div>
<>
<Avatar style={{ backgroundColor: color, verticalAlign: 'middle' }} size="large" gap={gap}>
{user}
</Avatar>
@ -49,7 +49,7 @@ const Autoset: React.FC = () => {
<Button size="small" style={{ verticalAlign: 'middle' }} onClick={changeGap}>
changeGap
</Button>
</div>
</>
);
};

View File

@ -41,7 +41,7 @@ const App: React.FC = () => {
};
return (
<div>
<>
<Button onClick={toggle}>Toggle Avatar visibility</Button>
<Button onClick={toggleSize}>Toggle Avatar size</Button>
<Button onClick={changeScale}>Change Avatar scale</Button>
@ -67,7 +67,7 @@ const App: React.FC = () => {
</Avatar>
</div>
</div>
</div>
</>
);
};

View File

@ -18,14 +18,14 @@ import { Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons';
ReactDOM.render(
<div>
<>
<Avatar icon={<UserOutlined />} />
<Avatar>U</Avatar>
<Avatar>USER</Avatar>
<Avatar size={40}>USER</Avatar>
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
<Avatar style={{ color: '#f56a00', backgroundColor: '#fde3cf' }}>U</Avatar>
<Avatar style={{ backgroundColor: '#87d068' }} icon={<UserOutlined />} />
</div>,
</>,
mountNode,
);
```

View File

@ -202,8 +202,15 @@ export default class Avatar extends React.Component<AvatarProps, AvatarState> {
);
}
}
// The event is triggered twice from bubbling up the DOM tree.
// see https://codesandbox.io/s/kind-snow-9lidz
delete others.onError;
delete others.gap;
return (
<span
{...others}
style={{ ...sizeStyle, ...others.style }}
className={classString}
ref={(node: HTMLElement) => (this.avatarNode = node)}

View File

@ -1,11 +1,10 @@
import * as React from 'react';
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import omit from 'omit.js';
import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator';
import Menu from '../menu';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning';
import { Omit } from '../_util/type';
import { cloneElement } from '../_util/reactNode';
@ -49,41 +48,48 @@ function defaultItemRender(route: Route, params: any, routes: Route[], paths: st
return isLastItem ? <span>{name}</span> : <a href={`#/${paths.join('/')}`}>{name}</a>;
}
export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
static Item: typeof BreadcrumbItem;
const getPath = (path: string, params: any) => {
path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]);
});
return path;
};
static Separator: typeof BreadcrumbSeparator;
const addChildPath = (paths: string[], childPath: string = '', params: any) => {
const originalPaths = [...paths];
const path = getPath(childPath, params);
if (path) {
originalPaths.push(path);
}
return originalPaths;
};
static defaultProps = {
separator: '/',
};
interface BreadcrumbInterface extends React.FC<BreadcrumbProps> {
Item: typeof BreadcrumbItem;
Separator: typeof BreadcrumbSeparator;
}
getPath = (path: string, params: any) => {
path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]);
});
return path;
};
const Breadcrumb: BreadcrumbInterface = ({
prefixCls: customizePrefixCls,
separator = '/',
style,
className,
routes,
children,
itemRender = defaultItemRender,
params = {},
...restProps
}) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
addChildPath = (paths: string[], childPath: string = '', params: any) => {
const originalPaths = [...paths];
const path = this.getPath(childPath, params);
if (path) {
originalPaths.push(path);
}
return originalPaths;
};
genForRoutes = ({
routes = [],
params = {},
separator,
itemRender = defaultItemRender,
}: BreadcrumbProps) => {
let crumbs;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
if (routes && routes.length > 0) {
// generated by route
const paths: string[] = [];
return routes.map(route => {
const path = this.getPath(route.path, params);
crumbs = routes.map(route => {
const path = getPath(route.path, params);
if (path) {
paths.push(path);
@ -95,7 +101,7 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
<Menu>
{route.children.map(child => (
<Menu.Item key={child.path || child.breadcrumbName}>
{itemRender(child, params, routes, this.addChildPath(paths, child.path, params))}
{itemRender(child, params, routes, addChildPath(paths, child.path, params))}
</Menu.Item>
))}
</Menu>
@ -108,58 +114,40 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
</BreadcrumbItem>
);
});
};
} else if (children) {
crumbs = toArray(children).map((element: any, index) => {
if (!element) {
return element;
}
renderBreadcrumb = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
let crumbs;
const {
prefixCls: customizePrefixCls,
separator,
style,
className,
routes,
children,
...restProps
} = this.props;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
if (routes && routes.length > 0) {
// generated by route
crumbs = this.genForRoutes(this.props);
} else if (children) {
crumbs = toArray(children).map((element: any, index) => {
if (!element) {
return element;
}
devWarning(
element.type &&
(element.type.__ANT_BREADCRUMB_ITEM === true ||
element.type.__ANT_BREADCRUMB_SEPARATOR === true),
'Breadcrumb',
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
);
devWarning(
element.type &&
(element.type.__ANT_BREADCRUMB_ITEM === true ||
element.type.__ANT_BREADCRUMB_SEPARATOR === true),
'Breadcrumb',
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
);
return cloneElement(element, {
separator,
key: index,
});
return cloneElement(element, {
separator,
key: index,
});
}
const breadcrumbClassName = classNames(className, prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
return (
<div
className={breadcrumbClassName}
style={style}
{...omit(restProps, ['itemRender', 'linkRender', 'nameRender', 'params'])}
>
{crumbs}
</div>
);
};
render() {
return <ConfigConsumer>{this.renderBreadcrumb}</ConfigConsumer>;
}
}
const breadcrumbClassName = classNames(className, prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
return (
<div className={breadcrumbClassName} style={style} {...restProps}>
{crumbs}
</div>
);
};
Breadcrumb.Item = BreadcrumbItem;
Breadcrumb.Separator = BreadcrumbSeparator;
export default Breadcrumb;

View File

@ -1,9 +1,8 @@
import * as React from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined';
import omit from 'omit.js';
import DropDown, { DropDownProps } from '../dropdown/dropdown';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
export interface BreadcrumbItemProps {
prefixCls?: string;
@ -13,53 +12,24 @@ export interface BreadcrumbItemProps {
dropdownProps?: DropDownProps;
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLSpanElement>;
}
export default class BreadcrumbItem extends React.Component<BreadcrumbItemProps, any> {
static __ANT_BREADCRUMB_ITEM = true;
static defaultProps = {
separator: '/',
};
renderBreadcrumbItem = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, separator, children, ...restProps } = this.props;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
let link;
if ('href' in this.props) {
link = (
<a className={`${prefixCls}-link`} {...omit(restProps, ['overlay'])}>
{children}
</a>
);
} else {
link = (
<span className={`${prefixCls}-link`} {...omit(restProps, ['overlay'])}>
{children}
</span>
);
}
// wrap to dropDown
link = this.renderBreadcrumbNode(link, prefixCls);
if (children) {
return (
<span>
{link}
{separator && separator !== '' && (
<span className={`${prefixCls}-separator`}>{separator}</span>
)}
</span>
);
}
return null;
};
interface BreadcrumbItemInterface extends React.FC<BreadcrumbItemProps> {
__ANT_BREADCRUMB_ITEM: boolean;
}
const BreadcrumbItem: BreadcrumbItemInterface = ({
prefixCls: customizePrefixCls,
separator,
children,
overlay,
dropdownProps,
...restProps
}) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
/**
* if overlay is have
* Wrap a DropDown
*/
renderBreadcrumbNode = (breadcrumbItem: React.ReactNode, prefixCls: string) => {
const { overlay, dropdownProps } = this.props;
const renderBreadcrumbNode = (breadcrumbItem: React.ReactNode) => {
if (overlay) {
return (
<DropDown overlay={overlay} placement="bottomCenter" {...dropdownProps}>
@ -73,7 +43,36 @@ export default class BreadcrumbItem extends React.Component<BreadcrumbItemProps,
return breadcrumbItem;
};
render() {
return <ConfigConsumer>{this.renderBreadcrumbItem}</ConfigConsumer>;
let link;
if ('href' in restProps) {
link = (
<a className={`${prefixCls}-link`} {...restProps}>
{children}
</a>
);
} else {
link = (
<span className={`${prefixCls}-link`} {...restProps}>
{children}
</span>
);
}
}
// wrap to dropDown
link = renderBreadcrumbNode(link);
if (children) {
return (
<span>
{link}
{separator && separator !== '' && (
<span className={`${prefixCls}-separator`}>{separator}</span>
)}
</span>
);
}
return null;
};
BreadcrumbItem.__ANT_BREADCRUMB_ITEM = true;
export default BreadcrumbItem;

View File

@ -1,17 +1,17 @@
import * as React from 'react';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
export default class BreadcrumbSeparator extends React.Component<any> {
static __ANT_BREADCRUMB_SEPARATOR = true;
renderSeparator = ({ getPrefixCls }: ConfigConsumerProps) => {
const { children } = this.props;
const prefixCls = getPrefixCls('breadcrumb');
return <span className={`${prefixCls}-separator`}>{children || '/'}</span>;
};
render() {
return <ConfigConsumer>{this.renderSeparator}</ConfigConsumer>;
}
interface BreadcrumbSeparatorInterface extends React.FC {
__ANT_BREADCRUMB_SEPARATOR: boolean;
}
const BreadcrumbSeparator: BreadcrumbSeparatorInterface = ({ children }) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb');
return <span className={`${prefixCls}-separator`}>{children || '/'}</span>;
};
BreadcrumbSeparator.__ANT_BREADCRUMB_SEPARATOR = true;
export default BreadcrumbSeparator;

View File

@ -111,21 +111,6 @@ describe('Breadcrumb', () => {
it('should accept undefined routes', () => {
const wrapper = render(<Breadcrumb routes={undefined} />);
expect(wrapper).toMatchSnapshot();
})
it('props#linkRender and props#nameRender do not warn anymore', () => {
const linkRender = jest.fn();
const nameRender = jest.fn();
mount(
<Breadcrumb linkRender={linkRender} nameRender={nameRender}>
<Breadcrumb.Item />
<Breadcrumb.Item>xxx</Breadcrumb.Item>
</Breadcrumb>,
);
expect(errorSpy.mock.calls.length).toBe(0);
expect(linkRender).not.toHaveBeenCalled();
expect(nameRender).not.toHaveBeenCalled();
});
it('should support custom attribute', () => {

View File

@ -73,7 +73,6 @@ exports[`react router react router 3 1`] = `
},
]
}
separator="/"
>
<div
className="ant-breadcrumb"

View File

@ -1,10 +1,6 @@
import Breadcrumb from './Breadcrumb';
import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator';
export { BreadcrumbProps } from './Breadcrumb';
export { BreadcrumbItemProps } from './BreadcrumbItem';
Breadcrumb.Item = BreadcrumbItem;
Breadcrumb.Separator = BreadcrumbSeparator;
export default Breadcrumb;

View File

@ -38,7 +38,8 @@
}
&-link {
> .@{iconfont-css-prefix} + span {
> .@{iconfont-css-prefix} + span,
> .@{iconfont-css-prefix} + a {
margin-left: 4px;
}
}

View File

@ -14,7 +14,8 @@
}
&-link {
> .@{iconfont-css-prefix} + span {
> .@{iconfont-css-prefix} + span,
> .@{iconfont-css-prefix} + a {
.@{breadcrumb-prefix-cls}-rtl & {
margin-right: 4px;
margin-left: 0;

View File

@ -106,7 +106,7 @@ Array [
type="button"
>
<span>
link
Dashed
</span>
</button>,
<button

View File

@ -0,0 +1,14 @@
import React from 'react';
import Button from '..';
import imageTest from '../../../tests/shared/imageTest';
describe('Button image', () => {
imageTest(
<>
<Button type="primary">Primary</Button>
<Button>Default</Button>
<Button type="dashed">Dashed</Button>
<Button type="link">Link</Button>
</>,
);
});

View File

@ -23,7 +23,7 @@ ReactDOM.render(
</Button>
<Button danger>Default</Button>
<Button type="dashed" danger>
link
Dashed
</Button>
<Button type="link" danger>
link

View File

@ -39,6 +39,14 @@
}
}
}
.@{calendar-prefix-cls}-date {
&-content {
.@{calendar-prefix-cls}-rtl& {
text-align: right;
}
}
}
}
}
}

View File

@ -340,11 +340,6 @@ exports[`renders ./components/card/demo/inner.md correctly 1`] = `
<div
class="ant-card-body"
>
<p
class="site-card-demo-inner-p"
>
Group title
</p>
<div
class="ant-card ant-card-bordered ant-card-type-inner"
>

View File

@ -18,7 +18,6 @@ import { Card } from 'antd';
ReactDOM.render(
<Card title="Card title">
<p className="site-card-demo-inner-p">Group title</p>
<Card type="inner" title="Inner Card title" extra={<a href="#">More</a>}>
Inner Card content
</Card>
@ -34,18 +33,3 @@ ReactDOM.render(
mountNode,
);
```
```css
.site-card-demo-inner-p {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 16px;
font-weight: 500;
}
```
<style>
[data-theme="dark"] .site-card-demo-inner-p {
color: rgba(255,255,255,.85);
}
</style>

View File

@ -228,6 +228,10 @@
position: absolute;
right: @control-padding-horizontal;
color: @text-color-secondary;
.@{cascader-prefix-cls}-menu-item-disabled& {
color: @disabled-color;
}
}
& &-keyword {

View File

@ -51,15 +51,21 @@
}
&-menu {
&-rtl {
&-rtl & {
direction: rtl;
border-right: none;
border-left: @border-width-base @border-style-base @border-color-split;
&:first-child {
border-radius: 0 @border-radius-base @border-radius-base 0;
}
&:last-child {
margin-right: 0;
margin-left: -1px;
border-left-color: transparent;
border-radius: 0 0 4px 4px;
border-radius: @border-radius-base 0 0 @border-radius-base;
}
&:only-child {
border-radius: @border-radius-base;
}
}
}

View File

@ -24,6 +24,7 @@ export interface AbstractCheckboxProps<T> {
children?: React.ReactNode;
id?: string;
autoFocus?: boolean;
type?: string;
}
export interface CheckboxProps extends AbstractCheckboxProps<CheckboxChangeEvent> {

View File

@ -4,7 +4,7 @@ import classNames from 'classnames';
import RightOutlined from '@ant-design/icons/RightOutlined';
import CollapsePanel from './CollapsePanel';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import animation from '../_util/openAnimation';
import { cloneElement } from '../_util/reactNode';
@ -36,23 +36,25 @@ interface PanelProps {
extra?: React.ReactNode;
}
export default class Collapse extends React.Component<CollapseProps, any> {
static Panel = CollapsePanel;
interface CollapseInterface extends React.FC<CollapseProps> {
Panel: typeof CollapsePanel;
}
static defaultProps = {
bordered: true,
};
const Collapse: CollapseInterface = props => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, className = '', bordered } = props;
const prefixCls = getPrefixCls('collapse', customizePrefixCls);
getIconPosition(direction: string = 'ltr') {
const { expandIconPosition } = this.props;
const getIconPosition = () => {
const { expandIconPosition } = props;
if (expandIconPosition !== undefined) {
return expandIconPosition;
}
return direction === 'rtl' ? 'right' : 'left';
}
};
renderExpandIcon = (panelProps: PanelProps = {}, prefixCls: string) => {
const { expandIcon } = this.props;
const renderExpandIcon = (panelProps: PanelProps = {}) => {
const { expandIcon } = props;
const icon = (expandIcon ? (
expandIcon(panelProps)
) : (
@ -64,32 +66,32 @@ export default class Collapse extends React.Component<CollapseProps, any> {
}));
};
renderCollapse = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className = '', bordered } = this.props;
const prefixCls = getPrefixCls('collapse', customizePrefixCls);
const iconPosition = this.getIconPosition(direction);
const collapseClassName = classNames(
{
[`${prefixCls}-borderless`]: !bordered,
[`${prefixCls}-icon-position-${iconPosition}`]: true,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const openAnimation = { ...animation, appear() {} };
const iconPosition = getIconPosition();
const collapseClassName = classNames(
{
[`${prefixCls}-borderless`]: !bordered,
[`${prefixCls}-icon-position-${iconPosition}`]: true,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const openAnimation = { ...animation, appear() {} };
return (
<RcCollapse
openAnimation={openAnimation}
{...this.props}
expandIcon={(panelProps: PanelProps) => this.renderExpandIcon(panelProps, prefixCls)}
prefixCls={prefixCls}
className={collapseClassName}
/>
);
};
return (
<RcCollapse
openAnimation={openAnimation}
{...props}
expandIcon={(panelProps: PanelProps) => renderExpandIcon(panelProps)}
prefixCls={prefixCls}
className={collapseClassName}
/>
);
};
render() {
return <ConfigConsumer>{this.renderCollapse}</ConfigConsumer>;
}
}
Collapse.Panel = CollapsePanel;
Collapse.defaultProps = {
bordered: true,
};
export default Collapse;

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import RcCollapse from 'rc-collapse';
import classNames from 'classnames';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
export interface CollapsePanelProps {
key: string | number;
@ -16,22 +16,17 @@ export interface CollapsePanelProps {
extra?: React.ReactNode;
}
export default class CollapsePanel extends React.Component<CollapsePanelProps, {}> {
renderCollapsePanel = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className = '', showArrow = true } = this.props;
const prefixCls = getPrefixCls('collapse', customizePrefixCls);
const collapsePanelClassName = classNames(
{
[`${prefixCls}-no-arrow`]: !showArrow,
},
className,
);
return (
<RcCollapse.Panel {...this.props} prefixCls={prefixCls} className={collapsePanelClassName} />
);
};
const CollapsePanel: React.FC<CollapsePanelProps> = props => {
const { getPrefixCls } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, className = '', showArrow = true } = props;
const prefixCls = getPrefixCls('collapse', customizePrefixCls);
const collapsePanelClassName = classNames(
{
[`${prefixCls}-no-arrow`]: !showArrow,
},
className,
);
return <RcCollapse.Panel {...props} prefixCls={prefixCls} className={collapsePanelClassName} />;
};
render() {
return <ConfigConsumer>{this.renderCollapsePanel}</ConfigConsumer>;
}
}
export default CollapsePanel;

View File

@ -122,77 +122,70 @@ exports[`renders ./components/comment/demo/basic.md correctly 1`] = `
`;
exports[`renders ./components/comment/demo/editor.md correctly 1`] = `
<div>
<div
class="ant-comment"
>
<div
class="ant-comment"
class="ant-comment-inner"
>
<div
class="ant-comment-inner"
class="ant-comment-avatar"
>
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
alt="Han Solo"
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</span>
</div>
<div
class="ant-comment-content"
>
<div
class="ant-comment-avatar"
>
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
alt="Han Solo"
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</span>
</div>
<div
class="ant-comment-content"
class="ant-comment-content-detail"
>
<div
class="ant-comment-content-author"
/>
<div
class="ant-comment-content-detail"
class="ant-row ant-form-item"
>
<div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-row ant-form-item"
class="ant-form-item-control-input"
>
<div
class="ant-col ant-form-item-control"
class="ant-form-item-control-input-content"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<textarea
class="ant-input"
rows="4"
/>
</div>
</div>
<textarea
class="ant-input"
rows="4"
/>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-row ant-form-item"
class="ant-form-item-control-input"
>
<div
class="ant-col ant-form-item-control"
class="ant-form-item-control-input-content"
>
<div
class="ant-form-item-control-input"
<button
class="ant-btn ant-btn-primary"
type="submit"
>
<div
class="ant-form-item-control-input-content"
>
<button
class="ant-btn ant-btn-primary"
type="submit"
>
<span>
Add Comment
</span>
</button>
</div>
</div>
<span>
Add Comment
</span>
</button>
</div>
</div>
</div>

View File

@ -10,9 +10,6 @@ exports[`Comment rtl render component should be rendered correctly in RTL direct
<div
class="ant-comment-content"
>
<div
class="ant-comment-content-author"
/>
<div
class="ant-comment-content-detail"
/>
@ -20,3 +17,41 @@ exports[`Comment rtl render component should be rendered correctly in RTL direct
</div>
</div>
`;
exports[`Comment should support empty actions 1`] = `
<div
class="ant-comment"
>
<div
class="ant-comment-inner"
>
<div
class="ant-comment-content"
>
<div
class="ant-comment-content-author"
>
<span
class="ant-comment-content-author-name"
>
<a>
Han Solo
</a>
</span>
<span
class="ant-comment-content-author-time"
>
YYYY-MM-DD HH:mm:ss
</span>
</div>
<div
class="ant-comment-content-detail"
>
<p>
We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.
</p>
</div>
</div>
</div>
</div>
`;

View File

@ -1,3 +1,5 @@
import React from 'react';
import { mount } from 'enzyme';
import Comment from '../index';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
@ -5,4 +7,22 @@ import rtlTest from '../../../tests/shared/rtlTest';
describe('Comment', () => {
mountTest(Comment);
rtlTest(Comment);
it('should support empty actions', () => {
const wrapper = mount(
<Comment
actions={[]}
author={<a>Han Solo</a>}
content={
<p>
We supply a series of design principles, practical patterns and high quality design
resources (Sketch and Axure), to help people create their product prototypes beautifully
and efficiently.
</p>
}
datetime="YYYY-MM-DD HH:mm:ss"
/>,
);
expect(wrapper).toMatchRenderedSnapshot();
});
});

View File

@ -45,7 +45,7 @@ const Demo = () => {
</Tooltip>
<span className="comment-action">{likes}</span>
</span>,
<span key=' key="comment-basic-dislike"'>
<span key="comment-basic-dislike">
<Tooltip title="Dislike">
{React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined, {
onClick: dislike,
@ -80,7 +80,7 @@ const Demo = () => {
}
/>
);
}
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -29,7 +29,7 @@ const CommentList = ({ comments }) => (
);
const Editor = ({ onChange, onSubmit, submitting, value }) => (
<div>
<>
<Form.Item>
<TextArea rows={4} onChange={onChange} value={value} />
</Form.Item>
@ -38,7 +38,7 @@ const Editor = ({ onChange, onSubmit, submitting, value }) => (
Add Comment
</Button>
</Form.Item>
</div>
</>
);
class App extends React.Component {
@ -84,7 +84,7 @@ class App extends React.Component {
const { comments, submitting, value } = this.state;
return (
<div>
<>
{comments.length > 0 && <CommentList comments={comments} />}
<Comment
avatar={
@ -102,7 +102,7 @@ class App extends React.Component {
/>
}
/>
</div>
</>
);
}
}

View File

@ -2,15 +2,6 @@ import * as React from 'react';
import classNames from 'classnames';
import { ConfigContext } from '../config-provider';
function getAction(actions: React.ReactNode[]) {
if (!actions || !actions.length) {
return null;
}
// eslint-disable-next-line react/no-array-index-key
const actionList = actions.map((action, index) => <li key={`action-${index}`}>{action}</li>);
return actionList;
}
export interface CommentProps {
/** List of action items rendered below the comment content */
actions?: Array<React.ReactNode>;
@ -40,7 +31,6 @@ const Comment: React.FC<CommentProps> = ({
className,
content,
prefixCls: customizePrefixCls,
style,
datetime,
...otherProps
}) => {
@ -60,10 +50,14 @@ const Comment: React.FC<CommentProps> = ({
const actionDom =
actions && actions.length ? (
<ul className={`${prefixCls}-actions`}>{getAction(actions)}</ul>
<ul className={`${prefixCls}-actions`}>
{actions.map((action, index) => (
<li key={`action-${index}`}>{action}</li> // eslint-disable-line react/no-array-index-key
))}
</ul>
) : null;
const authorContent = (
const authorContent = (author || datetime) && (
<div className={`${prefixCls}-content-author`}>
{author && <span className={`${prefixCls}-content-author-name`}>{author}</span>}
{datetime && <span className={`${prefixCls}-content-author-time`}>{datetime}</span>}
@ -78,19 +72,16 @@ const Comment: React.FC<CommentProps> = ({
</div>
);
const comment = (
<div className={`${prefixCls}-inner`}>
{avatarDom}
{contentDom}
</div>
);
const cls = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
return (
<div {...otherProps} className={cls} style={style}>
{comment}
<div {...otherProps} className={cls}>
<div className={`${prefixCls}-inner`}>
{avatarDom}
{contentDom}
</div>
{children ? renderNested(prefixCls, children) : null}
</div>
);

View File

@ -11446,9 +11446,6 @@ exports[`ConfigProvider components Comment configProvider 1`] = `
<div
class="config-comment-content"
>
<div
class="config-comment-content-author"
/>
<div
class="config-comment-content-detail"
>
@ -11468,9 +11465,6 @@ exports[`ConfigProvider components Comment configProvider 1`] = `
<div
class="config-comment-content"
>
<div
class="config-comment-content-author"
/>
<div
class="config-comment-content-detail"
>
@ -11493,9 +11487,6 @@ exports[`ConfigProvider components Comment configProvider componentSize large 1`
<div
class="config-comment-content"
>
<div
class="config-comment-content-author"
/>
<div
class="config-comment-content-detail"
>
@ -11515,9 +11506,6 @@ exports[`ConfigProvider components Comment configProvider componentSize large 1`
<div
class="config-comment-content"
>
<div
class="config-comment-content-author"
/>
<div
class="config-comment-content-detail"
>
@ -11540,9 +11528,6 @@ exports[`ConfigProvider components Comment configProvider componentSize middle 1
<div
class="config-comment-content"
>
<div
class="config-comment-content-author"
/>
<div
class="config-comment-content-detail"
>
@ -11562,9 +11547,6 @@ exports[`ConfigProvider components Comment configProvider componentSize middle 1
<div
class="config-comment-content"
>
<div
class="config-comment-content-author"
/>
<div
class="config-comment-content-detail"
>
@ -11587,9 +11569,6 @@ exports[`ConfigProvider components Comment configProvider virtual and dropdownMa
<div
class="ant-comment-content"
>
<div
class="ant-comment-content-author"
/>
<div
class="ant-comment-content-detail"
>
@ -11609,9 +11588,6 @@ exports[`ConfigProvider components Comment configProvider virtual and dropdownMa
<div
class="ant-comment-content"
>
<div
class="ant-comment-content-author"
/>
<div
class="ant-comment-content-detail"
>
@ -11634,9 +11610,6 @@ exports[`ConfigProvider components Comment normal 1`] = `
<div
class="ant-comment-content"
>
<div
class="ant-comment-content-author"
/>
<div
class="ant-comment-content-detail"
>
@ -11656,9 +11629,6 @@ exports[`ConfigProvider components Comment normal 1`] = `
<div
class="ant-comment-content"
>
<div
class="ant-comment-content-author"
/>
<div
class="ant-comment-content-detail"
>
@ -11681,9 +11651,6 @@ exports[`ConfigProvider components Comment prefixCls 1`] = `
<div
class="prefix-Comment-content"
>
<div
class="prefix-Comment-content-author"
/>
<div
class="prefix-Comment-content-detail"
>
@ -11703,9 +11670,6 @@ exports[`ConfigProvider components Comment prefixCls 1`] = `
<div
class="prefix-Comment-content"
>
<div
class="prefix-Comment-content-author"
/>
<div
class="prefix-Comment-content-detail"
>

View File

@ -55,6 +55,10 @@ Some components use dynamic style to support wave effect. You can config `csp` p
## FAQ
#### How to contribute a new language?
See [<Adding new language>](/docs/react/i18n#Adding-newplanguage).
#### Does the locale problem still exist in DatePicker even if ConfigProvider `locale` is used?
Please make sure you set moment locale or that you don't have two different versions of moment.

View File

@ -56,6 +56,10 @@ return (
## FAQ
#### 如何增加一个新的语言包?
参考[《增加语言包》](/docs/react/i18n#%E5%A2%9E%E5%8A%A0%E8%AF%AD%E8%A8%80%E5%8C%85)。
#### 为什么我使用了 ConfigProvider `locale`,时间类组件的国际化还有问题?
请检查是否正确设置了 moment 语言包,或者是否有两个版本的 moment 共存。

View File

@ -271,7 +271,9 @@ exports[`MonthPicker and WeekPicker render WeekPicker 1`] = `
>
<thead>
<tr>
<th />
<th
aria-label="empty cell"
/>
<th>
Su
</th>

View File

@ -1,12 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DropdownButton rtl render component should be rendered correctly in RTL direction 1`] = `
<span
class="ant-dropdown-trigger ant-dropdown-rtl"
/>
`;
exports[`DropdownButton rtl render component should be rendered correctly in RTL direction 2`] = `
<div
class="ant-btn-group ant-btn-group-rtl ant-dropdown-button"
>

View File

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dropdown overlay is function and has custom transitionName 1`] = `
Array [
<button
class="ant-dropdown-trigger"
type="button"
>
button
</button>,
<div>
<div
class="ant-dropdown"
style="opacity:0"
>
<div>
menu
</div>
</div>
</div>,
]
`;
exports[`Dropdown overlay is string 1`] = `
Array [
<button
class="ant-dropdown-trigger"
type="button"
>
button
</button>,
<div>
<div
class="ant-dropdown"
style="opacity:0"
>
<span>
overlayNode
</span>
</div>
</div>,
]
`;
exports[`Dropdown rtl render component should be rendered correctly in RTL direction 1`] = `
<span
class="ant-dropdown-trigger ant-dropdown-rtl"
/>
`;

View File

@ -6,18 +6,7 @@ import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
describe('DropdownButton', () => {
mountTest(() => (
<Dropdown menu={<Menu />}>
<span />
</Dropdown>
));
mountTest(Dropdown.Button);
rtlTest(() => (
<Dropdown menu={<Menu />}>
<span />
</Dropdown>
));
rtlTest(Dropdown.Button);
it('pass appropriate props to Dropdown', () => {

View File

@ -0,0 +1,38 @@
import React from 'react';
import { mount } from 'enzyme';
import Dropdown from '..';
import Menu from '../../menu';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
describe('Dropdown', () => {
mountTest(() => (
<Dropdown menu={<Menu />}>
<span />
</Dropdown>
));
rtlTest(() => (
<Dropdown menu={<Menu />}>
<span />
</Dropdown>
));
it('overlay is function and has custom transitionName', () => {
const wrapper = mount(
<Dropdown overlay={() => <div>menu</div>} transitionName="move-up" visible>
<button type="button">button</button>
</Dropdown>,
);
expect(wrapper).toMatchRenderedSnapshot();
});
it('overlay is string', () => {
const wrapper = mount(
<Dropdown overlay="string" visible>
<button type="button">button</button>
</Dropdown>,
);
expect(wrapper).toMatchRenderedSnapshot();
});
});

View File

@ -85,7 +85,9 @@ const Dropdown: DropdownInterface = props => {
} else {
overlayNode = overlay;
}
overlayNode = React.Children.only(overlayNode) as React.ReactElement<any>;
overlayNode = React.Children.only(
typeof overlayNode === 'string' ? <span>overlayNode</span> : overlayNode,
);
const overlayProps = overlayNode.props;
@ -108,7 +110,7 @@ const Dropdown: DropdownInterface = props => {
const fixedModeOverlay =
typeof overlayNode.type === 'string'
? overlay
? overlayNode
: cloneElement(overlayNode, {
mode: 'vertical',
selectable,

View File

@ -16,6 +16,11 @@
}
&-menu {
&-rtl {
direction: rtl;
text-align: right;
}
&-item-group-title {
.@{dropdown-prefix-cls}-rtl & {
direction: rtl;

View File

@ -71,7 +71,7 @@
// ==============================================================
&-label {
display: inline-block;
flex: none;
flex-grow: 0;
overflow: hidden;
white-space: nowrap;
text-align: right;

View File

@ -20,6 +20,10 @@
vertical-align: top;
}
> .@{form-item-prefix-cls}-label {
flex: none;
}
.@{form-prefix-cls}-text {
display: inline-block;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -101,6 +101,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
allowClear,
direction,
style,
readOnly,
} = this.props;
const suffixNode = this.renderSuffix(prefixCls);
if (!hasPrefixSuffix(this.props)) {
@ -118,6 +119,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
[`${prefixCls}-affix-wrapper-lg`]: size === 'large',
[`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value,
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-readonly`]: readOnly,
});
return (
<span

View File

@ -190,7 +190,7 @@ class Input extends React.Component<InputProps, InputState> {
onFocus: React.FocusEventHandler<HTMLInputElement> = e => {
const { onFocus } = this.props;
this.setState({ focused: true });
this.setState({ focused: true }, this.clearPasswordValueAttribute);
if (onFocus) {
onFocus(e);
}
@ -198,7 +198,7 @@ class Input extends React.Component<InputProps, InputState> {
onBlur: React.FocusEventHandler<HTMLInputElement> = e => {
const { onBlur } = this.props;
this.setState({ focused: false });
this.setState({ focused: false }, this.clearPasswordValueAttribute);
if (onBlur) {
onBlur(e);
}

View File

@ -70,6 +70,23 @@ describe('Input.Password', () => {
expect(wrapper.find('input').at('0').getDOMNode().getAttribute('value')).toBeFalsy();
});
// https://github.com/ant-design/ant-design/issues/24526
it('should not show value attribute in input element after blur it', async () => {
const wrapper = mount(<Input.Password />);
wrapper
.find('input')
.at('0')
.simulate('change', { target: { value: 'value' } });
await sleep();
expect(wrapper.find('input').at('0').getDOMNode().getAttribute('value')).toBeFalsy();
wrapper.find('input').at('0').simulate('blur');
await sleep();
expect(wrapper.find('input').at('0').getDOMNode().getAttribute('value')).toBeFalsy();
wrapper.find('input').at('0').simulate('focus');
await sleep();
expect(wrapper.find('input').at('0').getDOMNode().getAttribute('value')).toBeFalsy();
});
// https://github.com/ant-design/ant-design/issues/20541
it('could be unmount without errors', () => {
expect(() => {

View File

@ -259,11 +259,11 @@
}
// Fix https://github.com/ant-design/ant-design/issues/5754
&-lg .@{ant-prefix}-select-single .ant-select-selector {
&-lg .@{ant-prefix}-select-single .@{ant-prefix}-select-selector {
height: @input-height-lg;
}
&-sm .@{ant-prefix}-select-single .ant-select-selector {
&-sm .@{ant-prefix}-select-single .@{ant-prefix}-select-selector {
height: @input-height-sm;
}

View File

@ -58,12 +58,19 @@ export interface ListItemTypeProps extends React.FC<ListItemProps> {
Meta: typeof Meta;
}
const Item: ListItemTypeProps = props => {
const Item: ListItemTypeProps = ({
prefixCls: customizePrefixCls,
children,
actions,
extra,
className,
colStyle,
...others
}) => {
const { grid, itemLayout } = React.useContext(ListContext);
const { getPrefixCls } = React.useContext(ConfigContext);
const isItemContainsTextNodeAndNotSingular = () => {
const { children } = props;
let result;
React.Children.forEach(children, (element: React.ReactElement<any>) => {
if (typeof element === 'string') {
@ -74,22 +81,12 @@ const Item: ListItemTypeProps = props => {
};
const isFlexMode = () => {
const { extra } = props;
if (itemLayout === 'vertical') {
return !!extra;
}
return !isItemContainsTextNodeAndNotSingular();
};
const {
prefixCls: customizePrefixCls,
children,
actions,
extra,
className,
colStyle,
...others
} = props;
const prefixCls = getPrefixCls('list', customizePrefixCls);
const actionsContent = actions && actions.length > 0 && (
<ul className={`${prefixCls}-item-action`} key="actions">

View File

@ -128,4 +128,54 @@ describe('List Item Layout', () => {
);
expect(render(wrapper)).toMatchSnapshot();
});
it('rowKey could be string', () => {
const dataWithId = [
{
id: 1,
title: `ant design`,
},
{
id: 2,
title: `ant design`,
},
{
id: 3,
title: `ant design`,
},
];
const wrapper = mount(
<List
dataSource={dataWithId}
rowKey="id"
renderItem={item => <List.Item>{item.title}</List.Item>}
/>,
);
expect(wrapper).toMatchSnapshot();
});
it('rowKey could be function', () => {
const dataWithId = [
{
id: 1,
title: `ant design`,
},
{
id: 2,
title: `ant design`,
},
{
id: 3,
title: `ant design`,
},
];
const wrapper = mount(
<List
dataSource={dataWithId}
rowKey={item => item.id}
renderItem={item => <List.Item>{item.title}</List.Item>}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -57,6 +57,154 @@ exports[`List Item Layout horizontal itemLayout List should accept extra node 1`
</div>
`;
exports[`List Item Layout rowKey could be function 1`] = `
<List
dataSource={
Array [
Object {
"id": 1,
"title": "ant design",
},
Object {
"id": 2,
"title": "ant design",
},
Object {
"id": 3,
"title": "ant design",
},
]
}
renderItem={[Function]}
rowKey={[Function]}
>
<div
className="ant-list ant-list-split"
>
<Spin
size="default"
spinning={false}
wrapperClassName=""
>
<div
className="ant-spin-nested-loading"
>
<div
className="ant-spin-container"
key="container"
>
<ul
className="ant-list-items"
>
<Item
key="1/.0"
>
<li
className="ant-list-item"
>
ant design
</li>
</Item>
<Item
key="2/.1"
>
<li
className="ant-list-item"
>
ant design
</li>
</Item>
<Item
key="3/.2"
>
<li
className="ant-list-item"
>
ant design
</li>
</Item>
</ul>
</div>
</div>
</Spin>
</div>
</List>
`;
exports[`List Item Layout rowKey could be string 1`] = `
<List
dataSource={
Array [
Object {
"id": 1,
"title": "ant design",
},
Object {
"id": 2,
"title": "ant design",
},
Object {
"id": 3,
"title": "ant design",
},
]
}
renderItem={[Function]}
rowKey="id"
>
<div
className="ant-list ant-list-split"
>
<Spin
size="default"
spinning={false}
wrapperClassName=""
>
<div
className="ant-spin-nested-loading"
>
<div
className="ant-spin-container"
key="container"
>
<ul
className="ant-list-items"
>
<Item
key="1/.0"
>
<li
className="ant-list-item"
>
ant design
</li>
</Item>
<Item
key="2/.1"
>
<li
className="ant-list-item"
>
ant design
</li>
</Item>
<Item
key="3/.2"
>
<li
className="ant-list-item"
>
ant design
</li>
</Item>
</ul>
</div>
</div>
</Spin>
</div>
</List>
`;
exports[`List Item Layout should render in RTL direction 1`] = `
<div
class="ant-list ant-list-split ant-list-rtl"

View File

@ -245,10 +245,17 @@ function List<T>({
if (splitDataSource.length > 0) {
const items = splitDataSource.map((item: any, index: number) => renderInnerItem(item, index));
const childrenList = React.Children.map(items, (child: any, index) =>
cloneElement(child, {
key: keys[index],
colStyle,
}),
cloneElement(
child,
grid
? {
key: keys[index],
colStyle,
}
: {
key: keys[index],
},
),
);
childrenContent = grid ? (
<Row gutter={grid.gutter}>{childrenList}</Row>

View File

@ -1,9 +1,12 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/en_GB';
import DatePicker from '../date-picker/locale/en_GB';
import TimePicker from '../time-picker/locale/en_GB';
import Calendar from '../calendar/locale/en_GB';
import { Locale } from '../locale-provider';
const typeTemplate = '${label} is not a valid ${type}';
const localeValues: Locale = {
locale: 'en-gb',
Pagination,
@ -41,6 +44,55 @@ const localeValues: Locale = {
Empty: {
description: 'No data',
},
Form: {
defaultValidateMessages: {
default: 'Field validation error ${label}',
required: 'Please enter ${label}',
enum: '${label} must be one of [${enum}]',
whitespace: '${label} cannot be a blank character',
date: {
format: '${label} date format is invalid',
parse: '${label} cannot be converted to a date',
invalid: '${label} is an invalid date',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label} must be ${len} characters',
min: '${label} at least ${min} characters',
max: '${label} up to ${max} characters',
range: '${label} must be between ${min}-${max} characters',
},
number: {
len: '${label} must be equal to ${len}',
min: '${label} minimum value is ${min}',
max: '${label} maximum value is ${max}',
range: '${label} must be between ${min}-${max}',
},
array: {
len: 'Must be ${len} ${label}',
min: 'At least ${min} ${label}',
max: 'At most ${max} ${label}',
range: 'The amount of ${label} must be between ${min}-${max}',
},
pattern: {
mismatch: '${label} does not match the pattern ${pattern}',
},
},
},
};
export default localeValues;

View File

@ -42,6 +42,12 @@ const localeValues: Locale = {
Empty: {
description: 'Ei kohteita',
},
Text: {
edit: 'Muokkaa',
copy: 'Kopioi',
copied: 'Kopioitu',
expand: 'Näytä lisää',
},
};
export default localeValues;

View File

@ -1,21 +1,35 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/pt_BR';
import DatePicker from '../date-picker/locale/pt_BR';
import TimePicker from '../time-picker/locale/pt_BR';
import Calendar from '../calendar/locale/pt_BR';
import { Locale } from '../locale-provider';
const typeTemplate = '${label} não é um ${type} válido';
const localeValues: Locale = {
locale: 'pt-br',
Pagination,
DatePicker,
TimePicker,
Calendar,
global: {
placeholder: 'Por favor escolha',
},
Table: {
filterTitle: 'Filtro',
filterTitle: 'Menu de Filtro',
filterConfirm: 'OK',
filterReset: 'Resetar',
selectAll: 'Selecionar página atual',
selectInvert: 'Inverter seleção',
selectionAll: 'Selecionar todo o conteúdo',
sortTitle: 'Ordenar título',
expand: 'Expandir linha',
collapse: 'Colapsar linha',
triggerDesc: 'Clique organiza por descendente',
triggerAsc: 'Clique organiza por ascendente',
cancelSort: 'Clique para cancelar organização',
},
Modal: {
okText: 'OK',
@ -41,12 +55,67 @@ const localeValues: Locale = {
Empty: {
description: 'Não há dados',
},
Icon: {
icon: 'ícone',
},
Text: {
edit: 'editar',
copy: 'copiar',
copied: 'copiado',
expand: 'expandir',
},
PageHeader: {
back: 'Retornar',
},
Form: {
defaultValidateMessages: {
default: 'Erro ${label} na validação de campo',
required: 'Por favor, insira ${label}',
enum: '${label} deve ser um dos seguinte: [${enum}]',
whitespace: '${label} não pode ser um caractér vazio',
date: {
format: ' O formato de data ${label} é inválido',
parse: '${label} não pode ser convertido para uma data',
invalid: '${label} é uma data inválida',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label} deve possuir ${len} caracteres',
min: '${label} deve possuir ao menos ${min} caracteres',
max: '${label} deve possuir no máximo ${max} caracteres',
range: '${label} deve possuir entre ${min} e ${max} caracteres',
},
number: {
len: '${label} deve ser igual à ${len}',
min: 'O valor mínimo de ${label} é ${min}',
max: 'O valor máximo de ${label} é ${max}',
range: '${label} deve estar entre ${min} e ${max}',
},
array: {
len: 'Deve ser ${len} ${label}',
min: 'No mínimo ${min} ${label}',
max: 'No máximo ${max} ${label}',
range: 'A quantidade de ${label} deve estar entre ${min} e ${max}',
},
pattern: {
mismatch: '${label} não se encaixa no padrão ${pattern}',
},
},
},
};
export default localeValues;

View File

@ -74,7 +74,7 @@ describe('Mentions', () => {
expect(onBlur).toHaveBeenCalled();
});
focusTest(Mentions);
focusTest(Mentions, { refFocus: true });
mountTest(Mentions);
rtlTest(Mentions);

View File

@ -1,12 +1,12 @@
import classNames from 'classnames';
import omit from 'omit.js';
import * as React from 'react';
import classNames from 'classnames';
import RcMentions from 'rc-mentions';
import { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions';
import Spin from '../spin';
import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider';
import { ConfigContext } from '../config-provider';
import { composeRef } from '../_util/ref';
const { Option } = RcMentions;
export const { Option } = RcMentions;
function loadingFilterOption() {
return true;
@ -38,75 +38,54 @@ interface MentionsEntity {
value: string;
}
class Mentions extends React.Component<MentionProps, MentionState> {
static Option = Option;
interface CompoundedComponent
extends React.ForwardRefExoticComponent<MentionProps & React.RefAttributes<HTMLElement>> {
Option: typeof Option;
getMentions: (value: string, config?: MentionsConfig) => MentionsEntity[];
}
static getMentions = (value: string = '', config?: MentionsConfig): MentionsEntity[] => {
const { prefix = '@', split = ' ' } = config || {};
const prefixList: string[] = Array.isArray(prefix) ? prefix : [prefix];
const InternalMentions: React.ForwardRefRenderFunction<unknown, MentionProps> = (
{
prefixCls: customizePrefixCls,
className,
disabled,
loading,
filterOption,
children,
notFoundContent,
...restProps
},
ref,
) => {
const [focused, setFocused] = React.useState(false);
const innerRef = React.useRef<HTMLElement>();
const mergedRef = composeRef(ref, innerRef);
const { getPrefixCls, renderEmpty, direction } = React.useContext(ConfigContext);
return value
.split(split)
.map((str = ''): MentionsEntity | null => {
let hitPrefix: string | null = null;
prefixList.some(prefixStr => {
const startStr = str.slice(0, prefixStr.length);
if (startStr === prefixStr) {
hitPrefix = prefixStr;
return true;
}
return false;
});
if (hitPrefix !== null) {
return {
prefix: hitPrefix,
value: str.slice(hitPrefix!.length),
};
}
return null;
})
.filter((entity): entity is MentionsEntity => !!entity && !!entity.value);
};
state = {
focused: false,
};
private rcMentions: any;
onFocus: React.FocusEventHandler<HTMLTextAreaElement> = (...args) => {
const { onFocus } = this.props;
if (onFocus) {
onFocus(...args);
const onFocus: React.FocusEventHandler<HTMLTextAreaElement> = (...args) => {
if (restProps.onFocus) {
restProps.onFocus(...args);
}
this.setState({
focused: true,
});
setFocused(true);
};
onBlur: React.FocusEventHandler<HTMLTextAreaElement> = (...args) => {
const { onBlur } = this.props;
if (onBlur) {
onBlur(...args);
const onBlur: React.FocusEventHandler<HTMLTextAreaElement> = (...args) => {
if (restProps.onBlur) {
restProps.onBlur(...args);
}
this.setState({
focused: false,
});
setFocused(false);
};
getNotFoundContent(renderEmpty: RenderEmptyHandler) {
const { notFoundContent } = this.props;
const getNotFoundContent = () => {
if (notFoundContent !== undefined) {
return notFoundContent;
}
return renderEmpty('Select');
}
};
getOptions = () => {
const { children, loading } = this.props;
const getOptions = () => {
if (loading) {
return (
<Option value="ANTD_SEARCHING" disabled>
@ -118,59 +97,70 @@ class Mentions extends React.Component<MentionProps, MentionState> {
return children;
};
getFilterOption = (): any => {
const { filterOption, loading } = this.props;
const getFilterOption = (): any => {
if (loading) {
return loadingFilterOption;
}
return filterOption;
};
saveMentions = (node: typeof RcMentions) => {
this.rcMentions = node;
};
const prefixCls = getPrefixCls('mentions', customizePrefixCls);
focus() {
this.rcMentions.focus();
}
const mergedClassName = classNames(className, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-focused`]: focused,
[`${prefixCls}-rtl`]: direction === 'rtl',
});
blur() {
this.rcMentions.blur();
}
return (
<RcMentions
prefixCls={prefixCls}
notFoundContent={getNotFoundContent()}
className={mergedClassName}
disabled={disabled}
direction={direction}
{...restProps}
filterOption={getFilterOption()}
onFocus={onFocus}
onBlur={onBlur}
ref={mergedRef as any}
>
{getOptions()}
</RcMentions>
);
};
renderMentions = ({ getPrefixCls, renderEmpty, direction }: ConfigConsumerProps) => {
const { focused } = this.state;
const { prefixCls: customizePrefixCls, className, disabled, ...restProps } = this.props;
const prefixCls = getPrefixCls('mentions', customizePrefixCls);
const mentionsProps = omit(restProps, ['loading']);
const Mentions = React.forwardRef<unknown, MentionProps>(InternalMentions) as CompoundedComponent;
Mentions.displayName = 'Mentions';
Mentions.Option = Option;
const mergedClassName = classNames(className, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-focused`]: focused,
[`${prefixCls}-rtl`]: direction === 'rtl',
});
Mentions.getMentions = (value: string = '', config?: MentionsConfig): MentionsEntity[] => {
const { prefix = '@', split = ' ' } = config || {};
const prefixList: string[] = Array.isArray(prefix) ? prefix : [prefix];
return (
<RcMentions
prefixCls={prefixCls}
notFoundContent={this.getNotFoundContent(renderEmpty)}
className={mergedClassName}
disabled={disabled}
direction={direction}
{...mentionsProps}
filterOption={this.getFilterOption()}
onFocus={this.onFocus}
onBlur={this.onBlur}
ref={this.saveMentions}
>
{this.getOptions()}
</RcMentions>
);
};
return value
.split(split)
.map((str = ''): MentionsEntity | null => {
let hitPrefix: string | null = null;
render() {
return <ConfigConsumer>{this.renderMentions}</ConfigConsumer>;
}
}
prefixList.some(prefixStr => {
const startStr = str.slice(0, prefixStr.length);
if (startStr === prefixStr) {
hitPrefix = prefixStr;
return true;
}
return false;
});
if (hitPrefix !== null) {
return {
prefix: hitPrefix,
value: str.slice(hitPrefix!.length),
};
}
return null;
})
.filter((entity): entity is MentionsEntity => !!entity && !!entity.value);
};
export default Mentions;

View File

@ -40,11 +40,16 @@ export default class MenuItem extends React.Component<MenuItemProps> {
this.menuItem = menuItem;
};
renderItemChildren() {
const { icon, children } = this.props;
renderItemChildren(inlineCollapsed: boolean) {
const { icon, children, level, rootPrefixCls } = this.props;
// inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span
// ref: https://github.com/ant-design/ant-design/pull/23456
if (!icon || (isValidElement(children) && children.type === 'span')) {
if (children && inlineCollapsed && level === 1 && typeof children === 'string') {
return (
<div className={`${rootPrefixCls}-inline-collapsed-noicon`}>{children.charAt(0)}</div>
);
}
return children;
}
return <span>{children}</span>;
@ -91,7 +96,7 @@ export default class MenuItem extends React.Component<MenuItemProps> {
ref={this.saveMenuItem}
>
{icon}
{this.renderItemChildren()}
{this.renderItemChildren(inlineCollapsed)}
</Item>
</Tooltip>
);

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { SubMenu as RcSubMenu } from 'rc-menu';
import classNames from 'classnames';
import omit from 'omit.js';
@ -15,6 +14,7 @@ export interface SubMenuProps {
rootPrefixCls?: string;
className?: string;
disabled?: boolean;
level?: number;
title?: React.ReactNode;
icon?: React.ReactNode;
style?: React.CSSProperties;
@ -26,9 +26,7 @@ export interface SubMenuProps {
}
class SubMenu extends React.Component<SubMenuProps, any> {
static contextTypes = {
antdMenuTheme: PropTypes.string,
};
static contextType = MenuContext;
// fix issue:https://github.com/ant-design/ant-design/issues/8666
static isSubMenu = 1;
@ -43,10 +41,14 @@ class SubMenu extends React.Component<SubMenuProps, any> {
this.subMenu = subMenu;
};
renderTitle() {
const { icon, title } = this.props;
renderTitle(inlineCollapsed: boolean) {
const { icon, title, level, rootPrefixCls } = this.props;
if (!icon) {
return title;
return inlineCollapsed && level === 1 && title && typeof title === 'string' ? (
<div className={`${rootPrefixCls}-inline-collapsed-noicon`}>{title.charAt(0)}</div>
) : (
title
);
}
// inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span
// ref: https://github.com/ant-design/ant-design/pull/23456
@ -63,10 +65,10 @@ class SubMenu extends React.Component<SubMenuProps, any> {
const { rootPrefixCls, popupClassName } = this.props;
return (
<MenuContext.Consumer>
{({ antdMenuTheme }: MenuContextProps) => (
{({ inlineCollapsed, antdMenuTheme }: MenuContextProps) => (
<RcSubMenu
{...omit(this.props, ['icon'])}
title={this.renderTitle()}
title={this.renderTitle(inlineCollapsed)}
ref={this.saveSubMenu}
popupClassName={classNames(
rootPrefixCls,

View File

@ -516,6 +516,16 @@
box-shadow: none;
}
&-root&-inline-collapsed {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu .@{menu-prefix-cls}-submenu-title {
> .@{menu-prefix-cls}-inline-collapsed-noicon {
font-size: @menu-icon-size-lg;
text-align: center;
}
}
}
&-sub&-inline {
padding: 0;
border: 0;

View File

@ -1,37 +0,0 @@
---
order: 5
title:
zh-CN: 确认对话框
en-US: Confirmation modal dialog
---
## zh-CN
使用 `confirm()` 可以快捷地弹出确认框。onCancel/onOk 返回 promise 可以延迟关闭。
## en-US
Use `confirm()` to show a confirmation modal dialog. Let onCancel/onOk function return a promise object to delay closing the dialog.
```jsx
import { Modal, Button } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons';
const { confirm } = Modal;
function showConfirm() {
confirm({
title: 'Do you want to delete these items?',
icon: <ExclamationCircleOutlined />,
content: 'When clicked the OK button, this dialog will be closed after 1 second',
onOk() {
return new Promise((resolve, reject) => {
setTimeout(Math.random() > 0.5 ? resolve : reject, 1000);
}).catch(() => console.log('Oops errors!'));
},
onCancel() {},
});
}
ReactDOM.render(<Button onClick={showConfirm}>Confirm</Button>, mountNode);
```

View File

@ -7,11 +7,11 @@ title:
## zh-CN
使用 `confirm()` 可以快捷地弹出确认框。
使用 `confirm()` 可以快捷地弹出确认框。onCancel/onOk 返回 promise 可以延迟关闭。
## en-US
Use `confirm()` to show a confirmation modal dialog.
Use `confirm()` to show a confirmation modal dialog. Let onCancel/onOk function return a promise object to delay closing the dialog.
```jsx
import { Modal, Button, Space } from 'antd';
@ -33,6 +33,20 @@ function showConfirm() {
});
}
function showPromiseConfirm() {
confirm({
title: 'Do you want to delete these items?',
icon: <ExclamationCircleOutlined />,
content: 'When clicked the OK button, this dialog will be closed after 1 second',
onOk() {
return new Promise((resolve, reject) => {
setTimeout(Math.random() > 0.5 ? resolve : reject, 1000);
}).catch(() => console.log('Oops errors!'));
},
onCancel() {},
});
}
function showDeleteConfirm() {
confirm({
title: 'Are you sure delete this task?',
@ -73,6 +87,7 @@ function showPropsConfirm() {
ReactDOM.render(
<Space>
<Button onClick={showConfirm}>Confirm</Button>
<Button onClick={showPromiseConfirm}>With promise</Button>
<Button onClick={showDeleteConfirm} type="dashed">
Delete
</Button>

View File

@ -1,42 +1,36 @@
import * as React from 'react';
import classNames from 'classnames';
import { ProgressProps, ProgressSize } from './progress';
interface StepsProps extends ProgressProps {
steps: number;
size?: ProgressSize;
strokeColor?: string;
}
const Steps: React.FC<StepsProps> = props => {
const {
size = 'default',
steps,
percent = 0,
strokeWidth = 8,
strokeColor,
prefixCls,
children,
} = props;
const getStyledSteps = () => {
const current = Math.floor(steps * (percent / 100));
const stepWidth = size === 'small' ? 2 : 14;
const styleSteps = [];
for (let i = 0; i < steps; i++) {
let color;
if (i <= current - 1) {
color = strokeColor;
}
const stepStyle = {
backgroundColor: `${color}`,
width: `${stepWidth}px`,
height: `${strokeWidth}px`,
};
styleSteps.push(<div key={i} className={`${prefixCls}-steps-item`} style={stepStyle} />);
}
return styleSteps;
};
const { size, steps, percent = 0, strokeWidth = 8, strokeColor, prefixCls, children } = props;
const current = Math.floor(steps * (percent / 100));
const stepWidth = size === 'small' ? 2 : 14;
const styledSteps = [];
for (let i = 0; i < steps; i += 1) {
styledSteps.push(
<div
key={i}
className={classNames(`${prefixCls}-steps-item`, {
[`${prefixCls}-steps-item-active`]: i <= current - 1,
})}
style={{
backgroundColor: i <= current - 1 ? strokeColor : undefined,
width: stepWidth,
height: strokeWidth,
}}
/>,
);
}
return (
<div className={`${prefixCls}-steps-outer`}>
{getStyledSteps()}
{styledSteps}
{children}
</div>
);

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/progress/demo/circle.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -43,7 +43,7 @@ exports[`renders ./components/progress/demo/circle.md correctly 1`] = `
75%
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-exception ant-progress-show-info ant-progress-default"
>
@ -103,7 +103,7 @@ exports[`renders ./components/progress/demo/circle.md correctly 1`] = `
</span>
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-success ant-progress-show-info ant-progress-default"
>
@ -163,12 +163,12 @@ exports[`renders ./components/progress/demo/circle.md correctly 1`] = `
</span>
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/circle-dynamic.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -210,7 +210,7 @@ exports[`renders ./components/progress/demo/circle-dynamic.md correctly 1`] = `
0%
</span>
</div>
</div>
</div>,
<div
class="ant-btn-group"
>
@ -268,12 +268,12 @@ exports[`renders ./components/progress/demo/circle-dynamic.md correctly 1`] = `
</svg>
</span>
</button>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/circle-mini.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -315,7 +315,7 @@ exports[`renders ./components/progress/demo/circle-mini.md correctly 1`] = `
30%
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-exception ant-progress-show-info ant-progress-default"
>
@ -375,7 +375,7 @@ exports[`renders ./components/progress/demo/circle-mini.md correctly 1`] = `
</span>
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-success ant-progress-show-info ant-progress-default"
>
@ -435,12 +435,12 @@ exports[`renders ./components/progress/demo/circle-mini.md correctly 1`] = `
</span>
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/dashboard.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -482,7 +482,7 @@ exports[`renders ./components/progress/demo/dashboard.md correctly 1`] = `
75%
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -524,12 +524,12 @@ exports[`renders ./components/progress/demo/dashboard.md correctly 1`] = `
75%
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/dynamic.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -551,7 +551,7 @@ exports[`renders ./components/progress/demo/dynamic.md correctly 1`] = `
>
0%
</span>
</div>
</div>,
<div
class="ant-btn-group"
>
@ -609,12 +609,12 @@ exports[`renders ./components/progress/demo/dynamic.md correctly 1`] = `
</svg>
</span>
</button>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/format.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -656,7 +656,7 @@ exports[`renders ./components/progress/demo/format.md correctly 1`] = `
75 Days
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-success ant-progress-show-info ant-progress-default"
>
@ -698,12 +698,12 @@ exports[`renders ./components/progress/demo/format.md correctly 1`] = `
Done
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/gradient-line.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -725,7 +725,7 @@ exports[`renders ./components/progress/demo/gradient-line.md correctly 1`] = `
>
99.9%
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-line ant-progress-status-active ant-progress-show-info ant-progress-default"
>
@ -747,7 +747,7 @@ exports[`renders ./components/progress/demo/gradient-line.md correctly 1`] = `
>
99.9%
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -807,7 +807,7 @@ exports[`renders ./components/progress/demo/gradient-line.md correctly 1`] = `
90%
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-success ant-progress-show-info ant-progress-default"
>
@ -885,12 +885,12 @@ exports[`renders ./components/progress/demo/gradient-line.md correctly 1`] = `
</span>
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/line.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -912,7 +912,7 @@ exports[`renders ./components/progress/demo/line.md correctly 1`] = `
>
30%
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-line ant-progress-status-active ant-progress-show-info ant-progress-default"
>
@ -934,7 +934,7 @@ exports[`renders ./components/progress/demo/line.md correctly 1`] = `
>
50%
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-line ant-progress-status-exception ant-progress-show-info ant-progress-default"
>
@ -974,7 +974,7 @@ exports[`renders ./components/progress/demo/line.md correctly 1`] = `
</svg>
</span>
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-line ant-progress-status-success ant-progress-show-info ant-progress-default"
>
@ -1014,7 +1014,7 @@ exports[`renders ./components/progress/demo/line.md correctly 1`] = `
</svg>
</span>
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-default"
>
@ -1030,8 +1030,8 @@ exports[`renders ./components/progress/demo/line.md correctly 1`] = `
/>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/line-mini.md correctly 1`] = `
@ -1166,7 +1166,7 @@ exports[`renders ./components/progress/demo/line-mini.md correctly 1`] = `
`;
exports[`renders ./components/progress/demo/linecap.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1188,7 +1188,7 @@ exports[`renders ./components/progress/demo/linecap.md correctly 1`] = `
>
75%
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1230,7 +1230,7 @@ exports[`renders ./components/progress/demo/linecap.md correctly 1`] = `
75%
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1272,12 +1272,12 @@ exports[`renders ./components/progress/demo/linecap.md correctly 1`] = `
75%
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/segment.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1303,7 +1303,7 @@ exports[`renders ./components/progress/demo/segment.md correctly 1`] = `
>
60%
</span>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1357,7 +1357,7 @@ exports[`renders ./components/progress/demo/segment.md correctly 1`] = `
60%
</span>
</div>
</div>
</div>,
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1411,12 +1411,12 @@ exports[`renders ./components/progress/demo/segment.md correctly 1`] = `
60%
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
<div>
Array [
<div
class="ant-progress ant-progress-steps ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1424,16 +1424,16 @@ exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
class="ant-progress-steps-outer"
>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:14px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:undefined;width:14px;height:8px"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:undefined;width:14px;height:8px"
style="width:14px;height:8px"
/>
<span
class="ant-progress-text"
@ -1442,8 +1442,8 @@ exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
50%
</span>
</div>
</div>
<br />
</div>,
<br />,
<div
class="ant-progress ant-progress-steps ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
@ -1451,24 +1451,24 @@ exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
class="ant-progress-steps-outer"
>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:14px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:undefined;width:14px;height:8px"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:undefined;width:14px;height:8px"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:undefined;width:14px;height:8px"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:undefined;width:14px;height:8px"
style="width:14px;height:8px"
/>
<span
class="ant-progress-text"
@ -1477,8 +1477,8 @@ exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
30%
</span>
</div>
</div>
<br />
</div>,
<br />,
<div
class="ant-progress ant-progress-steps ant-progress-status-success ant-progress-show-info ant-progress-small"
>
@ -1486,24 +1486,24 @@ exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
class="ant-progress-steps-outer"
>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:2px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="background-color:#52c41a;width:2px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:2px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="background-color:#52c41a;width:2px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:2px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="background-color:#52c41a;width:2px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:2px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="background-color:#52c41a;width:2px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="background-color:#1890ff;width:2px;height:8px"
class="ant-progress-steps-item ant-progress-steps-item-active"
style="background-color:#52c41a;width:2px;height:8px"
/>
<span
class="ant-progress-text"
@ -1530,6 +1530,6 @@ exports[`renders ./components/progress/demo/steps.md correctly 1`] = `
</span>
</span>
</div>
</div>
</div>
</div>,
]
`;

View File

@ -550,3 +550,32 @@ exports[`Progress rtl render component should be rendered correctly in RTL direc
</span>
</div>
`;
exports[`Progress should support steps 1`] = `
<div
class="ant-progress ant-progress-steps ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
<div
class="ant-progress-steps-outer"
>
<div
class="ant-progress-steps-item"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="width:14px;height:8px"
/>
<div
class="ant-progress-steps-item"
style="width:14px;height:8px"
/>
<span
class="ant-progress-text"
title="0%"
>
0%
</span>
</div>
</div>
`;

View File

@ -133,4 +133,30 @@ describe('Progress', () => {
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1);
errorSpy.mockRestore();
});
it('should support steps', () => {
const wrapper = mount(<Progress steps={3} />);
expect(wrapper).toMatchRenderedSnapshot();
});
it('steps should be changable', () => {
const wrapper = mount(<Progress steps={5} percent={60} />);
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(3);
wrapper.setProps({ percent: 40 });
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(2);
});
it('steps should be changable when has strokeColor', () => {
const wrapper = mount(<Progress steps={5} percent={60} strokeColor="#1890ff" />);
expect(wrapper.find('.ant-progress-steps-item').at(0).getDOMNode().style.backgroundColor).toBe(
'rgb(24, 144, 255)',
);
wrapper.setProps({ percent: 40 });
expect(wrapper.find('.ant-progress-steps-item').at(2).getDOMNode().style.backgroundColor).toBe(
'',
);
expect(wrapper.find('.ant-progress-steps-item').at(1).getDOMNode().style.backgroundColor).toBe(
'rgb(24, 144, 255)',
);
});
});

View File

@ -17,8 +17,6 @@ A dynamic progress bar is better.
import { Progress, Button } from 'antd';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
const ButtonGroup = Button.Group;
class App extends React.Component {
state = {
percent: 0,
@ -42,13 +40,13 @@ class App extends React.Component {
render() {
return (
<div>
<>
<Progress type="circle" percent={this.state.percent} />
<ButtonGroup>
<Button.Group>
<Button onClick={this.decline} icon={<MinusOutlined />} />
<Button onClick={this.increase} icon={<PlusOutlined />} />
</ButtonGroup>
</div>
</Button.Group>
</>
);
}
}

View File

@ -17,11 +17,11 @@ A smaller circular progress bar.
import { Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Progress type="circle" percent={30} width={80} />
<Progress type="circle" percent={70} width={80} status="exception" />
<Progress type="circle" percent={100} width={80} />
</div>,
</>,
mountNode,
);
```

View File

@ -17,11 +17,11 @@ A circular progress bar.
import { Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Progress type="circle" percent={75} />
<Progress type="circle" percent={70} status="exception" />
<Progress type="circle" percent={100} />
</div>,
</>,
mountNode,
);
```

View File

@ -17,9 +17,10 @@ By setting `type=dashboard`, you can get a dashboard style of progress easily. M
import { Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Progress type="dashboard" percent={75} />
<Progress type="dashboard" percent={75} gapDegree={30} />
</div>
, mountNode);
</>,
mountNode,
);
```

View File

@ -17,8 +17,6 @@ A dynamic progress bar is better.
import { Progress, Button } from 'antd';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
const ButtonGroup = Button.Group;
class App extends React.Component {
state = {
percent: 0,
@ -42,13 +40,13 @@ class App extends React.Component {
render() {
return (
<div>
<>
<Progress percent={this.state.percent} />
<ButtonGroup>
<Button.Group>
<Button onClick={this.decline} icon={<MinusOutlined />} />
<Button onClick={this.increase} icon={<PlusOutlined />} />
</ButtonGroup>
</div>
</Button.Group>
</>
);
}
}

View File

@ -17,10 +17,10 @@ You can set a custom text by setting the `format` prop.
import { Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Progress type="circle" percent={75} format={percent => `${percent} Days`} />
<Progress type="circle" percent={100} format={() => 'Done'} />
</div>,
</>,
mountNode,
);
```

View File

@ -17,7 +17,7 @@ A package of `linear-gradient`. It is recommended to only pass two colors.
import { Progress } from 'antd';
const Demo = () => (
<div>
<>
<Progress
strokeColor={{
'0%': '#108ee9',
@ -49,7 +49,7 @@ const Demo = () => (
}}
percent={100}
/>
</div>
</>
);
ReactDOM.render(<Demo />, mountNode);

View File

@ -17,13 +17,13 @@ A standard progress bar.
import { Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Progress percent={30} />
<Progress percent={50} status="active" />
<Progress percent={70} status="exception" />
<Progress percent={100} />
<Progress percent={50} showInfo={false} />
</div>,
</>,
mountNode,
);
```

View File

@ -17,11 +17,11 @@ By setting `strokeLinecap="square"`, you can change the linecaps from round to s
import { Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Progress strokeLinecap="square" percent={75} />
<Progress strokeLinecap="square" type="circle" percent={75} />
<Progress strokeLinecap="square" type="dashboard" percent={75} />
</div>,
</>,
mountNode,
);
```

View File

@ -17,7 +17,7 @@ A standard progress bar. Doesn't support trail color when `type="circle|dashboar
import { Tooltip, Progress } from 'antd';
ReactDOM.render(
<div>
<>
<Tooltip title="3 done / 3 in progress / 4 to do">
<Progress percent={60} successPercent={30} />
</Tooltip>
@ -29,7 +29,7 @@ ReactDOM.render(
<Tooltip title="3 done / 3 in progress / 4 to do">
<Progress percent={60} successPercent={30} type="dashboard" />
</Tooltip>
</div>,
</>,
mountNode,
);
```

View File

@ -17,13 +17,13 @@ A progress bar with steps.
import { Progress } from 'antd';
ReactDOM.render(
<div>
<Progress percent={50} steps={3} strokeColor="#1890ff" />
<>
<Progress percent={50} steps={3} />
<br />
<Progress percent={30} steps={5} strokeColor="#1890ff" />
<Progress percent={30} steps={5} />
<br />
<Progress percent={100} steps={5} size="small" strokeColor="#1890ff" />
</div>,
<Progress percent={100} steps={5} size="small" strokeColor="#52c41a" />
</>,
mountNode,
);
```

View File

@ -99,6 +99,7 @@ export default class Progress extends React.Component<ProgressProps> {
type,
steps,
showInfo,
strokeColor,
...restProps
} = props;
const prefixCls = getPrefixCls('progress', customizePrefixCls);
@ -108,7 +109,12 @@ export default class Progress extends React.Component<ProgressProps> {
// Render progress shape
if (type === 'line') {
progress = steps ? (
<Steps {...this.props} prefixCls={prefixCls} steps={steps}>
<Steps
{...this.props}
strokeColor={typeof strokeColor === 'string' ? strokeColor : undefined}
prefixCls={prefixCls}
steps={steps}
>
{progressInfo}
</Steps>
) : (

View File

@ -26,6 +26,11 @@
min-width: 2px;
margin-right: 2px;
background: @progress-steps-item-bg;
transition: all 0.3s;
&-active {
background: @progress-default-color;
}
}
}

View File

@ -35,16 +35,10 @@ describe('Radio Group', () => {
</RadioGroup>,
);
wrapper
.find('div')
.at(0)
.simulate('mouseenter');
wrapper.find('div').at(0).simulate('mouseenter');
expect(onMouseEnter).toHaveBeenCalled();
wrapper
.find('div')
.at(0)
.simulate('mouseleave');
wrapper.find('div').at(0).simulate('mouseleave');
expect(onMouseLeave).toHaveBeenCalled();
});
@ -58,15 +52,10 @@ describe('Radio Group', () => {
);
const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls.length).toBe(1);
});
it('both of radio and radioGroup will trigger onchange event when they exists', () => {
@ -88,16 +77,10 @@ describe('Radio Group', () => {
);
const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
expect(onChangeRadioGroup.mock.calls.length).toBe(1);
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls.length).toBe(1);
});
it('Trigger onChange when both of radioButton and radioGroup exists', () => {
@ -112,15 +95,10 @@ describe('Radio Group', () => {
);
const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls.length).toBe(1);
});
it('should only trigger once when in group with options', () => {
@ -142,11 +120,6 @@ describe('Radio Group', () => {
);
const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(0);
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(0).simulate('change');
@ -186,18 +159,23 @@ describe('Radio Group', () => {
const wrapper = mount(
<RadioGroup defaultValue="bamboo" value={undefined} options={options} />,
);
expect(wrapper.state().value).toEqual('bamboo');
expect(wrapper.find('.ant-radio-wrapper').at(0).hasClass('ant-radio-wrapper-checked')).toBe(
true,
);
});
[undefined, null].forEach(newValue => {
it(`should set value back when value change back to ${newValue}`, () => {
const options = [{ label: 'Bamboo', value: 'bamboo' }];
const wrapper = mount(<RadioGroup value="bamboo" options={options} />);
expect(wrapper.state().value).toEqual('bamboo');
expect(wrapper.find('.ant-radio-wrapper').at(0).hasClass('ant-radio-wrapper-checked')).toBe(
true,
);
wrapper.setProps({ value: newValue });
expect(wrapper.state().value).toEqual(newValue);
wrapper.update();
expect(wrapper.find('.ant-radio-wrapper').at(0).hasClass('ant-radio-wrapper-checked')).toBe(
false,
);
});
});
});

View File

@ -6,7 +6,7 @@ import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
describe('Radio', () => {
focusTest(Radio);
focusTest(Radio, { refFocus: true });
mountTest(Radio);
mountTest(Group);
mountTest(Button);

View File

@ -1,107 +1,72 @@
import * as React from 'react';
import classNames from 'classnames';
import Radio from './radio';
import {
RadioGroupProps,
RadioGroupState,
RadioChangeEvent,
RadioGroupButtonStyle,
} from './interface';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { RadioGroupProps, RadioChangeEvent, RadioGroupButtonStyle } from './interface';
import { ConfigContext } from '../config-provider';
import SizeContext from '../config-provider/SizeContext';
import { RadioGroupContextProvider } from './context';
function getCheckedValue(children: React.ReactNode) {
let value = null;
let matched = false;
React.Children.forEach(children, (radio: any) => {
if (radio && radio.props && radio.props.checked) {
value = radio.props.value;
matched = true;
}
});
return matched ? { value } : undefined;
}
const RadioGroup: React.FC<RadioGroupProps> = props => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const size = React.useContext(SizeContext);
class RadioGroup extends React.PureComponent<RadioGroupProps, RadioGroupState> {
static defaultProps = {
buttonStyle: 'outline' as RadioGroupButtonStyle,
};
static getDerivedStateFromProps(nextProps: RadioGroupProps, prevState: RadioGroupState) {
const newState: Partial<RadioGroupState> = {
prevPropValue: nextProps.value,
};
if (nextProps.value !== undefined || prevState.prevPropValue !== nextProps.value) {
newState.value = nextProps.value;
} else {
const checkedValue = getCheckedValue(nextProps.children);
if (checkedValue) {
newState.value = checkedValue.value;
}
}
return newState;
let initValue;
if (props.value !== undefined) {
initValue = props.value;
} else if (props.defaultValue !== undefined) {
initValue = props.defaultValue;
}
const [value, setValue] = React.useState(initValue);
const [prevPropValue, setPrevPropValue] = React.useState(props.value);
constructor(props: RadioGroupProps) {
super(props);
let value;
if (props.value !== undefined) {
value = props.value;
} else if (props.defaultValue !== undefined) {
value = props.defaultValue;
} else {
const checkedValue = getCheckedValue(props.children);
value = checkedValue && checkedValue.value;
React.useEffect(() => {
setPrevPropValue(props.value);
if (props.value !== undefined || prevPropValue !== props.value) {
setValue(props.value);
}
this.state = {
value,
prevPropValue: props.value,
};
}
}, [props.value]);
onRadioChange = (ev: RadioChangeEvent) => {
const lastValue = this.state.value;
const { value } = ev.target;
if (!('value' in this.props)) {
this.setState({
value,
});
const onRadioChange = (ev: RadioChangeEvent) => {
const lastValue = value;
const val = ev.target.value;
if (!('value' in props)) {
setValue(val);
}
const { onChange } = this.props;
if (onChange && value !== lastValue) {
const { onChange } = props;
if (onChange && val !== lastValue) {
onChange(ev);
}
};
renderGroup = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { props } = this;
const renderGroup = () => {
const {
prefixCls: customizePrefixCls,
className = '',
options,
buttonStyle,
disabled,
children,
size: customizeSize,
style,
id,
onMouseEnter,
onMouseLeave,
} = props;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
const groupPrefixCls = `${prefixCls}-group`;
let { children } = props;
let childrenToRender = children;
// 如果存在 options, 优先使用
if (options && options.length > 0) {
children = options.map(option => {
childrenToRender = options.map(option => {
if (typeof option === 'string') {
// 此处类型自动推导为 string
return (
<Radio
key={option}
prefixCls={prefixCls}
disabled={this.props.disabled}
disabled={disabled}
value={option}
checked={this.state.value === option}
checked={value === option}
>
{option}
</Radio>
@ -112,9 +77,9 @@ class RadioGroup extends React.PureComponent<RadioGroupProps, RadioGroupState> {
<Radio
key={`radio-group-value-options-${option.value}`}
prefixCls={prefixCls}
disabled={option.disabled || this.props.disabled}
disabled={option.disabled || disabled}
value={option.value}
checked={this.state.value === option.value}
checked={value === option.value}
style={option.style}
>
{option.label}
@ -123,50 +88,45 @@ class RadioGroup extends React.PureComponent<RadioGroupProps, RadioGroupState> {
});
}
const mergedSize = customizeSize || size;
const classString = classNames(
groupPrefixCls,
`${groupPrefixCls}-${buttonStyle}`,
{
[`${groupPrefixCls}-${mergedSize}`]: mergedSize,
[`${groupPrefixCls}-rtl`]: direction === 'rtl',
},
className,
);
return (
<SizeContext.Consumer>
{size => {
const mergedSize = customizeSize || size;
const classString = classNames(
groupPrefixCls,
`${groupPrefixCls}-${buttonStyle}`,
{
[`${groupPrefixCls}-${mergedSize}`]: mergedSize,
[`${groupPrefixCls}-rtl`]: direction === 'rtl',
},
className,
);
return (
<div
className={classString}
style={props.style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
id={props.id}
>
{children}
</div>
);
}}
</SizeContext.Consumer>
<div
className={classString}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
id={id}
>
{childrenToRender}
</div>
);
};
render() {
return (
<RadioGroupContextProvider
value={{
onChange: this.onRadioChange,
value: this.state.value,
disabled: this.props.disabled,
name: this.props.name,
}}
>
<ConfigConsumer>{this.renderGroup}</ConfigConsumer>
</RadioGroupContextProvider>
);
}
}
return (
<RadioGroupContextProvider
value={{
onChange: onRadioChange,
value,
disabled: props.disabled,
name: props.name,
}}
>
{renderGroup()}
</RadioGroupContextProvider>
);
};
export default RadioGroup;
RadioGroup.defaultProps = {
buttonStyle: 'outline' as RadioGroupButtonStyle,
};
export default React.memo(RadioGroup);

View File

@ -18,11 +18,6 @@ export interface RadioGroupProps extends AbstractCheckboxGroupProps {
buttonStyle?: RadioGroupButtonStyle;
}
export interface RadioGroupState {
value: any;
prevPropValue: any;
}
export interface RadioGroupContextProps {
onChange: (e: RadioChangeEvent) => void;
value: any;

View File

@ -4,77 +4,68 @@ import classNames from 'classnames';
import RadioGroup from './group';
import RadioButton from './radioButton';
import { RadioProps, RadioChangeEvent } from './interface';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
import { composeRef } from '../_util/ref';
export default class Radio extends React.PureComponent<RadioProps, {}> {
static Group: typeof RadioGroup;
static Button: typeof RadioButton;
static defaultProps = {
type: 'radio',
};
static contextType = RadioGroupContext;
private rcCheckbox: any;
saveCheckbox = (node: any) => {
this.rcCheckbox = node;
};
onChange = (e: RadioChangeEvent) => {
if (this.props.onChange) {
this.props.onChange(e);
}
if (this.context?.onChange) {
this.context.onChange(e);
}
};
focus() {
this.rcCheckbox.focus();
}
blur() {
this.rcCheckbox.blur();
}
renderRadio = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { props, context } = this;
const { prefixCls: customizePrefixCls, className, children, style, ...restProps } = props;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
const radioProps: RadioProps = { ...restProps };
if (context) {
radioProps.name = context.name;
radioProps.onChange = this.onChange;
radioProps.checked = props.value === context.value;
radioProps.disabled = props.disabled || context.disabled;
}
const wrapperClassString = classNames(className, {
[`${prefixCls}-wrapper`]: true,
[`${prefixCls}-wrapper-checked`]: radioProps.checked,
[`${prefixCls}-wrapper-disabled`]: radioProps.disabled,
[`${prefixCls}-wrapper-rtl`]: direction === 'rtl',
});
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={wrapperClassString}
style={style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<RcCheckbox {...radioProps} prefixCls={prefixCls} ref={this.saveCheckbox} />
{children !== undefined ? <span>{children}</span> : null}
</label>
);
};
render() {
return <ConfigConsumer>{this.renderRadio}</ConfigConsumer>;
}
interface CompoundedComponent
extends React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLElement>> {
Group: typeof RadioGroup;
Button: typeof RadioButton;
}
const InternalRadio: React.ForwardRefRenderFunction<unknown, RadioProps> = (props, ref) => {
const context = React.useContext(RadioGroupContext);
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const innerRef = React.useRef<HTMLElement>();
const mergedRef = composeRef(ref, innerRef);
const onChange = (e: RadioChangeEvent) => {
if (props.onChange) {
props.onChange(e);
}
if (context?.onChange) {
context.onChange(e);
}
};
const { prefixCls: customizePrefixCls, className, children, style, ...restProps } = props;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
const radioProps: RadioProps = { ...restProps };
if (context) {
radioProps.name = context.name;
radioProps.onChange = onChange;
radioProps.checked = props.value === context.value;
radioProps.disabled = props.disabled || context.disabled;
}
const wrapperClassString = classNames(className, {
[`${prefixCls}-wrapper`]: true,
[`${prefixCls}-wrapper-checked`]: radioProps.checked,
[`${prefixCls}-wrapper-disabled`]: radioProps.disabled,
[`${prefixCls}-wrapper-rtl`]: direction === 'rtl',
});
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={wrapperClassString}
style={style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<RcCheckbox {...radioProps} prefixCls={prefixCls} ref={mergedRef as any} />
{children !== undefined ? <span>{children}</span> : null}
</label>
);
};
const Radio = React.forwardRef<unknown, RadioProps>(InternalRadio) as CompoundedComponent;
Radio.displayName = 'Radio';
Radio.Group = RadioGroup;
Radio.Button = RadioButton;
Radio.defaultProps = {
type: 'radio',
};
export default Radio;

View File

@ -2,27 +2,22 @@ import * as React from 'react';
import Radio from './radio';
import { RadioChangeEvent } from './interface';
import { AbstractCheckboxProps } from '../checkbox/Checkbox';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
export type RadioButtonProps = AbstractCheckboxProps<RadioChangeEvent>;
const RadioButton = (props: RadioButtonProps, ref: React.Ref<any>) => {
const radioGroupContext = React.useContext(RadioGroupContext);
const { getPrefixCls } = React.useContext(ConfigContext);
return (
<ConfigConsumer>
{({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, ...radioProps }: RadioButtonProps = props;
const prefixCls = getPrefixCls('radio-button', customizePrefixCls);
if (radioGroupContext) {
radioProps.checked = props.value === radioGroupContext.value;
radioProps.disabled = props.disabled || radioGroupContext.disabled;
}
return <Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />;
}}
</ConfigConsumer>
);
const { prefixCls: customizePrefixCls, ...radioProps } = props;
const prefixCls = getPrefixCls('radio-button', customizePrefixCls);
if (radioGroupContext) {
radioProps.checked = props.value === radioGroupContext.value;
radioProps.disabled = props.disabled || radioGroupContext.disabled;
}
return <Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />;
};
export default React.forwardRef(RadioButton);

Some files were not shown because too many files have changed in this diff Show More