Merge pull request #23377 from ant-design/master

chore: Merge master into feature
This commit is contained in:
偏右 2020-04-18 23:54:51 +08:00 committed by GitHub
commit fabec4831c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 14576 additions and 926 deletions

View File

@ -6,6 +6,14 @@ const defaultVars = require('./scripts/default-vars');
const darkVars = require('./scripts/dark-vars');
const compactVars = require('./scripts/compact-vars');
function generateThemeFileContent(theme) {
return `const { ${theme}ThemeSingle } = require('./theme');\nconst defaultTheme = require('./default-theme');\n
module.exports = {
...defaultTheme,
...${theme}ThemeSingle
}`;
}
// We need compile additional content for antd user
function finalizeCompile() {
if (fs.existsSync(path.join(__dirname, './lib'))) {
@ -61,11 +69,27 @@ function buildThemeFile(theme, vars) {
// eslint-disable-next-line no-console
console.log(`Built a entry less file to dist/antd.${theme}.less`);
if (theme === 'default') {
fs.writeFileSync(
path.join(process.cwd(), 'dist', `default-theme.js`),
`module.exports = ${JSON.stringify(vars, null, 2)};\n`,
);
return;
}
// Build ${theme}.js: dist/${theme}-theme.js, for less-loader
fs.writeFileSync(
path.join(process.cwd(), 'dist', `theme.js`),
`const ${theme}ThemeSingle = ${JSON.stringify(vars, null, 2)};\n`,
{
flag: 'a',
},
);
fs.writeFileSync(
path.join(process.cwd(), 'dist', `${theme}-theme.js`),
`module.exports = ${JSON.stringify(vars, null, 2)};`,
generateThemeFileContent(theme),
);
// eslint-disable-next-line no-console
@ -80,10 +104,47 @@ function finalizeDist() {
'@import "../lib/style/index.less";\n@import "../lib/style/components.less";',
);
// eslint-disable-next-line no-console
fs.writeFileSync(
path.join(process.cwd(), 'dist', 'theme.js'),
`const defaultTheme = require('./default-theme.js');\n`,
);
// eslint-disable-next-line no-console
console.log('Built a entry less file to dist/antd.less');
buildThemeFile('default', defaultVars);
buildThemeFile('dark', darkVars);
buildThemeFile('compact', compactVars);
fs.writeFileSync(
path.join(process.cwd(), 'dist', `theme.js`),
`
function getThemeVariables(options = {}) {
let themeVar = {
'hack': \`true;@import "\${require.resolve('antd/lib/style/color/colorPalette.less')}";\`,
...defaultTheme
};
if(options.dark) {
themeVar = {
...themeVar,
...darkThemeSingle
}
}
if(options.compact){
themeVar = {
...themeVar,
...compactThemeSingle
}
}
return themeVar;
}
module.exports = {
darkThemeSingle,
compactThemeSingle,
getThemeVariables
}`,
{
flag: 'a',
},
);
}
}
@ -94,4 +155,6 @@ module.exports = {
dist: {
finalize: finalizeDist,
},
generateThemeFileContent,
};
finalizeDist();

View File

@ -1,5 +1,3 @@
const libDir = process.env.LIB_DIR;
const transformIgnorePatterns = [
'/dist/',
// Ignore modules without es dir.
@ -7,6 +5,13 @@ const transformIgnorePatterns = [
'node_modules/(?!.*@babel)[^/]+?/(?!(es|node_modules)/)',
];
function getTestRegex(libDir) {
if (libDir === 'dist') {
return 'demo\\.test\\.js$';
}
return '.*\\.test\\.(j|t)sx?$';
}
module.exports = {
verbose: true,
setupFiles: ['./tests/setup.js'],
@ -28,7 +33,7 @@ module.exports = {
'\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor',
'\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor',
},
testRegex: `${libDir === 'dist' ? 'demo' : '.*'}\\.test\\.js$`,
testRegex: getTestRegex(process.env.LIB_DIR),
collectCoverageFrom: [
'components/**/*.{ts,tsx}',
'!components/*/style/index.tsx',

View File

@ -12,7 +12,7 @@ module.exports = {
'\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor',
'\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor',
},
testRegex: 'demo\\.test\\.js$',
testRegex: 'demo\\.test\\.(j|t)s$',
testEnvironment: 'node',
transformIgnorePatterns,
snapshotSerializers: ['enzyme-to-json/serializer'],

View File

@ -15,8 +15,58 @@ timeline: true
---
## 4.1.4
`2020-04-18`
- 🐞 Fix dark theme and compact theme not working. [#23243](https://github.com/ant-design/ant-design/pull/23243)
- 🐞 Fix Modal.info executed only once when has argument. [#23360](https://github.com/ant-design/ant-design/pull/23360)
- 🐞 Fix Dropdown submenu background missing. [#23296](https://github.com/ant-design/ant-design/pull/23296)
- 💄 Optimize PageHeader responsive behavior. [#23277](https://github.com/ant-design/ant-design/pull/23277)
- 🐞 Fix TreeSelect render blank in compact mode. [#23231](https://github.com/ant-design/ant-design/pull/23231)
- 🛎 Fix Checkbox and Switch console warning typo (validate -> a valid). [#23240](https://github.com/ant-design/ant-design/pull/23240) [@evancharlton](https://github.com/evancharlton)
- 🐞 Fix Table `rowSelection` params issue when `childrenColumnName` configured. [#23205](https://github.com/ant-design/ant-design/pull/23205)
- Input
- 🐞 Fix Input `type="color"` height issue. [#23351](https://github.com/ant-design/ant-design/pull/23351)
- 🐞 Fix Input width shaking when trigger clear icon. [#23259](https://github.com/ant-design/ant-design/pull/23259)
- 🐞 Fix Input.Search `size` not affected by ConfigProvider `componentSize`. [#23331](https://github.com/ant-design/ant-design/pull/23331)
- Select
- 🐞 Fix multiple Select show remove icon when `disabled`. [#23295](https://github.com/ant-design/ant-design/pull/23295)
- 🐞 Fix Select custom `suffixIcon` cannot be access. [#23274](https://github.com/ant-design/ant-design/pull/23274)
- 🐞 Fix Select search input caret missing in Collapse. [#23250](https://github.com/ant-design/ant-design/pull/23250)
- Globalization
- 🇨🇳 Form validation messages support internalization and add zh_CN locale. [#23165](https://github.com/ant-design/ant-design/pull/23165) [@hengkx](https://github.com/hengkx)
- 🌐 Add missing translations in he_IL. [#23302](https://github.com/ant-design/ant-design/pull/23302) [@MishaKav](https://github.com/MishaKav)
- 🌐 Add missing translations in ru_RU. [#23303](https://github.com/ant-design/ant-design/pull/23303) [@MishaKav](https://github.com/MishaKav)
- TypeScript
- 🔷 Form.Item type upgrade. [#22962](https://github.com/ant-design/ant-design/pull/22962) [@fa93hws](https://github.com/fa93hws)
- 🔷 Tree type upgrade. [#23348](https://github.com/ant-design/ant-design/pull/23348) [@yoyo837](https://github.com/yoyo837)
- 🐞 Pass `popupClassName` prop to `rc-picker`. [#23214](https://github.com/ant-design/ant-design/pull/23214) [@tanmoyopenroot](https://github.com/tanmoyopenroot)
- RTL
- 💄 Fix Select RTL style. [#23235](https://github.com/ant-design/ant-design/pull/23235)
- 💄 Fix Menu RTL style. [#23319](https://github.com/ant-design/ant-design/pull/23319)
## 4.1.3
`2020-04-13`
- 💄 Adjust Form.Item `label` height style in vertical layout. [#23192](https://github.com/ant-design/ant-design/pull/23192)
- 🐞 Fix `Variable is undefined` when importing dark or compact theme and provide a `getThemeVariables` methold for getting theme variables easily. [#23171](https://github.com/ant-design/ant-design/pull/23171)
- 🐞 Fix PageHeader style breaks when `title` is too long and improve it's responsive design. [#23133](https://github.com/ant-design/ant-design/pull/23133)
- Tabs
- 🐞 Fix Tabs `@tabs-card-height` less variable not working. [#23168](https://github.com/ant-design/ant-design/pull/23168)
- 🐞 Fix Tabs cannot be displayed in Safari 13. [#23151](https://github.com/ant-design/ant-design/pull/23151) [@imhxc](https://github.com/imhxc)
- Table
- 🐞 Fix Table fixed columns cannot pin in Safari 12. [#23161](https://github.com/ant-design/ant-design/pull/23161)
- 🐞 Fix Table `summary` padding in small size. [#23140](https://github.com/ant-design/ant-design/pull/23140) [@someyoungideas](https://github.com/someyoungideas)
- 🐞 Fix Select align style with different size. [#23160](https://github.com/ant-design/ant-design/pull/23160)
- 🐞 Fix RangePicker under Input.Group style issue. [#23149](https://github.com/ant-design/ant-design/pull/23149)
- 🐞 Fix Pagination missing TypeScript definition of `showTitle`. [#23144](https://github.com/ant-design/ant-design/pull/23144) [@DongchengWang](https://github.com/DongchengWang)
## 4.1.2
`2020-04-10`
- Menu
- 🐞 Fix Menu SubMenu background in dark mode. [#22981](https://github.com/ant-design/ant-design/pull/22981) [@AshoneA](https://github.com/AshoneA)
- 🐞 Fix long SubMenu title being overlayed by arrow icon. [#23028](https://github.com/ant-design/ant-design/pull/23028) [@wwyx778](https://github.com/wwyx778)
@ -52,6 +102,8 @@ timeline: true
## 4.1.1
`2020-04-05`
- 🐞 Fix Tabs panel focus outline style. [#22752](https://github.com/ant-design/ant-design/pull/22752) [@MrHeer](https://github.com/MrHeer)
- 🐞 Fix Input affix with popup element can not get click focus. [#22887](https://github.com/ant-design/ant-design/pull/22887)
- Table

View File

@ -15,8 +15,58 @@ timeline: true
---
## 4.1.4
`2020-04-18`
- 🐞 修复暗黑主题和紧凑主题不生效的问题。[#23243](https://github.com/ant-design/ant-design/pull/23243)
- 🐞 修复 Modal.info 等方法的 `onOk` 函数有参数时只触发一次的问题。[#23360](https://github.com/ant-design/ant-design/pull/23360)
- 🐞 修复 Dropdown 弹出菜单背景样式问题。[#23296](https://github.com/ant-design/ant-design/pull/23296)
- 💄 优化 PageHeader 的响应式表现。[#23277](https://github.com/ant-design/ant-design/pull/23277)
- 🐞 修复紧凑模式下树选择出现空白。[#23231](https://github.com/ant-design/ant-design/pull/23231)
- 🛎 修改 Checkbox 和 Switch 中控制台输出的错别字 (validate -> a valid)。[#23240](https://github.com/ant-design/ant-design/pull/23240) [@evancharlton](https://github.com/evancharlton)
- 🐞 修复 Table `rowSelection` 在设置 `childrenColumnName` 时事件参数不正确的问题。[#23205](https://github.com/ant-design/ant-design/pull/23205)
- Input
- 🐞 修复 Input `type="color"` 的高度问题。[#23351](https://github.com/ant-design/ant-design/pull/23351)
- 🐞 修复 Input 设置 `allowClear` 内联展示时,触发清除按钮样式抖动的问题。[#23259](https://github.com/ant-design/ant-design/pull/23259)
- 🐞 修复 Input.Search 全局设置 `size` 不生效问题。[#23331](https://github.com/ant-design/ant-design/pull/23331)
- Select
- 🐞 修复 Select 多选时设置 `disabled` 选项仍然会展示移除按钮的问题。[#23295](https://github.com/ant-design/ant-design/pull/23295)
- 🐞 修复 Select 自定义 `suffixIcon` 无法交互的问题。[#23274](https://github.com/ant-design/ant-design/pull/23274)
- 🐞 修复 Select 输入光标在 Collapse 内不显示的问题。[#23250](https://github.com/ant-design/ant-design/pull/23250)
- 国际化
- 🌐 Form 校验信息支持国际化并增加中文文案。[#23165](https://github.com/ant-design/ant-design/pull/23165) [@hengkx](https://github.com/hengkx)
- 🌐 完善希伯来语(以色列) 国际化。[#23302](https://github.com/ant-design/ant-design/pull/23302) [@MishaKav](https://github.com/MishaKav)
- 🌐 完善俄语国际化。[#23303](https://github.com/ant-design/ant-design/pull/23303) [@MishaKav](https://github.com/MishaKav)
- TypeScript
- 🔷 更新 Tree 的类型定义。[#23348](https://github.com/ant-design/ant-design/pull/23348) [@yoyo837](https://github.com/yoyo837)
- 🔷 更新 Form Item 的类型定义。[#22962](https://github.com/ant-design/ant-design/pull/22962) [@fa93hws](https://github.com/fa93hws)
- 🐞 修复 Slider 组件 `value``defaultValue` 文档与 TypeScript 定义不一致的问题。[#23252](https://github.com/ant-design/ant-design/pull/23252) [@DongchengWang](https://github.com/DongchengWang)
- RTL
- 🐞 修复 Menu RTL 样式。[#23319](https://github.com/ant-design/ant-design/pull/23319)
- 💄 修复 Select 的 RTL 样式。[#23235](https://github.com/ant-design/ant-design/pull/23235)
## 4.1.3
`2020-04-13`
- 💄 调整 Form.Item `label` 在垂直布局下的高度样式。[#23192](https://github.com/ant-design/ant-design/pull/23192)
- 🐞 修复引用暗黑或紧凑主题时提示 `Variable is undefined` 的问题,并提供 `getThemeVariables` 方便获取对应主题变量。[#23171](https://github.com/ant-design/ant-design/pull/23171)
- 🐞 修复 PageHeader `title` 超长时布局被破坏的问题并优化响应式表现。[#23133](https://github.com/ant-design/ant-design/pull/23133)
- Tabs
- 🐞 修复 Tabs `@tabs-card-height` less 变量无效的问题。[#23168](https://github.com/ant-design/ant-design/pull/23168)
- 🐞 修复 Tabs 在 Safair 浏览器下无法显示的问题。[#23151](https://github.com/ant-design/ant-design/pull/23151) [@imhxc](https://github.com/imhxc)
- Table
- 🐞 修复 Table 固定列在 Safari 12 中不能固定的问题。[#23161](https://github.com/ant-design/ant-design/pull/23161)
- 🐞 修复 Table `summary` 在小尺寸下的内边距样式。[#23140](https://github.com/ant-design/ant-design/pull/23140) [@someyoungideas](https://github.com/someyoungideas)
- 🐞 修复 Select 不同尺寸下的对齐样式问题。[#23160](https://github.com/ant-design/ant-design/pull/23160)
- 🐞 修复 RangePicker 在 Input.Group 内的样式问题。[#23149](https://github.com/ant-design/ant-design/pull/23149)
- 🐞 修复 Pagination 缺少 `showTitle` TypeScript 定义的问题。[#23144](https://github.com/ant-design/ant-design/pull/23144) [@DongchengWang](https://github.com/DongchengWang)
## 4.1.2
`2020-04-10`
- Menu
- 🐞 修复暗色 Menu 弹出菜单背景色为白色的问题。[#22981](https://github.com/ant-design/ant-design/pull/22981) [@AshoneA](https://github.com/AshoneA)
- 🐞 修复 SubMenu 标题过长而导致被箭头图标部分覆盖的问题。[#23028](https://github.com/ant-design/ant-design/pull/23028) [@wwyx778](https://github.com/wwyx778)
@ -52,6 +102,8 @@ timeline: true
## 4.1.1
`2020-04-05`
- 🐞 移除 Tabs 的内容区域的 focus 蓝色轮廓线。[#22752](https://github.com/ant-design/ant-design/pull/22752) [@MrHeer](https://github.com/MrHeer)
- 🐞 修复 Input 前后缀添加弹出元素不能点击获得焦点的问题。[#22887](https://github.com/ant-design/ant-design/pull/22887)
- Table

View File

@ -1,6 +1,10 @@
const __NULL__ = { notExist: true };
export function spyElementPrototypes(Element, properties) {
type ElementType<P> = {
prototype: P;
};
export function spyElementPrototypes<P extends {}>(Element: ElementType<P>, properties: P) {
const propNames = Object.keys(properties);
const originDescriptors = {};
@ -51,7 +55,16 @@ export function spyElementPrototypes(Element, properties) {
};
}
export function spyElementPrototype(Element, propName, property) {
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T] &
string;
export function spyElementPrototype<P extends {}, K extends FunctionPropertyNames<P>>(
Element: ElementType<P>,
propName: K,
property: P[K],
) {
return spyElementPrototypes(Element, {
[propName]: property,
});

View File

@ -7,9 +7,17 @@ import { spyElementPrototype } from '../../__tests__/util/domHook';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils';
const events = {};
const events: any = {};
class AffixMounter extends React.Component<{
offsetBottom?: number;
offsetTop?: number;
onTestUpdatePosition?(): void;
}> {
private container: HTMLDivElement;
private affix: Affix;
class AffixMounter extends React.Component {
componentDidMount() {
this.container.addEventListener = jest.fn().mockImplementation((event, cb) => {
events[event] = cb;
@ -47,7 +55,7 @@ describe('Affix Render', () => {
let wrapper;
let domMock;
const classRect = {
const classRect: any = {
container: {
top: 0,
bottom: 100,
@ -135,9 +143,9 @@ describe('Affix Render', () => {
describe('updatePosition when target changed', () => {
it('function change', () => {
document.body.innerHTML = '<div id="mounter" />';
const container = document.querySelector('#id');
const container = document.querySelector('#id') as HTMLDivElement;
const getTarget = () => container;
wrapper = mount(<Affix target={getTarget} />);
wrapper = mount(<Affix target={getTarget}>{null}</Affix>);
wrapper.setProps({ target: null });
expect(wrapper.instance().state.status).toBe(0);
expect(wrapper.instance().state.affixStyle).toBe(undefined);
@ -153,7 +161,7 @@ describe('Affix Render', () => {
const originLength = getObserverLength();
const getTarget = () => target;
wrapper = mount(<Affix target={getTarget} />);
wrapper = mount(<Affix target={getTarget}>{null}</Affix>);
await sleep(50);
expect(getObserverLength()).toBe(originLength + 1);

View File

@ -32,7 +32,7 @@ export interface AffixProps {
target?: () => Window | HTMLElement | null;
prefixCls?: string;
className?: string;
children: React.ReactElement;
children: React.ReactNode;
}
enum AffixStatus {

View File

@ -1,12 +1,12 @@
import React, { Component } from 'react';
import { mount, render } from 'enzyme';
import renderer from 'react-test-renderer';
import { SearchOutlined } from '@ant-design/icons';
import Button from '..';
import ConfigProvider from '../../config-provider';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils';
import { SizeType } from '../../config-provider/SizeContext';
describe('Button', () => {
mountTest(Button);
@ -30,13 +30,14 @@ describe('Button', () => {
});
it('mount correctly', () => {
expect(() => renderer.create(<Button>Follow</Button>)).not.toThrow();
expect(() => mount(<Button>Follow</Button>)).not.toThrow();
});
it('warns if size is wrong', () => {
const mockWarn = jest.fn();
jest.spyOn(console, 'warn').mockImplementation(mockWarn);
render(<Button.Group size="who am I" />);
const size = ('who am I' as any) as SizeType;
render(<Button.Group size={size} />);
expect(mockWarn).toHaveBeenCalledTimes(1);
expect(mockWarn.mock.calls[0][0]).toMatchObject({
message: 'unreachable case: "who am I"',
@ -74,7 +75,7 @@ describe('Button', () => {
});
it('renders Chinese characters correctly in HOC', () => {
const Text = ({ children }) => <span>{children}</span>;
const Text = ({ children }: { children: React.ReactNode }) => <span>{children}</span>;
const wrapper = mount(
<Button>
<Text></Text>
@ -110,7 +111,7 @@ describe('Button', () => {
it('have static property for type detecting', () => {
const wrapper = mount(<Button>Button Text</Button>);
expect(wrapper.type().__ANT_BUTTON).toBe(true);
expect((wrapper.type() as any).__ANT_BUTTON).toBe(true);
});
it('should change loading state instantly by default', () => {
@ -189,7 +190,7 @@ describe('Button', () => {
it('should has click wave effect', async () => {
const wrapper = mount(<Button type="primary">button</Button>);
wrapper.find('.ant-btn').getDOMNode().click();
wrapper.find('.ant-btn').getDOMNode<HTMLButtonElement>().click();
await new Promise(resolve => setTimeout(resolve, 0));
expect(wrapper.render()).toMatchSnapshot();
});
@ -262,6 +263,7 @@ describe('Button', () => {
throw new Error('Should not called!!!');
},
});
wrapper.find('Button').instance().forceUpdate();
expect(wrapper.find('Button').instance()).toBe(null);
});
});

View File

@ -4,7 +4,7 @@ import classNames from 'classnames';
import omit from 'omit.js';
import Group from './button-group';
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import Wave from '../_util/wave';
import { Omit, tuple } from '../_util/type';
import warning from '../_util/warning';
@ -104,72 +104,57 @@ export type NativeButtonProps = {
export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;
interface ButtonState {
loading?: boolean | { delay?: number };
hasTwoCNChar: boolean;
interface ButtonTypeProps extends React.FC<ButtonProps> {
Group: typeof Group;
__ANT_BUTTON: boolean;
}
class Button extends React.Component<ButtonProps, ButtonState> {
static Group: typeof Group;
const Button: ButtonTypeProps = ({ ...props }) => {
const [loading, setLoading] = React.useState(props.loading);
const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext);
const buttonRef = React.createRef<HTMLButtonElement>();
let delayTimeout: number;
static __ANT_BUTTON = true;
static contextType = ConfigContext;
static defaultProps = {
loading: false,
ghost: false,
block: false,
htmlType: 'button' as ButtonProps['htmlType'],
const isNeedInserted = () => {
const { icon, children, type } = props;
return React.Children.count(children) === 1 && !icon && type !== 'link';
};
private delayTimeout: number;
private buttonNode: HTMLElement | null;
constructor(props: ButtonProps) {
super(props);
this.state = {
loading: props.loading,
hasTwoCNChar: false,
};
}
componentDidMount() {
this.fixTwoCNChar();
}
componentDidUpdate(prevProps: ButtonProps) {
this.fixTwoCNChar();
if (prevProps.loading && typeof prevProps.loading !== 'boolean') {
clearTimeout(this.delayTimeout);
const fixTwoCNChar = () => {
// Fix for HOC usage like <FormatMessage />
if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) {
return;
}
const { loading } = this.props;
if (loading && typeof loading !== 'boolean' && loading.delay) {
this.delayTimeout = window.setTimeout(() => {
this.setState({ loading });
}, loading.delay);
} else if (prevProps.loading !== loading) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ loading });
const buttonText = buttonRef.current.textContent;
if (isNeedInserted() && isTwoCNChar(buttonText)) {
if (!hasTwoCNChar) {
setHasTwoCNChar(true);
}
} else if (hasTwoCNChar) {
setHasTwoCNChar(false);
}
}
componentWillUnmount() {
if (this.delayTimeout) {
clearTimeout(this.delayTimeout);
}
}
saveButtonRef = (node: HTMLElement | null) => {
this.buttonNode = node;
};
handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => {
const { loading } = this.state;
const { onClick } = this.props;
React.useEffect(() => {
if (props.loading && typeof props.loading !== 'boolean') {
clearTimeout(delayTimeout);
}
if (props.loading && typeof props.loading !== 'boolean' && props.loading.delay) {
delayTimeout = window.setTimeout(() => {
setLoading(props.loading);
}, props.loading.delay);
} else if (props.loading !== loading) {
setLoading(props.loading);
}
}, [props.loading]);
React.useEffect(() => {
fixTwoCNChar();
}, [buttonRef]);
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
const { onClick } = props;
if (loading) {
return;
}
@ -178,143 +163,117 @@ class Button extends React.Component<ButtonProps, ButtonState> {
}
};
fixTwoCNChar() {
const { autoInsertSpaceInButton }: ConfigConsumerProps = this.context;
return (
<SizeContext.Consumer>
{size => {
const {
prefixCls: customizePrefixCls,
type,
danger,
shape,
size: customizeSize,
className,
children,
icon,
ghost,
block,
...rest
} = props;
// Fix for HOC usage like <FormatMessage />
if (!this.buttonNode || autoInsertSpaceInButton === false) {
return;
}
const buttonText = this.buttonNode.textContent;
if (this.isNeedInserted() && isTwoCNChar(buttonText)) {
if (!this.state.hasTwoCNChar) {
this.setState({
hasTwoCNChar: true,
warning(
!(typeof icon === 'string' && icon.length > 2),
'Button',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
const prefixCls = getPrefixCls('btn', customizePrefixCls);
const autoInsertSpace = autoInsertSpaceInButton !== false;
// large => lg
// small => sm
let sizeCls = '';
switch (customizeSize || size) {
case 'large':
sizeCls = 'lg';
break;
case 'small':
sizeCls = 'sm';
break;
default:
break;
}
const iconType = loading ? 'loading' : icon;
const classes = classNames(prefixCls, className, {
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && iconType,
[`${prefixCls}-background-ghost`]: ghost,
[`${prefixCls}-loading`]: loading,
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
});
}
} else if (this.state.hasTwoCNChar) {
this.setState({
hasTwoCNChar: false,
});
}
}
isNeedInserted() {
const { icon, children, type } = this.props;
return React.Children.count(children) === 1 && !icon && type !== 'link';
}
render() {
const { getPrefixCls, autoInsertSpaceInButton, direction }: ConfigConsumerProps = this.context;
return (
<SizeContext.Consumer>
{size => {
const {
prefixCls: customizePrefixCls,
type,
danger,
shape,
size: customizeSize,
className,
children,
icon,
ghost,
block,
...rest
} = this.props;
const { loading, hasTwoCNChar } = this.state;
warning(
!(typeof icon === 'string' && icon.length > 2),
'Button',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
const iconNode =
icon && !loading ? (
icon
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={loading} />
);
const prefixCls = getPrefixCls('btn', customizePrefixCls);
const autoInsertSpace = autoInsertSpaceInButton !== false;
const kids =
children || children === 0
? spaceChildren(children, isNeedInserted() && autoInsertSpace)
: null;
// large => lg
// small => sm
let sizeCls = '';
switch (customizeSize || size) {
case 'large':
sizeCls = 'lg';
break;
case 'small':
sizeCls = 'sm';
break;
default:
break;
}
const iconType = loading ? 'loading' : icon;
const classes = classNames(prefixCls, className, {
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && iconType,
[`${prefixCls}-background-ghost`]: ghost,
[`${prefixCls}-loading`]: loading,
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
});
const iconNode =
icon && !loading ? (
icon
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={loading} />
);
const kids =
children || children === 0
? spaceChildren(children, this.isNeedInserted() && autoInsertSpace)
: null;
const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
if (linkButtonRestProps.href !== undefined) {
return (
<a
{...linkButtonRestProps}
className={classes}
onClick={this.handleClick}
ref={this.saveButtonRef}
>
{iconNode}
{kids}
</a>
);
}
// React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
const { htmlType, ...otherProps } = rest as NativeButtonProps;
const buttonNode = (
<button
{...(omit(otherProps, ['loading']) as NativeButtonProps)}
type={htmlType}
className={classes}
onClick={this.handleClick}
ref={this.saveButtonRef}
>
const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
if (linkButtonRestProps.href !== undefined) {
return (
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
{iconNode}
{kids}
</button>
</a>
);
}
if (type === 'link') {
return buttonNode;
}
// React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
const { htmlType, ...otherProps } = rest as NativeButtonProps;
return <Wave>{buttonNode}</Wave>;
}}
</SizeContext.Consumer>
);
}
}
const buttonNode = (
<button
{...(omit(otherProps, ['loading']) as NativeButtonProps)}
type={htmlType}
className={classes}
onClick={handleClick}
ref={buttonRef}
>
{iconNode}
{kids}
</button>
);
if (type === 'link') {
return buttonNode;
}
return <Wave>{buttonNode}</Wave>;
}}
</SizeContext.Consumer>
);
};
Button.defaultProps = {
loading: false,
ghost: false,
block: false,
htmlType: 'button' as ButtonProps['htmlType'],
};
Button.Group = Group;
Button.__ANT_BUTTON = true;
export default Button;

View File

@ -1,9 +1,7 @@
import Button from './button';
import ButtonGroup from './button-group';
export { ButtonProps, ButtonShape, ButtonType } from './button';
export { ButtonGroupProps } from './button-group';
export { SizeType as ButtonSize } from '../config-provider/SizeContext';
Button.Group = ButtonGroup;
export default Button;

View File

@ -37,7 +37,7 @@ A card can be used to display content related to a single subject. The content c
| title | Card title | string\|ReactNode | - | |
| type | Card style type, can be set to `inner` or not set | string | - | |
| onTabChange | Callback when tab is switched | (key) => void | - | |
| tabProps | [Tabs](https://ant.design/components/tabs/#Tabs) | - | - | |
| tabProps | [Tabs](/components/tabs/#Tabs) | - | - | |
### Card.Grid

View File

@ -38,7 +38,7 @@ cols: 1
| title | 卡片标题 | string\|ReactNode | - | |
| type | 卡片类型,可设置为 `inner` 或 不设置 | string | - | |
| onTabChange | 页签切换的回调 | (key) => void | - | |
| tabProps | [Tabs](https://ant.design/components/tabs-cn/#Tabs) | - | - | |
| tabProps | [Tabs](/components/tabs/#Tabs) | - | - | |
### Card.Grid

View File

@ -63,7 +63,7 @@ class Checkbox extends React.PureComponent<CheckboxProps, {}> {
warning(
'checked' in this.props || this.context || !('value' in this.props),
'Checkbox',
'`value` is not validate prop, do you mean `checked`?',
'`value` is not a valid prop, do you mean `checked`?',
);
}

View File

@ -30,7 +30,7 @@ describe('Checkbox', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<Checkbox value />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Checkbox] `value` is not validate prop, do you mean `checked`?',
'Warning: [antd: Checkbox] `value` is not a valid prop, do you mean `checked`?',
);
errorSpy.mockRestore();
});

View File

@ -82,6 +82,26 @@ describe('ConfigProvider', () => {
),
).toMatchSnapshot();
});
it('configProvider componentSize large', () => {
expect(
render(
<ConfigProvider componentSize="large" prefixCls="config">
{renderComponent({})}
</ConfigProvider>,
),
).toMatchSnapshot();
});
it('configProvider componentSize middle', () => {
expect(
render(
<ConfigProvider componentSize="middle" prefixCls="config">
{renderComponent({})}
</ConfigProvider>,
),
).toMatchSnapshot();
});
});
}

View File

@ -6,6 +6,12 @@ import zhCN from '../../locale/zh_CN';
import enUS from '../../locale/en_US';
import TimePicker from '../../time-picker';
import Modal from '../../modal';
import Form from '../../form';
const delay = (timeout = 0) =>
new Promise(resolve => {
setTimeout(resolve, timeout);
});
describe('ConfigProvider.Locale', () => {
function $$(className) {
@ -104,4 +110,41 @@ describe('ConfigProvider.Locale', () => {
testLocale(wrapper);
});
});
describe('form validateMessages', () => {
const wrapperComponent = ({ validateMessages }) =>
mount(
<ConfigProvider locale={zhCN} form={{ validateMessages }}>
<Form initialValues={{ age: 18 }}>
<Form.Item name="test" label="姓名" rules={[{ required: true }]}>
<input />
</Form.Item>
<Form.Item name="age" label="年龄" rules={[{ type: 'number', len: 17 }]}>
<input />
</Form.Item>
</Form>
</ConfigProvider>,
);
it('set locale zhCN', async () => {
const wrapper = wrapperComponent({});
wrapper.find('form').simulate('submit');
await delay(50);
wrapper.update();
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('请输入姓名');
});
it('set locale zhCN and set form validateMessages one item, other use default message', async () => {
const wrapper = wrapperComponent({ validateMessages: { required: '必须' } });
wrapper.find('form').simulate('submit');
await delay(50);
wrapper.update();
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('必须');
expect(wrapper.find('.ant-form-item-explain').last().text()).toEqual('年龄必须等于17');
});
});
});

View File

@ -46,6 +46,9 @@ const FormSizeDemo = () => {
<div className="example">
<Input />
</div>
<div className="example">
<Input.Search />
</div>
<div className="example">
<Select defaultValue="demo" options={[{ value: 'demo' }]} />
</div>

View File

@ -93,10 +93,17 @@ class ConfigProvider extends React.Component<ConfigProviderProps> {
let childNode = children;
// Additional Form provider
let validateMessages: ValidateMessages = {};
if (locale && locale.Form && locale.Form.defaultValidateMessages) {
validateMessages = locale.Form.defaultValidateMessages;
}
if (form && form.validateMessages) {
childNode = (
<RcFormProvider validateMessages={form.validateMessages}>{children}</RcFormProvider>
);
validateMessages = { ...validateMessages, ...form.validateMessages };
}
if (Object.keys(validateMessages).length > 0) {
childNode = <RcFormProvider validateMessages={validateMessages}>{children}</RcFormProvider>;
}
return (

View File

@ -68,6 +68,8 @@
&-submenu-popup {
position: absolute;
z-index: @zindex-dropdown;
background: transparent;
box-shadow: none;
> .@{dropdown-prefix-cls}-menu {
transform-origin: 0 0;

View File

@ -24,7 +24,7 @@ Empty state placeholder.
| --- | --- | --- | --- | --- |
| description | Customize description | string \| ReactNode | - | |
| imageStyle | style of image | CSSProperties | - | |
| image | Customize image. Will tread as image url when string provided. | string \| ReactNode | `Empty.PRESENTED_IMAGE_DEFAULT` | |
| image | Customize image. Will treat as image url when string provided. | string \| ReactNode | `Empty.PRESENTED_IMAGE_DEFAULT` | |
## Built-in images

View File

@ -24,22 +24,17 @@ type ChildrenType = RenderChildren | React.ReactNode;
interface MemoInputProps {
value: any;
update: number;
children: any;
children: React.ReactNode;
}
const MemoInput = React.memo<MemoInputProps>(
({ children }) => {
return children;
},
const MemoInput = React.memo(
({ children }: MemoInputProps) => children as JSX.Element,
(prev, next) => {
return prev.value === next.value && prev.update === next.update;
},
);
export interface FormItemProps
extends FormItemLabelProps,
FormItemInputProps,
Omit<RcFieldProps, 'children'> {
export interface FormItemProps extends FormItemLabelProps, FormItemInputProps, RcFieldProps {
prefixCls?: string;
noStyle?: boolean;
style?: React.CSSProperties;
@ -129,7 +124,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
fieldId?: string,
meta?: Meta,
isRequired?: boolean,
): any {
): React.ReactNode {
if (noStyle) {
return baseChildren;
}
@ -226,7 +221,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
updateRef.current += 1;
if (!hasName && !isRenderProps && !dependencies) {
return renderLayout(children);
return renderLayout(children) as JSX.Element;
}
const variables: Record<string, string> = {};
@ -304,7 +299,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
);
} else if (React.isValidElement(children)) {
warning(
(children.props as any).defaultValue === undefined,
children.props.defaultValue === undefined,
'Form.Item',
'`defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.',
);
@ -337,7 +332,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
'Form.Item',
'`name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.',
);
childNode = children as any;
childNode = children;
}
return renderLayout(childNode, fieldId, meta, isRequired);

View File

@ -6206,7 +6206,29 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</div>
<span
@ -6360,6 +6382,27 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
<span
aria-label="eye-invisible"
class="anticon anticon-eye-invisible ant-input-password-icon"

View File

@ -585,9 +585,9 @@ describe('Form', () => {
);
wrapper.find('form').simulate('submit');
await delay(50);
await delay(100);
wrapper.update();
await delay(100);
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!');
});

View File

@ -1,5 +1,3 @@
/* eslint-disable no-unused-expressions */
import * as React from 'react';
import Form from '..';
import Input from '../../input';
@ -17,5 +15,3 @@ describe('Form.typescript', () => {
expect(form).toBeTruthy();
});
});
/* eslint-enable */

View File

@ -25,13 +25,13 @@ High performance Form component with data scope management. Including data colle
| hideRequiredMark | Hide required mark for all form items | boolean | false |
| initialValues | Set value by Form initialization or reset | object | - |
| labelAlign | text align of label of all items | `left` \| `right` | `right` |
| labelCol | label layout, like `<Col>` component. Set `span` `offset` value like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | - |
| labelCol | label layout, like `<Col>` component. Set `span` `offset` value like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` | [object](/components/grid/#Col) | - |
| layout | Form layout | `horizontal` \| `vertical` \| `inline` | `horizontal` |
| name | Form name. Will be the prefix of Field `id` | string | - |
| scrollToFirstError | Auto scroll to first failed field when submit | false | - |
| size | Set field component size (antd components only) | `small` \| `middle` \| `large` | - |
| validateMessages | Validation prompt template, description [see below](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - |
| wrapperCol | The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | - |
| wrapperCol | The layout for input controls, same as `labelCol` | [object](/components/grid/#Col) | - |
| onFinish | Trigger after submitting the form and verifying data successfully | Function(values) | - |
| onFinishFailed | Trigger after submitting the form and verifying data failed | Function({ values, errorFields, outOfDate }) | - |
| onFieldsChange | Trigger when field updated | Function(changedFields, allFields) | - |
@ -84,7 +84,7 @@ Form field component for data bidirectional binding, validation, layout, and so
| labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `<Col>`. You can set `labelCol` on Form. If both exists, use Item first | [object](/components/grid/#Col) | - | |
| name | Field name, support array | [NamePath](#NamePath) | - | |
| normalize | Normalize value from component value before passing to Form instance | (value, prevValue, prevValues) => any | - | |
| required | Whether provided or not, it will be generated by the validation rule | boolean | false | |
| required | Display required style. It will be generated by the validation rule | boolean | false | |
| rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - | |
| shouldUpdate | Custom field update logic. See [below](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
| trigger | When to collect the value of children node | string | onChange | |

View File

@ -26,13 +26,13 @@ title: Form
| hideRequiredMark | 隐藏所有表单项的必选标记 | boolean | false |
| initialValues | 表单默认值,只有初始化以及重置时生效 | object | - |
| labelAlign | label 标签的文本对齐方式 | `left` \| `right` | `right` |
| labelCol | label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | - |
| labelCol | label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}` | [object](/components/grid/#Col) | - |
| layout | 表单布局 | `horizontal` \| `vertical` \| `inline` | `horizontal` |
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - |
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | false | - |
| size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - |
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | - |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](/components/grid/#Col) | - |
| onFinish | 提交表单且数据验证成功后回调事件 | Function(values) | - |
| onFinishFailed | 提交表单且数据验证失败后回调事件 | Function({ values, errorFields, outOfDate }) | - |
| onFieldsChange | 字段更新时触发回调事件 | Function(changedFields, allFields) | - |
@ -85,7 +85,7 @@ const validateMessages = {
| labelCol | `label` 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - | |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | |
| normalize | 组件获取值后进行转换,再放入 Form 中 | (value, prevValue, prevValues) => any | - | |
| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | false | |
| required | 必填样式设置。如不设置,则会根据校验规则自动生成 | boolean | false | |
| rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - | |
| shouldUpdate | 自定义字段更新逻辑,说明[见下](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
| trigger | 设置收集字段值变更的时机 | string | onChange | |

View File

@ -36,6 +36,10 @@
.@{form-prefix-cls}-vertical {
.@{form-item-prefix-cls} {
flex-direction: column;
&-label > label {
height: auto;
}
}
}

View File

@ -54,21 +54,25 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
renderClearIcon(prefixCls: string) {
const { allowClear, value, disabled, readOnly, inputType, handleReset } = this.props;
if (
!allowClear ||
disabled ||
readOnly ||
value === undefined ||
value === null ||
value === ''
) {
if (!allowClear) {
return null;
}
const needClear = !disabled && !readOnly && value;
const className =
inputType === ClearableInputType[0]
? `${prefixCls}-textarea-clear-icon`
: `${prefixCls}-clear-icon`;
return <CloseCircleFilled onClick={handleReset} className={className} role="button" />;
return (
<CloseCircleFilled
onClick={handleReset}
className={classNames(className, {
[`${className}-hidden`]: !needClear,
})}
role="button"
/>
);
}
renderSuffix(prefixCls: string) {

View File

@ -4,6 +4,7 @@ import SearchOutlined from '@ant-design/icons/SearchOutlined';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import Input, { InputProps } from './Input';
import Button from '../button';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
export interface SearchProps extends InputProps {
@ -65,13 +66,22 @@ export default class Search extends React.Component<SearchProps, any> {
}
renderLoading = (prefixCls: string) => {
const { enterButton, size } = this.props;
const { enterButton, size: customizeSize } = this.props;
if (enterButton) {
return (
<Button className={`${prefixCls}-button`} type="primary" size={size} key="enterButton">
<LoadingOutlined />
</Button>
<SizeContext.Consumer>
{size => (
<Button
className={`${prefixCls}-button`}
type="primary"
size={customizeSize || size}
key="enterButton"
>
<LoadingOutlined />
</Button>
)}
</SizeContext.Consumer>
);
}
return <LoadingOutlined className={`${prefixCls}-icon`} key="loadingIcon" />;
@ -104,8 +114,8 @@ export default class Search extends React.Component<SearchProps, any> {
return icon;
};
renderAddonAfter = (prefixCls: string) => {
const { enterButton, size, disabled, addonAfter, loading } = this.props;
renderAddonAfter = (prefixCls: string, size: SizeType) => {
const { enterButton, disabled, addonAfter, loading } = this.props;
const btnClassName = `${prefixCls}-button`;
if (loading && enterButton) {
@ -165,9 +175,9 @@ export default class Search extends React.Component<SearchProps, any> {
const {
prefixCls: customizePrefixCls,
inputPrefixCls: customizeInputPrefixCls,
size,
enterButton,
className,
size: customizeSize,
...restProps
} = this.props;
@ -177,32 +187,38 @@ export default class Search extends React.Component<SearchProps, any> {
const prefixCls = getPrefixCls('input-search', customizePrefixCls);
const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls);
let inputClassName;
if (enterButton) {
inputClassName = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-enter-button`]: !!enterButton,
[`${prefixCls}-${size}`]: !!size,
});
} else {
inputClassName = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
}
const getClassName = (size: SizeType) => {
let inputClassName;
if (enterButton) {
inputClassName = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-enter-button`]: !!enterButton,
[`${prefixCls}-${size}`]: !!size,
});
} else {
inputClassName = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
}
return inputClassName;
};
return (
<Input
onPressEnter={this.onSearch}
{...restProps}
size={size}
prefixCls={inputPrefixCls}
addonAfter={this.renderAddonAfter(prefixCls)}
suffix={this.renderSuffix(prefixCls)}
onChange={this.onChange}
ref={this.saveInput}
className={inputClassName}
/>
<SizeContext.Consumer>
{size => (
<Input
onPressEnter={this.onSearch}
{...restProps}
size={customizeSize || size}
prefixCls={inputPrefixCls}
addonAfter={this.renderAddonAfter(prefixCls, customizeSize || size)}
suffix={this.renderSuffix(prefixCls)}
onChange={this.onChange}
ref={this.saveInput}
className={getClassName(customizeSize || size)}
/>
)}
</SizeContext.Consumer>
);
};

View File

@ -960,7 +960,29 @@ exports[`renders ./components/input/demo/allowClear.md correctly 1`] = `
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
<br />
<br />
@ -971,6 +993,27 @@ exports[`renders ./components/input/demo/allowClear.md correctly 1`] = `
class="ant-input"
placeholder="textarea with clear icon"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
`;

View File

@ -48,7 +48,29 @@ exports[`Input allowClear should change type when click 2`] = `
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;
@ -63,7 +85,29 @@ exports[`Input allowClear should not show icon if defaultValue is undefined, nul
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;
@ -78,7 +122,29 @@ exports[`Input allowClear should not show icon if defaultValue is undefined, nul
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;
@ -93,7 +159,29 @@ exports[`Input allowClear should not show icon if defaultValue is undefined, nul
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;
@ -108,7 +196,29 @@ exports[`Input allowClear should not show icon if value is undefined, null or em
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;
@ -123,7 +233,29 @@ exports[`Input allowClear should not show icon if value is undefined, null or em
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;
@ -138,7 +270,29 @@ exports[`Input allowClear should not show icon if value is undefined, null or em
/>
<span
class="ant-input-suffix"
/>
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;

View File

@ -40,6 +40,27 @@ exports[`TextArea allowClear should change type when click 2`] = `
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;
@ -50,6 +71,27 @@ exports[`TextArea allowClear should not show icon if defaultValue is undefined,
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;
@ -60,6 +102,27 @@ exports[`TextArea allowClear should not show icon if defaultValue is undefined,
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;
@ -70,6 +133,27 @@ exports[`TextArea allowClear should not show icon if defaultValue is undefined,
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;
@ -80,6 +164,27 @@ exports[`TextArea allowClear should not show icon if value is undefined, null or
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;
@ -90,6 +195,27 @@ exports[`TextArea allowClear should not show icon if value is undefined, null or
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;
@ -100,6 +226,27 @@ exports[`TextArea allowClear should not show icon if value is undefined, null or
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
`;

View File

@ -56,10 +56,7 @@ describe('Input', () => {
describe('focus trigger warning', () => {
it('not trigger', () => {
const wrapper = mount(<Input suffix="bamboo" />);
wrapper
.find('input')
.instance()
.focus();
wrapper.find('input').instance().focus();
wrapper.setProps({
suffix: 'light',
});
@ -67,10 +64,7 @@ describe('Input', () => {
});
it('trigger warning', () => {
const wrapper = mount(<Input />, { attachTo: document.body });
wrapper
.find('input')
.instance()
.focus();
wrapper.find('input').instance().focus();
wrapper.setProps({
suffix: 'light',
});
@ -129,10 +123,7 @@ describe('Input allowClear', () => {
wrapper.find('input').simulate('change', { target: { value: '111' } });
expect(wrapper.find('input').getDOMNode().value).toEqual('111');
expect(wrapper.render()).toMatchSnapshot();
wrapper
.find('.ant-input-clear-icon')
.at(0)
.simulate('click');
wrapper.find('.ant-input-clear-icon').at(0).simulate('click');
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.find('input').getDOMNode().value).toEqual('');
});
@ -141,7 +132,7 @@ describe('Input allowClear', () => {
const wrappers = [null, undefined, ''].map(val => mount(<Input allowClear value={val} />));
wrappers.forEach(wrapper => {
expect(wrapper.find('input').getDOMNode().value).toEqual('');
expect(wrapper.find('.ant-input-clear-icon').exists()).toEqual(false);
expect(wrapper.find('.ant-input-clear-icon-hidden').exists()).toBeTruthy();
expect(wrapper.render()).toMatchSnapshot();
});
});
@ -152,7 +143,7 @@ describe('Input allowClear', () => {
);
wrappers.forEach(wrapper => {
expect(wrapper.find('input').getDOMNode().value).toEqual('');
expect(wrapper.find('.ant-input-clear-icon').exists()).toEqual(false);
expect(wrapper.find('.ant-input-clear-icon-hidden').exists()).toBeTruthy();
expect(wrapper.render()).toMatchSnapshot();
});
});
@ -165,18 +156,10 @@ describe('Input allowClear', () => {
argumentEventObjectValue = e.target.value;
};
const wrapper = mount(<Input allowClear defaultValue="111" onChange={onChange} />);
wrapper
.find('.ant-input-clear-icon')
.at(0)
.simulate('click');
wrapper.find('.ant-input-clear-icon').at(0).simulate('click');
expect(argumentEventObject.type).toBe('click');
expect(argumentEventObjectValue).toBe('');
expect(
wrapper
.find('input')
.at(0)
.getDOMNode().value,
).toBe('');
expect(wrapper.find('input').at(0).getDOMNode().value).toBe('');
});
it('should trigger event correctly on controlled mode', () => {
@ -187,39 +170,23 @@ describe('Input allowClear', () => {
argumentEventObjectValue = e.target.value;
};
const wrapper = mount(<Input allowClear value="111" onChange={onChange} />);
wrapper
.find('.ant-input-clear-icon')
.at(0)
.simulate('click');
wrapper.find('.ant-input-clear-icon').at(0).simulate('click');
expect(argumentEventObject.type).toBe('click');
expect(argumentEventObjectValue).toBe('');
expect(
wrapper
.find('input')
.at(0)
.getDOMNode().value,
).toBe('111');
expect(wrapper.find('input').at(0).getDOMNode().value).toBe('111');
});
it('should focus input after clear', () => {
const wrapper = mount(<Input allowClear defaultValue="111" />, { attachTo: document.body });
wrapper
.find('.ant-input-clear-icon')
.at(0)
.simulate('click');
expect(document.activeElement).toBe(
wrapper
.find('input')
.at(0)
.getDOMNode(),
);
wrapper.find('.ant-input-clear-icon').at(0).simulate('click');
expect(document.activeElement).toBe(wrapper.find('input').at(0).getDOMNode());
wrapper.unmount();
});
['disabled', 'readOnly'].forEach(prop => {
it(`should not support allowClear when it is ${prop}`, () => {
const wrapper = mount(<Input allowClear defaultValue="111" {...{ [prop]: true }} />);
expect(wrapper.find('.ant-input-clear-icon').length).toBe(0);
expect(wrapper.find('.ant-input-clear-icon-hidden').exists()).toBeTruthy();
});
});
});

View File

@ -183,7 +183,7 @@ describe('TextArea allowClear', () => {
const wrappers = [null, undefined, ''].map(val => mount(<TextArea allowClear value={val} />));
wrappers.forEach(wrapper => {
expect(wrapper.find('textarea').getDOMNode().value).toEqual('');
expect(wrapper.find('.ant-input-textarea-clear-icon').exists()).toEqual(false);
expect(wrapper.find('.ant-input-textarea-clear-icon-hidden').exists()).toBeTruthy();
expect(wrapper.render()).toMatchSnapshot();
});
});
@ -236,7 +236,7 @@ describe('TextArea allowClear', () => {
it('should not support allowClear when it is disabled', () => {
const wrapper = mount(<TextArea allowClear defaultValue="111" disabled />);
expect(wrapper.find('.ant-input-textarea-clear-icon').length).toBe(0);
expect(wrapper.find('.ant-input-textarea-clear-icon-hidden').exists()).toBeTruthy();
});
it('not block input when `value` is undefined', () => {

View File

@ -19,6 +19,10 @@
+ i {
margin-left: 6px;
}
&-hidden {
visibility: hidden;
}
}
// ========================= Input =========================

View File

@ -8,27 +8,40 @@
.@{ant-prefix}-input {
.reset-component;
.input;
}
//== Style for input-group: input with label, with button or dropdown...
.@{ant-prefix}-input-group {
.reset-component;
.input-group(~'@{ant-prefix}-input');
&-wrapper {
display: inline-block;
width: 100%;
text-align: start;
vertical-align: top; // https://github.com/ant-design/ant-design/issues/6403
//== Style for input-group: input with label, with button or dropdown...
&-group {
.reset-component;
.input-group(~'@{ant-prefix}-input');
&-wrapper {
display: inline-block;
width: 100%;
text-align: start;
vertical-align: top; // https://github.com/ant-design/ant-design/issues/6403
}
}
}
.@{ant-prefix}-input-password-icon {
color: @text-color-secondary;
cursor: pointer;
transition: all 0.3s;
&-password-icon {
color: @text-color-secondary;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: @input-icon-hover-color;
&:hover {
color: @input-icon-hover-color;
}
}
&[type='color'] {
height: @input-height-base;
&.@{ant-prefix}-input-lg {
height: @input-height-lg;
}
&.@{ant-prefix}-input-sm {
height: @input-height-sm;
padding-top: 3px;
padding-bottom: 3px;
}
}
}

View File

@ -23,10 +23,10 @@ A list can be used to display content related to a single subject. The content c
| header | List header renderer | string\|ReactNode | - | |
| itemLayout | The layout of list, default is `horizontal`, If a vertical list is desired, set the itemLayout property to `vertical` | string | - | |
| rowKey | Item's unique key, could be a string or function that returns a string | string\|Function(record):string | `key` | |
| loading | Shows a loading indicator while the contents of the list are being fetched | boolean\|[SpinProps](https://ant.design/components/spin/#API) ([more](https://github.com/ant-design/ant-design/issues/8659)) | false | |
| loading | Shows a loading indicator while the contents of the list are being fetched | boolean\|[SpinProps](/components/spin/#API) ([more](https://github.com/ant-design/ant-design/issues/8659)) | false | |
| loadMore | Shows a load more content | string\|ReactNode | - | |
| locale | i18n text including empty text | object | emptyText: 'No Data' <br> | |
| pagination | Pagination [config](https://ant.design/components/pagination/), hide it by setting it to false | boolean \| object | false | |
| pagination | Pagination [config](/components/pagination/), hide it by setting it to false | boolean \| object | false | |
| size | Size of list | `default` \| `large` \| `small` | `default` | |
| split | Toggles rendering of the split under the list item | boolean | true | |
| dataSource | dataSource array for list | any[] | - | |

View File

@ -23,7 +23,7 @@ cols: 1
| grid | 列表栅格配置 | [object](#List-grid-props) | - | |
| header | 列表头部 | string\|ReactNode | - | |
| itemLayout | 设置 `List.Item` 布局, 设置成 `vertical` 则竖直样式显示, 默认横排 | string | - | |
| loading | 当卡片内容还在加载中时,可以用 `loading` 展示一个占位 | boolean\|[object](https://ant.design/components/spin-cn/#API) ([更多](https://github.com/ant-design/ant-design/issues/8659)) | false | |
| loading | 当卡片内容还在加载中时,可以用 `loading` 展示一个占位 | boolean\|[object](/components/spin/#API) ([更多](https://github.com/ant-design/ant-design/issues/8659)) | false | |
| loadMore | 加载更多 | string\|ReactNode | - | |
| locale | 默认文案设置,目前包括空数据文案 | object | emptyText: '暂无数据' | |
| pagination | 对应的 `pagination` 配置, 设置 `false` 不显示 | boolean\|object | false | |

View File

@ -1,4 +1,5 @@
import * as React from 'react';
import { ValidateMessages } from 'rc-field-form/lib/interface';
import warning from '../_util/warning';
import { ModalLocale, changeConfirmLocale } from '../modal/locale';
@ -30,6 +31,9 @@ export interface Locale {
PageHeader?: Object;
Icon?: Object;
Text?: Object;
Form?: {
defaultValidateMessages: ValidateMessages;
};
}
export interface LocaleProviderProps {

View File

@ -10,12 +10,22 @@ const localeValues: Locale = {
DatePicker,
TimePicker,
Calendar,
global: {
placeholder: 'אנא בחר',
},
Table: {
filterTitle: 'תפריט סינון',
filterConfirm: 'אישור',
filterReset: 'איפוס',
selectAll: 'בחר הכל',
selectInvert: 'הפוך בחירה',
selectionAll: 'בחר את כל הנתונים',
sortTitle: 'מיון',
expand: 'הרחב שורה',
collapse: 'צמצם שורהw',
triggerDesc: 'לחץ על מיון לפי סדר יורד',
triggerAsc: 'לחץ על מיון לפי סדר עולה',
cancelSort: 'לחץ כדי לבטל את המיון',
},
Modal: {
okText: 'אישור',
@ -41,6 +51,18 @@ const localeValues: Locale = {
Empty: {
description: 'אין מידע',
},
Icon: {
icon: 'סמל',
},
Text: {
edit: 'ערוך',
copy: 'העתק',
copied: 'הועתק',
expand: 'הרחב',
},
PageHeader: {
back: 'חזרה',
},
};
export default localeValues;

View File

@ -10,6 +10,9 @@ const localeValues: Locale = {
DatePicker,
TimePicker,
Calendar,
global: {
placeholder: 'Пожалуйста выберите',
},
Table: {
filterTitle: 'Фильтр',
filterConfirm: 'OK',
@ -17,6 +20,11 @@ const localeValues: Locale = {
selectAll: 'Выбрать всё',
selectInvert: 'Инвертировать выбор',
sortTitle: 'Сортировка',
expand: 'Развернуть строку',
collapse: 'Свернуть строку',
triggerDesc: 'Нажмите для сортировки по убыванию',
triggerAsc: 'Нажмите для сортировки по возрастанию',
cancelSort: 'Нажмите, чтобы отменить сортировку',
},
Modal: {
okText: 'OK',
@ -42,6 +50,9 @@ const localeValues: Locale = {
Empty: {
description: 'Нет данных',
},
Icon: {
icon: 'иконка',
},
Text: {
edit: 'редактировать',
copy: 'копировать',

View File

@ -1,9 +1,12 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/zh_CN';
import DatePicker from '../date-picker/locale/zh_CN';
import TimePicker from '../time-picker/locale/zh_CN';
import Calendar from '../calendar/locale/zh_CN';
import { Locale } from '../locale-provider';
const typeTemplate = '${label}不是一个有效的${type}';
const localeValues: Locale = {
locale: 'zh-cn',
Pagination,
@ -64,6 +67,55 @@ const localeValues: Locale = {
PageHeader: {
back: '返回',
},
Form: {
defaultValidateMessages: {
default: '字段验证错误${label}',
required: '请输入${label}',
enum: '${label}必须是其中一个[${enum}]',
whitespace: '${label}不能为空字符',
date: {
format: '${label}日期格式无效',
parse: '${label}不能转换为日期',
invalid: '${label}是一个无效日期',
},
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}须为${len}个字符',
min: '${label}最少${min}个字符',
max: '${label}最多${max}个字符',
range: '${label}须在${min}-${max}字符之间',
},
number: {
len: '${label}必须等于${len}',
min: '${label}最小值为${min}',
max: '${label}最大值为${max}',
range: '${label}须在${min}-${max}之间',
},
array: {
len: '须为${len}个${label}',
min: '最少${min}个${label}',
max: '最多${max}个${label}',
range: '${label}数量须在${min}-${max}之间',
},
pattern: {
mismatch: '${label}与模式不匹配${pattern}',
},
},
},
};
export default localeValues;

View File

@ -126,30 +126,26 @@ exports[`renders ./components/menu/demo/horizontal.md correctly 1`] = `
role="button"
>
<span
class="submenu-title-wrapper"
aria-label="setting"
class="anticon anticon-setting"
role="img"
>
<span
aria-label="setting"
class="anticon anticon-setting"
role="img"
<svg
aria-hidden="true"
class=""
data-icon="setting"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<svg
aria-hidden="true"
class=""
data-icon="setting"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"
/>
</svg>
</span>
Navigation Three - Submenu
<path
d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"
/>
</svg>
</span>
Navigation Three - Submenu
<i
class="ant-menu-submenu-arrow"
/>

View File

@ -15,11 +15,7 @@ Horizontal top navigation menu.
```jsx
import { Menu } from 'antd';
import {
MailOutlined,
AppstoreOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { MailOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons';
const { SubMenu } = Menu;
@ -48,10 +44,10 @@ class App extends React.Component {
</Menu.Item>
<SubMenu
title={
<span className="submenu-title-wrapper">
<>
<SettingOutlined />
Navigation Three - Submenu
</span>
</>
}
>
<Menu.ItemGroup title="Item 1">

View File

@ -210,10 +210,6 @@
z-index: @zindex-dropdown;
border-radius: @border-radius-base;
.submenu-title-wrapper {
padding-right: 20px;
}
&::before {
position: absolute;
top: -7px;

View File

@ -22,6 +22,15 @@
}
}
&-dark {
.@{menu-prefix-cls}-inline,
.@{menu-prefix-cls}-vertical {
.@{menu-prefix-cls}-rtl& {
border-left: none;
}
}
}
&-vertical&-sub,
&-vertical-left&-sub,
&-vertical-right&-sub {
@ -56,15 +65,6 @@
}
&-submenu {
&-popup {
.submenu-title-wrapper {
.@{menu-prefix-cls}-submenu-rtl& {
padding-right: 0;
padding-left: 20px;
}
}
}
&-vertical,
&-vertical-left,
&-vertical-right,
@ -134,6 +134,14 @@
}
}
&-inline-collapsed&-vertical {
.@{menu-prefix-cls}-submenu-title {
.@{menu-prefix-cls}-rtl& {
padding: 0 (@menu-collapsed-width - @menu-icon-size-lg) / 2;
}
}
}
&-item-group-list {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {

View File

@ -35,55 +35,59 @@ export default class ActionButton extends React.Component<ActionButtonProps, Act
clearTimeout(this.timeoutId);
}
handlePromiseOnOk(returnValueOfOnOk?: PromiseLike<any>) {
const { closeModal } = this.props;
if (!returnValueOfOnOk || !returnValueOfOnOk.then) {
return;
}
this.setState({ loading: true });
returnValueOfOnOk.then(
(...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close.
// this.setState({ loading: false });
closeModal(...args);
},
(e: Error) => {
// Emit error when catch promise reject
// eslint-disable-next-line no-console
console.error(e);
// See: https://github.com/ant-design/ant-design/issues/6183
this.setState({ loading: false });
this.clicked = false;
},
);
}
onClick = () => {
const { actionFn, closeModal } = this.props;
if (this.clicked) {
return;
}
this.clicked = true;
if (actionFn) {
let ret;
if (actionFn.length) {
ret = actionFn(closeModal);
} else {
ret = actionFn();
if (!ret) {
closeModal();
}
}
if (ret && ret.then) {
this.setState({ loading: true });
ret.then(
(...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close.
// this.setState({ loading: false });
closeModal(...args);
},
(e: Error) => {
// Emit error when catch promise reject
// eslint-disable-next-line no-console
console.error(e);
// See: https://github.com/ant-design/ant-design/issues/6183
this.setState({ loading: false });
this.clicked = false;
},
);
}
} else {
if (!actionFn) {
closeModal();
return;
}
let returnValueOfOnOk;
if (actionFn.length) {
returnValueOfOnOk = actionFn(closeModal);
// https://github.com/ant-design/ant-design/issues/23358
this.clicked = false;
} else {
returnValueOfOnOk = actionFn();
if (!returnValueOfOnOk) {
closeModal();
return;
}
}
this.handlePromiseOnOk(returnValueOfOnOk);
};
render() {
const { type, children, buttonProps } = this.props;
const { loading } = this.state;
return (
<Button
type={type}
onClick={this.onClick}
loading={loading}
{...buttonProps}
>
<Button type={type} onClick={this.onClick} loading={loading} {...buttonProps}>
{children}
</Button>
);

View File

@ -9,6 +9,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
afterEach(() => {
errorSpy.mockReset();
document.body.innerHTML = '';
Modal.destroyAll();
});
afterAll(() => {
@ -74,12 +75,19 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect(errorSpy).not.toHaveBeenCalled();
});
it('should not hide confirm when onOk return Promise.resolve', () => {
open({
onOk: () => Promise.resolve(''),
});
$$('.ant-btn-primary')[0].click();
expect($$('.ant-modal-confirm')).toHaveLength(1);
});
it('should emit error when onOk return Promise.reject', () => {
const error = new Error('something wrong');
open({
onOk: () => Promise.reject(error),
});
// Fifth Modal
$$('.ant-btn-primary')[0].click();
// wait promise
return Promise.resolve().then(() => {
@ -125,6 +133,22 @@ describe('Modal.confirm triggers callbacks correctly', () => {
jest.useRealTimers();
});
it('should not close modals when click confirm button when onOk has argument', () => {
jest.useFakeTimers();
['info', 'success', 'warning', 'error'].forEach(type => {
Modal[type]({
title: 'title',
content: 'content',
onOk: close => null, // eslint-disable-line no-unused-vars
});
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
$$('.ant-btn')[0].click();
jest.runAllTimers();
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
});
jest.useRealTimers();
});
it('could be update', () => {
jest.useFakeTimers();
['info', 'success', 'warning', 'error'].forEach(type => {
@ -235,9 +259,23 @@ describe('Modal.confirm triggers callbacks correctly', () => {
it('ok button should trigger onOk once when click it many times quickly', () => {
const onOk = jest.fn();
open({ onOk });
// Fifth Modal
$$('.ant-btn-primary')[0].click();
$$('.ant-btn-primary')[0].click();
expect(onOk).toHaveBeenCalledTimes(1);
});
// https://github.com/ant-design/ant-design/issues/23358
it('ok button should trigger onOk multiple times when onOk has close argument', () => {
const onOk = jest.fn();
open({
onOk: close => {
onOk();
(() => {})(close); // do nothing
},
});
$$('.ant-btn-primary')[0].click();
$$('.ant-btn-primary')[0].click();
$$('.ant-btn-primary')[0].click();
expect(onOk).toHaveBeenCalledTimes(3);
});
});

View File

@ -75,8 +75,8 @@ The items listed above are all functions, expecting a settings object as paramet
| title | Title | string\|ReactNode | - |
| width | Width of the modal dialog | string\|number | 416 |
| zIndex | The `z-index` of the Modal | Number | 1000 |
| onCancel | Specify a function that will be called when the user clicks the Cancel button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function | - |
| onOk | Specify a function that will be called when the user clicks the OK button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function | - |
| onCancel | Specify a function that will be called when the user clicks the Cancel button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - |
| onOk | Specify a function that will be called when the user clicks the OK button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - |
All the `Modal.method`s will return a reference, and then we can update and close the modal dialog by the reference.

View File

@ -77,8 +77,8 @@ title: Modal
| title | 标题 | string\|ReactNode | - |
| width | 宽度 | string\|number | 416 |
| zIndex | 设置 Modal 的 `z-index` | Number | 1000 |
| onCancel | 取消回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function | - |
| onOk | 点击确定回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function | - |
| onCancel | 取消回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - |
| onOk | 点击确定回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - |
以上函数调用后,会返回一个引用,可以通过该引用更新和关闭弹窗。

View File

@ -4,11 +4,26 @@ import PageHeader from '..';
import ConfigProvider from '../../config-provider';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { spyElementPrototypes } from '../../__tests__/util/domHook';
describe('PageHeader', () => {
mountTest(PageHeader);
rtlTest(PageHeader);
let spy;
beforeAll(() => {
spy = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
width: 100,
}),
});
});
afterAll(() => {
spy.mockRestore();
});
it('pageHeader should not contain back it back', () => {
const routes = [
{
@ -101,4 +116,11 @@ describe('PageHeader', () => {
expect(render(wrapper)).toMatchSnapshot();
});
it('change container width', () => {
const wrapper = mount(<PageHeader title="Page Title" extra="extra" />);
wrapper.triggerResize();
wrapper.update();
expect(wrapper.find('.ant-page-header').hasClass('ant-page-header-compact')).toBe(true);
});
});

View File

@ -21,9 +21,9 @@ PageHeader can be used to highlight the page topic, display important informatio
| ghost | PageHeader type, will change background color | boolean | true | |
| avatar | Avatar next to the title bar | [avatar props](/components/avatar/) | - | |
| backIcon | Custom back icon, if false the back icon will not be displayed | ReactNode \| boolean | `<ArrowLeft />` | |
| tags | Tag list next to title | [Tag](https://ant.design/components/tag-cn/)[] \| [Tag](https://ant.design/components/tag-cn/) | - | |
| tags | Tag list next to title | [Tag](/components/tag/)[] \| [Tag](/components/tag/) | - | |
| extra | Operating area, at the end of the line of the title line | ReactNode | - | |
| breadcrumb | Breadcrumb configuration | [breadcrumb](https://ant.design/components/breadcrumb-cn/) | - | |
| breadcrumb | Breadcrumb configuration | [breadcrumb](/components/breadcrumb/) | - | |
| footer | PageHeader's footer, generally used to render TabBar | ReactNode | - | |
| onBack | Back icon click event | `()=>void` | `()=>history.back()` | |

View File

@ -116,7 +116,7 @@ const renderChildren = (prefixCls: string, children: React.ReactNode) => {
const PageHeader: React.FC<PageHeaderProps> = props => {
const [compact, updateCompact] = React.useState(false);
const onResize = ({ width }: { width: number }) => {
updateCompact(width < 540);
updateCompact(width < 768);
};
return (
<ConfigConsumer>

View File

@ -21,9 +21,9 @@ subtitle: 页头
| ghost | pageHeader 的类型,将会改变背景颜色 | boolean | true | |
| avatar | 标题栏旁的头像 | [avatar props](/components/avatar/) | - | |
| backIcon | 自定义 back icon ,如果为 false 不渲染 back icon | ReactNode \| boolean | `<ArrowLeft />` | |
| tags | title 旁的 tag 列表 | [Tag](https://ant.design/components/tag-cn/)[] \| [Tag](https://ant.design/components/tag-cn/) | - | |
| tags | title 旁的 tag 列表 | [Tag](/components/tag/)[] \| [Tag](/components/tag/) | - | |
| extra | 操作区,位于 title 行的行尾 | ReactNode | - | |
| breadcrumb | 面包屑的配置 | [breadcrumb](https://ant.design/components/breadcrumb-cn/) | - | |
| breadcrumb | 面包屑的配置 | [breadcrumb](/components/breadcrumb/) | - | |
| footer | PageHeader 的页脚,一般用于渲染 TabBar | ReactNode | - | |
| onBack | 返回按钮的点击事件 | `()=>void` | `()=>history.back()` | |

View File

@ -56,6 +56,7 @@
&-left {
display: flex;
align-items: center;
margin: (@margin-xs / 2) 0;
overflow: hidden;
}
@ -82,7 +83,9 @@
}
&-extra {
margin: (@margin-xs / 2) 0;
white-space: nowrap;
> * {
margin-left: @margin-sm;
white-space: unset;
@ -112,11 +115,7 @@
}
&-compact &-heading {
flex-direction: column;
&-extra {
margin-top: @margin-xs;
}
flex-wrap: wrap;
}
}

View File

@ -25,7 +25,7 @@ The difference with the `confirm` modal dialog is that it's more lightweight tha
| icon | customize icon of confirmation | ReactNode | `<ExclamationCircle />` |
| disabled | is show popconfirm when click its childrenNode | boolean | false |
Consult [Tooltip's documentation](https://ant.design/components/tooltip/#API) to find more APIs.
Consult [Tooltip's documentation](/components/tooltip/#API) to find more APIs.
## Note

View File

@ -19,7 +19,7 @@ Comparing with `Tooltip`, besides information `Popover` card can also provide ac
| content | Content of the card | string\|ReactNode\|() => ReactNode | - | |
| title | Title of the card | string\|ReactNode\|() => ReactNode | - | |
Consult [Tooltip's documentation](https://ant.design/components/tooltip/#API) to find more APIs.
Consult [Tooltip's documentation](/components/tooltip/#API) to find more APIs.
## Note

View File

@ -34,9 +34,9 @@ Select component to select value from options.
| dropdownRender | Customize dropdown content | (menuNode: ReactNode, props) => ReactNode | - | |
| dropdownStyle | style of dropdown menu | object | - | |
| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns `true`, the option will be included in the filtered set; Otherwise, it will be excluded. | boolean or function(inputValue, option) | true | |
| firstActiveValue | Value of action option by default | string\|string\[] | - | |
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative. [Example](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | |
| labelInValue | whether to embed label in value, turn the format of value from `string` to `{key: string, label: ReactNode}` | boolean | false | |
| listHeight | Config popup height | number | 256 | |
| maxTagCount | Max tag count to show | number | - | |
| maxTagTextLength | Max tag text length to show | number | - | |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode/function(omittedValues) | - | |

View File

@ -35,9 +35,9 @@ title: Select
| dropdownRender | 自定义下拉框内容 | (menuNode: ReactNode, props) => ReactNode | - | |
| dropdownStyle | 下拉菜单的 style 属性 | object | - | |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | boolean or function(inputValue, option) | true | |
| firstActiveValue | 默认高亮的选项 | string\|string\[] | - | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codesandbox.io/s/4j168r7jw0) | Function(triggerNode) | () => document.body | |
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 `{key: string, label: ReactNode}` 的格式 | boolean | false | |
| listHeight | 设置弹窗滚动高度 | number | 256 | |
| maxTagCount | 最多显示多少个 tag | number | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode/function(omittedValues) | - | |

View File

@ -98,7 +98,6 @@
// ========================== Arrow ==========================
&-arrow {
.iconfont-mixin();
position: absolute;
top: 53%;
right: @control-padding-horizontal - 1px;
@ -109,8 +108,6 @@
font-size: @font-size-sm;
line-height: 1;
text-align: center;
// transform-origin: 50% 50%;
pointer-events: none;
.anticon {
vertical-align: top;
@ -124,6 +121,10 @@
transform: rotate(180deg);
}
}
&.anticon-down {
pointer-events: none;
}
}
// ========================== Clear ==========================

View File

@ -13,170 +13,178 @@
* since chrome may update to redesign with its align logic.
*/
.@{select-prefix-cls}-multiple {
// ========================= Selector =========================
.@{select-prefix-cls}-selector {
.select-selector();
.select-search-input-without-border();
.@{select-prefix-cls} {
&-multiple {
// ========================= Selector =========================
.@{select-prefix-cls}-selector {
.select-selector();
.select-search-input-without-border();
display: flex;
flex-wrap: wrap;
align-items: center;
// Multiple is little different that horizontal is follow the vertical
padding: @select-multiple-padding @input-padding-vertical-base;
display: flex;
flex-wrap: wrap;
align-items: center;
// Multiple is little different that horizontal is follow the vertical
padding: @select-multiple-padding @input-padding-vertical-base;
.@{select-prefix-cls}-show-search& {
cursor: text;
}
&::after {
display: inline-block;
width: 0;
margin: @select-multiple-item-spacing-half 0;
line-height: @select-multiple-item-height;
content: '\a0';
}
}
&.@{select-prefix-cls}-allow-clear .@{select-prefix-cls}-selector {
padding-right: @font-size-sm + @control-padding-horizontal;
}
// ======================== Selections ========================
.@{select-prefix-cls}-selection-item {
position: relative;
display: flex;
flex: none;
box-sizing: border-box;
max-width: 100%;
height: @select-multiple-item-height;
margin-top: @select-multiple-item-spacing-half;
margin-right: @input-padding-vertical-base;
margin-bottom: @select-multiple-item-spacing-half;
padding: 0 (@padding-xs / 2) 0 @padding-xs;
line-height: @select-multiple-item-height - @select-multiple-item-border-width * 2;
background: @select-selection-item-bg;
border: 1px solid @select-selection-item-border-color;
border-radius: @border-radius-base;
cursor: default;
transition: font-size 0.3s, line-height 0.3s, height 0.3s;
user-select: none;
// It's ok not to do this, but 24px makes bottom narrow in view should adjust
&-content {
display: inline-block;
margin-right: @padding-xs / 2;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&-remove {
.iconfont-mixin();
display: inline-block;
color: @text-color-secondary;
font-weight: bold;
font-size: @font-size-sm;
line-height: inherit;
cursor: pointer;
.iconfont-size-under-12px(10px);
&:hover {
color: @icon-color-hover;
}
}
}
// ========================== Input ==========================
.@{select-prefix-cls}-selection-search {
position: relative;
margin-left: @select-multiple-padding / 2;
&-input,
&-mirror {
font-family: @font-family;
line-height: @line-height-base;
transition: all 0.3s;
}
&-input {
width: 100%;
}
&-mirror {
position: absolute;
top: 0;
left: 0;
z-index: 999;
white-space: nowrap;
visibility: hidden;
}
// https://github.com/ant-design/ant-design/issues/22906
&:first-child .@{select-prefix-cls}-selection-search-input {
margin-left: 6.5px;
}
}
// ======================= Placeholder =======================
.@{select-prefix-cls}-selection-placeholder {
position: absolute;
top: 50%;
right: @input-padding-horizontal;
left: @input-padding-horizontal;
transform: translateY(-50%);
transition: all 0.3s;
}
// ============================================================
// == Size ==
// ============================================================
.select-size(@suffix, @input-height) {
@merged-cls: ~'@{select-prefix-cls}-@{suffix}';
&.@{merged-cls} {
@select-selection-height: @input-height - @input-padding-vertical-base * 2;
@select-height-without-border: @input-height - @border-width-base * 2;
.@{select-prefix-cls}-selector::after {
line-height: @select-selection-height;
.@{select-prefix-cls}-show-search& {
cursor: text;
}
.@{select-prefix-cls}-selection-item {
height: @select-selection-height;
line-height: @select-selection-height - @border-width-base * 2;
&::after {
display: inline-block;
width: 0;
margin: @select-multiple-item-spacing-half 0;
line-height: @select-multiple-item-height;
content: '\a0';
}
}
&.@{select-prefix-cls}-allow-clear .@{select-prefix-cls}-selector {
padding-right: @font-size-sm + @control-padding-horizontal;
}
// ======================== Selections ========================
.@{select-prefix-cls}-selection-item {
position: relative;
display: flex;
flex: none;
box-sizing: border-box;
max-width: 100%;
height: @select-multiple-item-height;
margin-top: @select-multiple-item-spacing-half;
margin-right: @input-padding-vertical-base;
margin-bottom: @select-multiple-item-spacing-half;
padding: 0 (@padding-xs / 2) 0 @padding-xs;
line-height: @select-multiple-item-height - @select-multiple-item-border-width * 2;
background: @select-selection-item-bg;
border: 1px solid @select-selection-item-border-color;
border-radius: @border-radius-base;
cursor: default;
transition: font-size 0.3s, line-height 0.3s, height 0.3s;
user-select: none;
// It's ok not to do this, but 24px makes bottom narrow in view should adjust
&-content {
display: inline-block;
margin-right: @padding-xs / 2;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.@{select-prefix-cls}-selection-search {
height: @select-selection-height + @select-multiple-padding;
line-height: @select-selection-height + @select-multiple-padding;
&-remove {
.iconfont-mixin();
&-input,
&-mirror {
height: @select-selection-height;
line-height: @select-selection-height - @border-width-base * 2;
display: inline-block;
color: @text-color-secondary;
font-weight: bold;
font-size: @font-size-sm;
line-height: inherit;
cursor: pointer;
.iconfont-size-under-12px(10px);
&:hover {
color: @icon-color-hover;
}
}
}
}
.select-size('lg', @input-height-lg);
.select-size('sm', @input-height-sm);
// ========================== Input ==========================
.@{select-prefix-cls}-selection-search {
position: relative;
margin-left: @select-multiple-padding / 2;
// Size small need additional set padding
&.@{select-prefix-cls}-sm {
&-input,
&-mirror {
font-family: @font-family;
line-height: @line-height-base;
transition: all 0.3s;
}
&-input {
width: 100%;
min-width: 3px;
}
&-mirror {
position: absolute;
top: 0;
left: 0;
z-index: 999;
white-space: nowrap;
visibility: hidden;
}
// https://github.com/ant-design/ant-design/issues/22906
&:first-child .@{select-prefix-cls}-selection-search-input {
margin-left: 6.5px;
}
}
// ======================= Placeholder =======================
.@{select-prefix-cls}-selection-placeholder {
left: @input-padding-horizontal-sm;
position: absolute;
top: 50%;
right: @input-padding-horizontal;
left: @input-padding-horizontal;
transform: translateY(-50%);
transition: all 0.3s;
}
// https://github.com/ant-design/ant-design/issues/22906
.@{select-prefix-cls}-selection-search:first-child .@{select-prefix-cls}-selection-search-input {
margin-left: 3px;
// ============================================================
// == Size ==
// ============================================================
.select-size(@suffix, @input-height) {
@merged-cls: ~'@{select-prefix-cls}-@{suffix}';
&.@{merged-cls} {
@select-selection-height: @input-height - @input-padding-vertical-base * 2;
@select-height-without-border: @input-height - @border-width-base * 2;
.@{select-prefix-cls}-selector::after {
line-height: @select-selection-height;
}
.@{select-prefix-cls}-selection-item {
height: @select-selection-height;
line-height: @select-selection-height - @border-width-base * 2;
}
.@{select-prefix-cls}-selection-search {
height: @select-selection-height + @select-multiple-padding;
line-height: @select-selection-height + @select-multiple-padding;
&-input,
&-mirror {
height: @select-selection-height;
line-height: @select-selection-height - @border-width-base * 2;
}
}
}
}
.select-size('lg', @input-height-lg);
.select-size('sm', @input-height-sm);
// Size small need additional set padding
&.@{select-prefix-cls}-sm {
.@{select-prefix-cls}-selection-placeholder {
left: @input-padding-horizontal-sm;
}
// https://github.com/ant-design/ant-design/issues/22906
.@{select-prefix-cls}-selection-search:first-child
.@{select-prefix-cls}-selection-search-input {
margin-left: 3px;
}
}
&.@{select-prefix-cls}-lg {
.@{select-prefix-cls}-selection-item {
height: @select-multiple-item-height-lg;
line-height: @select-multiple-item-height-lg;
}
}
}
&.@{select-prefix-cls}-lg {
.@{select-prefix-cls}-selection-item {
height: @select-multiple-item-height-lg;
line-height: @select-multiple-item-height-lg;
}
&-disabled .@{select-prefix-cls}-selection-item-remove {
display: none;
}
}

View File

@ -28,8 +28,7 @@
// ========================== Popup ==========================
&-dropdown {
&-rtl {
direction: ltr;
text-align: right;
direction: rtl;
}
}

View File

@ -117,7 +117,7 @@
.@{select-prefix-cls}-selection-item,
.@{select-prefix-cls}-selection-placeholder {
line-height: @input-height;
line-height: @input-height - 2 * @border-width-base;
}
}

View File

@ -8,17 +8,11 @@ export interface AvatarProps extends Omit<SkeletonElementProps, 'shape'> {
shape?: 'circle' | 'square';
}
// eslint-disable-next-line react/prefer-stateless-function
class SkeletonAvatar extends React.Component<AvatarProps, any> {
static defaultProps: Partial<AvatarProps> = {
size: 'default',
shape: 'circle',
};
renderSkeletonAvatar = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = this.props;
const SkeletonAvatar = (props: AvatarProps) => {
const renderSkeletonAvatar = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
const otherProps = omit(this.props, ['prefixCls']);
const otherProps = omit(props, ['prefixCls']);
const cls = classNames(prefixCls, className, `${prefixCls}-element`, {
[`${prefixCls}-active`]: active,
});
@ -28,10 +22,12 @@ class SkeletonAvatar extends React.Component<AvatarProps, any> {
</div>
);
};
render() {
return <ConfigConsumer>{this.renderSkeletonAvatar}</ConfigConsumer>;
}
return <ConfigConsumer>{renderSkeletonAvatar}</ConfigConsumer>;
}
SkeletonAvatar.defaultProps = {
size: 'default',
shape: 'circle',
};
export default SkeletonAvatar;

View File

@ -4,20 +4,15 @@ import classNames from 'classnames';
import Element, { SkeletonElementProps } from './Element';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
interface SkeletonButtonProps extends Omit<SkeletonElementProps, 'size'> {
export interface SkeletonButtonProps extends Omit<SkeletonElementProps, 'size'> {
size?: 'large' | 'small' | 'default';
}
// eslint-disable-next-line react/prefer-stateless-function
class SkeletonButton extends React.Component<SkeletonButtonProps, any> {
static defaultProps: Partial<SkeletonButtonProps> = {
size: 'default',
};
renderSkeletonButton = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = this.props;
const SkeletonButton = (props: SkeletonButtonProps) => {
const renderSkeletonButton = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
const otherProps = omit(this.props, ['prefixCls']);
const otherProps = omit(props, ['prefixCls']);
const cls = classNames(prefixCls, className, `${prefixCls}-element`, {
[`${prefixCls}-active`]: active,
});
@ -27,10 +22,11 @@ class SkeletonButton extends React.Component<SkeletonButtonProps, any> {
</div>
);
};
return <ConfigConsumer>{renderSkeletonButton}</ConfigConsumer>;
};
render() {
return <ConfigConsumer>{this.renderSkeletonButton}</ConfigConsumer>;
}
}
SkeletonButton.defaultProps = {
size: 'default',
};
export default SkeletonButton;

View File

@ -10,37 +10,35 @@ export interface SkeletonElementProps {
active?: boolean;
}
// eslint-disable-next-line react/prefer-stateless-function
class Element extends React.Component<SkeletonElementProps, any> {
render() {
const { prefixCls, className, style, size, shape } = this.props;
const Element = (props: SkeletonElementProps) => {
const { prefixCls, className, style, size, shape } = props;
const sizeCls = classNames({
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
});
const sizeCls = classNames({
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
});
const shapeCls = classNames({
[`${prefixCls}-circle`]: shape === 'circle',
[`${prefixCls}-square`]: shape === 'square',
[`${prefixCls}-round`]: shape === 'round',
});
const shapeCls = classNames({
[`${prefixCls}-circle`]: shape === 'circle',
[`${prefixCls}-square`]: shape === 'square',
[`${prefixCls}-round`]: shape === 'round',
});
const sizeStyle: React.CSSProperties =
typeof size === 'number'
? {
width: size,
height: size,
lineHeight: `${size}px`,
}
: {};
return (
<span
className={classNames(prefixCls, className, sizeCls, shapeCls)}
style={{ ...sizeStyle, ...style }}
/>
);
};
const sizeStyle: React.CSSProperties =
typeof size === 'number'
? {
width: size,
height: size,
lineHeight: `${size}px`,
}
: {};
return (
<span
className={classNames(prefixCls, className, sizeCls, shapeCls)}
style={{ ...sizeStyle, ...style }}
/>
);
}
}
export default Element;

View File

@ -4,20 +4,15 @@ import classNames from 'classnames';
import Element, { SkeletonElementProps } from './Element';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
interface SkeletonInputProps extends Omit<SkeletonElementProps, 'size' | 'shape'> {
export interface SkeletonInputProps extends Omit<SkeletonElementProps, 'size' | 'shape'> {
size?: 'large' | 'small' | 'default';
}
// eslint-disable-next-line react/prefer-stateless-function
class SkeletonInput extends React.Component<SkeletonInputProps, any> {
static defaultProps: Partial<SkeletonInputProps> = {
size: 'default',
};
renderSkeletonInput = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = this.props;
const SkeletonInput = (props: SkeletonInputProps) => {
const renderSkeletonInput = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
const otherProps = omit(this.props, ['prefixCls']);
const otherProps = omit(props, ['prefixCls']);
const cls = classNames(prefixCls, className, `${prefixCls}-element`, {
[`${prefixCls}-active`]: active,
});
@ -27,10 +22,11 @@ class SkeletonInput extends React.Component<SkeletonInputProps, any> {
</div>
);
};
return <ConfigConsumer>{renderSkeletonInput}</ConfigConsumer>;
};
render() {
return <ConfigConsumer>{this.renderSkeletonInput}</ConfigConsumer>;
}
}
SkeletonInput.defaultProps = {
size: 'default',
};
export default SkeletonInput;

View File

@ -11,9 +11,9 @@ export interface SkeletonParagraphProps {
rows?: number;
}
class Paragraph extends React.Component<SkeletonParagraphProps, {}> {
getWidth(index: number) {
const { width, rows = 2 } = this.props;
const Paragraph = (props: SkeletonParagraphProps) => {
const getWidth = (index: number) => {
const { width, rows = 2 } = props;
if (Array.isArray(width)) {
return width[index];
}
@ -22,20 +22,17 @@ class Paragraph extends React.Component<SkeletonParagraphProps, {}> {
return width;
}
return undefined;
}
render() {
const { prefixCls, className, style, rows } = this.props;
const rowList = [...Array(rows)].map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<li key={index} style={{ width: this.getWidth(index) }} />
));
return (
<ul className={classNames(prefixCls, className)} style={style}>
{rowList}
</ul>
);
}
}
};
const { prefixCls, className, style, rows } = props;
const rowList = [...Array(rows)].map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<li key={index} style={{ width: getWidth(index) }}/>
));
return (
<ul className={classNames(prefixCls, className)} style={style}>
{rowList}
</ul>
);
};
export default Paragraph;

View File

@ -3,13 +3,14 @@ import classNames from 'classnames';
import Title, { SkeletonTitleProps } from './Title';
import Paragraph, { SkeletonParagraphProps } from './Paragraph';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import SkeletonButton from './Button';
import Element from './Element';
import SkeletonAvatar, { AvatarProps } from './Avatar';
import SkeletonButton from './Button';
import SkeletonInput from './Input';
/* This only for skeleton internal. */
interface SkeletonAvatarProps extends Omit<AvatarProps, 'active'> {}
interface SkeletonAvatarProps extends Omit<AvatarProps, 'active'> {
}
export interface SkeletonProps {
active?: boolean;
@ -68,20 +69,8 @@ function getParagraphBasicProps(hasAvatar: boolean, hasTitle: boolean): Skeleton
return basicProps;
}
class Skeleton extends React.Component<SkeletonProps, any> {
static Button: typeof SkeletonButton;
static Avatar: typeof SkeletonAvatar;
static Input: typeof SkeletonInput;
static defaultProps: Partial<SkeletonProps> = {
avatar: false,
title: true,
paragraph: true,
};
renderSkeleton = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const Skeleton = (props: SkeletonProps) => {
const renderSkeleton = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
loading,
@ -91,11 +80,11 @@ class Skeleton extends React.Component<SkeletonProps, any> {
title,
paragraph,
active,
} = this.props;
} = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
if (loading || !('loading' in this.props)) {
if (loading || !('loading' in props)) {
const hasAvatar = !!avatar;
const hasTitle = !!title;
const hasParagraph = !!paragraph;
@ -166,10 +155,17 @@ class Skeleton extends React.Component<SkeletonProps, any> {
return children;
};
return <ConfigConsumer>{renderSkeleton}</ConfigConsumer>;
};
render() {
return <ConfigConsumer>{this.renderSkeleton}</ConfigConsumer>;
}
}
Skeleton.defaultProps = {
avatar: false,
title: true,
paragraph: true,
};
Skeleton.Button = SkeletonButton;
Skeleton.Avatar = SkeletonAvatar;
Skeleton.Input = SkeletonInput;
export default Skeleton;

View File

@ -1,11 +1,5 @@
import Skeleton from './Skeleton';
import SkeletonButton from './Button';
import SkeletonAvatar from './Avatar';
import SkeletonInput from './Input';
export { SkeletonProps } from './Skeleton';
Skeleton.Button = SkeletonButton;
Skeleton.Avatar = SkeletonAvatar;
Skeleton.Input = SkeletonInput;
export default Skeleton;

View File

@ -15,7 +15,7 @@ To input a value in a range.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| autoFocus | get focus when component mounted | boolean | false | |
| defaultValue | The default value of slider. When `range` is `false`, use `number`, otherwise, use `[number, number]` | number\|number\[] | 0 or \[0, 0] | |
| defaultValue | The default value of slider. When `range` is `false`, use `number`, otherwise, use `[number, number]` | number\|\[number, number] | 0 or \[0, 0] | |
| disabled | If true, the slider will not be interactable. | boolean | false | |
| dots | Whether the thumb can drag over tick only. | boolean | false | |
| included | Make effect when `marks` not null, `true` means containment and `false` means coordinative | boolean | true | |
@ -26,7 +26,7 @@ To input a value in a range.
| reverse | reverse the component | boolean | false | |
| step | The granularity the slider can step through values. Must greater than 0, and be divided by (max - min) . When `marks` no null, `step` can be `null`. | number\|null | 1 | |
| tipFormatter | Slider will pass its value to `tipFormatter`, and display its value in Tooltip, and hide Tooltip when return value is null. | Function\|null | IDENTITY | |
| value | The value of slider. When `range` is `false`, use `number`, otherwise, use `[number, number]` | number\|number\[] | |
| value | The value of slider. When `range` is `false`, use `number`, otherwise, use `[number, number]` | number\|\[number, number] | |
| vertical | If true, the slider will be vertical. | Boolean | false | |
| onAfterChange | Fire when `onmouseup` is fired. | Function(value) | NOOP | |
| onChange | Callback function that is fired when the user changes the slider's value. | Function(value) | NOOP | |

View File

@ -16,7 +16,7 @@ title: Slider
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| allowClear | 支持清除, 单选模式有效 | boolean | false | |
| defaultValue | 设置初始取值。当 `range``false` 时,使用 `number`,否则用 `[number, number]` | number\|number\[] | 0 or \[0, 0] | |
| defaultValue | 设置初始取值。当 `range``false` 时,使用 `number`,否则用 `[number, number]` | number\|\[number, number] | 0 or \[0, 0] | |
| disabled | 值为 `true` 时,滑块为禁用状态 | boolean | false | |
| dots | 是否只能拖拽到刻度上 | boolean | false | |
| included | `marks` 不为空对象时有效,值为 true 时表示值为包含关系false 表示并列 | boolean | true | |
@ -27,7 +27,7 @@ title: Slider
| reverse | 反向坐标轴 | boolean | false | |
| step | 步长,取值必须大于 0并且可被 (max - min) 整除。当 `marks` 不为空对象时,可以设置 `step``null`,此时 Slider 的可选值仅有 marks 标出来的部分。 | number\|null | 1 | |
| tipFormatter | Slider 会把当前值传给 `tipFormatter`,并在 Tooltip 中显示 `tipFormatter` 的返回值,若为 null则隐藏 Tooltip。 | Function\|null | IDENTITY | |
| value | 设置当前取值。当 `range``false` 时,使用 `number`,否则用 `[number, number]` | number\|number\[] | | |
| value | 设置当前取值。当 `range``false` 时,使用 `number`,否则用 `[number, number]` | number\|\[number, number] | | |
| vertical | 值为 `true`Slider 为垂直方向 | Boolean | false | |
| onAfterChange | 与 `onmouseup` 触发时机一致,把当前值作为参数传入。 | Function(value) | NOOP | |
| onChange | 当 Slider 的值发生改变时,会触发 onChange 事件,并把改变后的值作为参数传入。 | Function(value) | NOOP | |

View File

@ -10,28 +10,14 @@
margin: 0 0 0 @steps-desciption-max-width / 2;
padding: 0;
.@{steps-prefix-cls}-rtl& {
margin: 0 @steps-desciption-max-width / 2 0 0;
}
&::after {
width: ~'calc(100% - 20px)';
height: 3px;
margin-left: 12px;
.@{steps-prefix-cls}-rtl& {
margin-right: 12px;
margin-left: 0;
}
}
}
&:first-child .@{steps-prefix-cls}-icon-dot {
left: 2px;
.@{steps-prefix-cls}-rtl& {
right: 2px;
left: auto;
}
}
&-icon {
width: @steps-dot-size;
@ -42,11 +28,6 @@
background: transparent;
border: 0;
.@{steps-prefix-cls}-rtl& {
margin-right: 67px;
margin-left: 0;
}
.@{steps-prefix-cls}-icon-dot {
position: relative;
float: left;
@ -54,10 +35,6 @@
height: 100%;
border-radius: 100px;
transition: all 0.3s;
.@{steps-prefix-cls}-rtl& {
float: right;
}
/* expand hover area */
&::after {
position: absolute;
@ -67,11 +44,6 @@
height: 32px;
background: fade(@black, 0.1%);
content: '';
.@{steps-prefix-cls}-rtl& {
right: -26px;
left: auto;
}
}
}
}
@ -94,11 +66,6 @@
margin-top: 8px;
margin-left: 0;
background: none;
.@{steps-prefix-cls}-rtl& {
margin-right: 0;
margin-left: 16px;
}
}
// https://github.com/ant-design/ant-design/issues/18354
.@{steps-prefix-cls}-item > .@{steps-prefix-cls}-item-container > .@{steps-prefix-cls}-item-tail {
@ -106,26 +73,11 @@
left: -9px;
margin: 0;
padding: 22px 0 4px;
.@{steps-prefix-cls}-rtl& {
right: -9px;
left: auto;
}
}
.@{steps-prefix-cls}-item:first-child .@{steps-prefix-cls}-icon-dot {
left: 0;
.@{steps-prefix-cls}-rtl& {
right: 0;
left: auto;
}
}
.@{steps-prefix-cls}-item-process .@{steps-prefix-cls}-icon-dot {
left: -2px;
.@{steps-prefix-cls}-rtl& {
right: -2px;
left: auto;
}
}
}

View File

@ -174,3 +174,75 @@
}
}
}
// progress-dot
.@{steps-prefix-cls}-dot,
.@{steps-prefix-cls}-dot.@{steps-prefix-cls}-small {
.@{steps-prefix-cls}-item {
&-tail {
.@{steps-prefix-cls}-rtl& {
margin: 0 @steps-desciption-max-width / 2 0 0;
}
&::after {
.@{steps-prefix-cls}-rtl& {
margin-right: 12px;
margin-left: 0;
}
}
}
&:first-child .@{steps-prefix-cls}-icon-dot {
.@{steps-prefix-cls}-rtl& {
right: 2px;
left: auto;
}
}
&-icon {
.@{steps-prefix-cls}-rtl& {
margin-right: 67px;
margin-left: 0;
}
.@{steps-prefix-cls}-icon-dot {
.@{steps-prefix-cls}-rtl& {
float: right;
}
/* expand hover area */
&::after {
.@{steps-prefix-cls}-rtl& {
right: -26px;
left: auto;
}
}
}
}
}
}
.@{steps-prefix-cls}-vertical.@{steps-prefix-cls}-dot {
.@{steps-prefix-cls}-item-icon {
.@{steps-prefix-cls}-rtl& {
margin-right: 0;
margin-left: 16px;
}
}
// https://github.com/ant-design/ant-design/issues/18354
.@{steps-prefix-cls}-item > .@{steps-prefix-cls}-item-container > .@{steps-prefix-cls}-item-tail {
.@{steps-prefix-cls}-rtl& {
right: -9px;
left: auto;
}
}
.@{steps-prefix-cls}-item:first-child .@{steps-prefix-cls}-icon-dot {
.@{steps-prefix-cls}-rtl& {
right: 0;
left: auto;
}
}
.@{steps-prefix-cls}-item-process .@{steps-prefix-cls}-icon-dot {
.@{steps-prefix-cls}-rtl& {
right: -2px;
left: auto;
}
}
}

View File

@ -656,7 +656,8 @@
@tabs-card-head-background: @background-color-light;
@tabs-card-height: 40px;
@tabs-card-active-color: @primary-color;
@tabs-card-horizontal-padding: 8px @padding-md;
@tabs-card-horizontal-padding: (@tabs-card-height - floor(@font-size-base * @line-height-base)) / 2 -
@border-width-base @padding-md;
@tabs-card-horizontal-padding-sm: 6px @padding-md;
@tabs-title-font-size: @font-size-base;
@tabs-title-font-size-lg: @font-size-lg;

View File

@ -13,10 +13,7 @@ describe('Switch', () => {
it('should has click wave effect', async () => {
const wrapper = mount(<Switch />);
wrapper
.find('.ant-switch')
.getDOMNode()
.click();
wrapper.find('.ant-switch').getDOMNode().click();
await new Promise(resolve => setTimeout(resolve, 0));
expect(wrapper.render()).toMatchSnapshot();
});
@ -27,7 +24,7 @@ describe('Switch', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<Switch value />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Switch] `value` is not validate prop, do you mean `checked`?',
'Warning: [antd: Switch] `value` is not a valid prop, do you mean `checked`?',
);
errorSpy.mockRestore();
});

View File

@ -41,7 +41,7 @@ export default class Switch extends React.Component<SwitchProps, {}> {
warning(
'checked' in props || !('value' in props),
'Switch',
'`value` is not validate prop, do you mean `checked`?',
'`value` is not a valid prop, do you mean `checked`?',
);
}

View File

@ -98,6 +98,7 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
rowClassName,
columns,
children,
childrenColumnName: legacyChildrenColumnName,
onChange,
getPopupContainer,
loading,
@ -106,7 +107,6 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
expandedRowRender,
expandIconColumnIndex,
indentSize,
childrenColumnName = 'children',
scroll,
sortDirections,
locale,
@ -128,9 +128,11 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
const dropdownPrefixCls = getPrefixCls('dropdown', customizeDropdownPrefixCls);
const mergedExpandable: ExpandableConfig<RecordType> = {
childrenColumnName: legacyChildrenColumnName,
expandIconColumnIndex,
...expandable,
};
const { childrenColumnName = 'children' } = mergedExpandable;
const expandType: ExpandType = React.useMemo<ExpandType>(() => {
if (rawData.some(item => (item as any)[childrenColumnName])) {

View File

@ -779,4 +779,22 @@ describe('Table.rowSelection', () => {
jest.runAllTimers();
expect(wrapper.render()).toMatchSnapshot();
});
it('Table selection should check', () => {
const onChange = jest.fn();
const wrapper = mount(
<Table
dataSource={[{ name: 'light', sub: [{ name: 'bamboo' }] }]}
expandable={{ expandedRowKeys: ['light'], childrenColumnName: 'sub' }}
rowSelection={{ onChange }}
rowKey="name"
/>,
);
wrapper
.find('input')
.last()
.simulate('change', { target: { checked: true } });
expect(onChange.mock.calls[0][1]).toEqual([expect.objectContaining({ name: 'bamboo' })]);
});
});

View File

@ -1,5 +1,3 @@
/* eslint-disable no-unused-expressions */
import * as React from 'react';
import Table from '../Table';
import { ColumnProps } from '..';
@ -44,4 +42,3 @@ describe('Table.typescript types', () => {
expect(columns).toBeTruthy();
});
});
/* eslint-enable */

View File

@ -85,7 +85,7 @@ const EditableTable = () => {
const isEditing = (record: Item) => record.key === editingKey;
const edit = (record: Item) => {
form.setFieldsValue({ ...record });
form.setFieldsValue({ name: '', age: '', address: '', ...record });
setEditingKey(record.key);
};

View File

@ -66,7 +66,7 @@ const columns = [
| dataSource | Data record array to be displayed | any\[] | - |
| expandable | Config expandable content | [expandable](#expandable) | - |
| footer | Table footer renderer | Function(currentPageData) | - |
| loading | Loading status of table | boolean\|[object](https://ant.design/components/spin-cn/#API) ([more](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | `false` |
| loading | Loading status of table | boolean\|[object](/components/spin/#API) ([more](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | `false` |
| locale | i18n text including filter, sort, empty text, etc | object | filterConfirm: 'Ok' <br> filterReset: 'Reset' <br> emptyText: 'No Data' <br> [Default](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) |
| pagination | Config of pagination. You can ref table pagination [config](#pagination) or full [`pagination`](/components/pagination/) document, hide it by setting it to `false` | object | - |
| rowClassName | Row's className | Function(record, index):string | - |

View File

@ -71,7 +71,7 @@ const columns = [
| dataSource | 数据数组 | any\[] | - |
| expandable | 配置展开属性 | [expandable](#expandable) | - |
| footer | 表格尾部 | Function(currentPageData) | - |
| loading | 页面是否加载中 | boolean\|[object](https://ant.design/components/spin-cn/#API) ([更多](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | false |
| loading | 页面是否加载中 | boolean\|[object](/components/spin/#API) ([更多](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | false |
| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: '确定' <br> filterReset: '重置' <br> emptyText: '暂无数据' <br> [默认值](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) |
| pagination | 分页器,参考[配置项](#pagination)或 [pagination](/components/pagination/) 文档,设为 false 时不展示和进行分页 | object | - |
| rowClassName | 表格行的类名 | Function(record, index):string | - |

View File

@ -507,6 +507,8 @@
// ============================ Fixed =============================
&-cell-fix-left,
&-cell-fix-right {
position: -webkit-sticky !important;
position: sticky !important;
z-index: 2;
background: @table-bg;
}

View File

@ -261,10 +261,10 @@
.@{tab-prefix-cls}-top-content,
.@{tab-prefix-cls}-bottom-content {
width: 100%;
> .@{tab-prefix-cls}-tabpane {
flex-shrink: 0;
width: 100%;
-webkit-backface-visibility: hidden;
opacity: 1;
transition: opacity 0.45s;
}

View File

@ -222,6 +222,51 @@ exports[`renders ./components/time-picker/demo/basic.md correctly 1`] = `
</div>
`;
exports[`renders ./components/time-picker/demo/colored-popup.md correctly 1`] = `
<div
class="ant-picker"
>
<div
class="ant-picker-input"
>
<input
placeholder="Select time"
readonly=""
size="10"
title=""
value=""
/>
<span
class="ant-picker-suffix"
>
<span
aria-label="clock-circle"
class="anticon anticon-clock-circle"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="clock-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
/>
</svg>
</span>
</span>
</div>
</div>
`;
exports[`renders ./components/time-picker/demo/disabled.md correctly 1`] = `
<div
class="ant-picker ant-picker-disabled"

View File

@ -61,4 +61,15 @@ describe('TimePicker', () => {
);
expect(wrapper.render()).toMatchSnapshot();
});
it('should pass popupClassName prop to Picker as dropdownClassName prop', () => {
const popupClassName = 'myCustomClassName';
const wrapper = mount(
<TimePicker
defaultOpenValue={moment('00:00:00', 'HH:mm:ss')}
popupClassName={popupClassName}
/>,
);
expect(wrapper.find('Picker').prop('dropdownClassName')).toEqual(popupClassName);
});
});

View File

@ -0,0 +1,39 @@
---
order: 9
title:
zh-CN: 色付きポップアップ
en-US: Colored Popup
debug: true
---
## zh-CN
カスタムクラスを `TimePicker`ポップアップに渡す
## en-US
Passing custom class to `TimePicker` popup
```jsx
import { TimePicker } from 'antd';
import moment from 'moment';
const onChange = (time, timeString) => {
console.log(time, timeString);
};
ReactDOM.render(
<TimePicker
onChange={onChange}
defaultOpenValue={moment('00:00:00', 'HH:mm:ss')}
popupClassName="myCustomClassName"
/>,
mountNode,
);
```
```css
.myCustomClassName .ant-picker-time-panel-cell-inner {
color: red !important;
}
```

View File

@ -41,7 +41,7 @@ import moment from 'moment';
| minuteStep | interval between minutes in picker | number | 1 | |
| open | whether to popup panel | boolean | false | |
| placeholder | display when there's no value | string | "Select a time" | |
| popupClassName | className of panel | string | '' | |
| popupClassName | className of panel | string | - | |
| popupStyle | style of panel | object | - | |
| secondStep | interval between seconds in picker | number | 1 | |
| suffixIcon | The custom suffix icon | ReactNode | - | |

View File

@ -20,10 +20,11 @@ const RangePicker = React.forwardRef<any, TimeRangePickerProps>((props, ref) =>
export interface TimePickerProps extends Omit<PickerTimeProps<Moment>, 'picker'> {
addon?: () => React.ReactNode;
popupClassName?: string;
}
const TimePicker = React.forwardRef<any, TimePickerProps>(
({ addon, renderExtraFooter, ...restProps }, ref) => {
({ addon, renderExtraFooter, popupClassName, ...restProps }, ref) => {
const internalRenderExtraFooter = React.useMemo(() => {
if (renderExtraFooter) {
return renderExtraFooter;
@ -42,6 +43,7 @@ const TimePicker = React.forwardRef<any, TimePickerProps>(
return (
<InternalTimePicker
{...restProps}
dropdownClassName={popupClassName}
mode={undefined}
ref={ref}
renderExtraFooter={internalRenderExtraFooter}

View File

@ -42,7 +42,7 @@ import moment from 'moment';
| minuteStep | 分钟选项间隔 | number | 1 | |
| open | 面板是否打开 | boolean | false | |
| placeholder | 没有值的时候显示的内容 | string | "请选择时间" | |
| popupClassName | 弹出层类名 | string | '' | |
| popupClassName | 弹出层类名 | string | - | |
| popupStyle | 弹出层样式对象 | object | - | |
| secondStep | 秒选项间隔 | number | 1 | |
| suffixIcon | 自定义的选择框后缀图标 | ReactNode | - | |

View File

@ -128,18 +128,8 @@ describe('Tooltip', () => {
</Button>
</Tooltip>,
);
expect(
wrapper1
.find('span')
.first()
.getDOMNode().style.display,
).toBe('inline-block');
expect(
wrapper2
.find('span')
.first()
.getDOMNode().style.display,
).toBe('block');
expect(wrapper1.find('span').first().getDOMNode().style.display).toBe('inline-block');
expect(wrapper2.find('span').first().getDOMNode().style.display).toBe('block');
});
it('should works for arrowPointAtCenter', () => {
@ -161,10 +151,7 @@ describe('Tooltip', () => {
</button>
</Tooltip>,
);
wrapper
.find('button')
.at(0)
.simulate('click');
wrapper.find('button').at(0).simulate('click');
const popupLeftDefault = parseInt(wrapper.instance().getPopupDomNode().style.left, 10);
const wrapper2 = mount(
@ -181,10 +168,7 @@ describe('Tooltip', () => {
</button>
</Tooltip>,
);
wrapper2
.find('button')
.at(0)
.simulate('click');
wrapper2.find('button').at(0).simulate('click');
const popupLeftArrowPointAtCenter = parseInt(
wrapper2.instance().getPopupDomNode().style.left,
10,
@ -272,22 +256,26 @@ describe('Tooltip', () => {
}).not.toThrow();
});
it('support other placement', async () => {
it('support other placement', done => {
const wrapper = mount(
<Tooltip
title="xxxxx"
placement="bottomLeft"
visible
transitionName=""
mouseEnterDelay={0}
afterVisibleChange={visible => {
if (visible) {
expect(wrapper.find('Trigger').props().popupPlacement).toBe('bottomLeft');
}
done();
}}
>
<span>
Hello world!
</span>
<span>Hello world!</span>
</Tooltip>,
);
await sleep(1000);
expect(wrapper.instance().getPopupDomNode().className).toContain('placement-bottomLeft');
expect(wrapper.find('span')).toHaveLength(1);
const button = wrapper.find('span').at(0);
button.simulate('mouseenter');
});
it('other placement when mouse enter', async () => {
@ -296,18 +284,17 @@ describe('Tooltip', () => {
title="xxxxx"
placement="topRight"
transitionName=""
popupTransitionName=""
mouseEnterDelay={0}
>
<span>
Hello world!
</span>
<span>Hello world!</span>
</Tooltip>,
);
expect(wrapper.find('span')).toHaveLength(1);
const button = wrapper.find('span').at(0);
button.simulate('mouseenter');
await sleep(300);
await sleep(500);
expect(wrapper.instance().getPopupDomNode().className).toContain('placement-topRight');
});
});

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