Merge pull request #15279 from ant-design/master

Sync master changes to feature branch
This commit is contained in:
偏右 2019-03-08 14:09:45 +08:00 committed by GitHub
commit 057f215061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 614 additions and 324 deletions

View File

@ -54,7 +54,7 @@ jobs:
clean: false
- task: NodeTool@0
inputs:
versionSpec: '11.x'
versionSpec: '10.x'
- script: npm install
displayName: install
- script: scripts/install-react.sh

View File

@ -17,31 +17,21 @@ class AffixMounter extends React.Component {
render() {
return (
<div
style={{
height: 100,
overflowY: 'scroll',
}}
ref={node => {
this.container = node;
}}
className="container"
>
<div
className="background"
style={{
paddingTop: 60,
height: 300,
<Affix
className="fixed"
target={this.getTarget}
ref={ele => {
this.affix = ele;
}}
{...this.props}
>
<Affix
target={() => this.container}
ref={ele => {
this.affix = ele;
}}
{...this.props}
>
<Button type="primary">Fixed at the top of container</Button>
</Affix>
</div>
<Button type="primary">Fixed at the top of container</Button>
</Affix>
</div>
);
}
@ -50,24 +40,36 @@ class AffixMounter extends React.Component {
describe('Affix Render', () => {
let wrapper;
const classRect = {
container: {
top: 0,
bottom: 100,
},
};
const originGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
HTMLElement.prototype.getBoundingClientRect = function() {
return (
classRect[this.className] || {
top: 0,
bottom: 0,
}
);
};
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
HTMLElement.prototype.getBoundingClientRect = originGetBoundingClientRect;
});
const scrollTo = top => {
wrapper.instance().affix.fixedNode.parentNode.getBoundingClientRect = jest.fn(() => ({
bottom: 100,
height: 28,
left: 0,
right: 0,
top: 50 - top,
width: 195,
}));
wrapper.instance().container.scrollTop = top;
const movePlaceholder = top => {
classRect.fixed = {
top,
bottom: top,
};
events.scroll({
type: 'scroll',
});
@ -80,14 +82,14 @@ describe('Affix Render', () => {
wrapper = mount(<AffixMounter />, { attachTo: document.getElementById('mounter') });
jest.runAllTimers();
scrollTo(0);
expect(wrapper.instance().affix.state.affixStyle).toBe(null);
movePlaceholder(0);
expect(wrapper.instance().affix.state.affixStyle).toBeFalsy();
scrollTo(100);
expect(wrapper.instance().affix.state.affixStyle).not.toBe(null);
movePlaceholder(-100);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
scrollTo(0);
expect(wrapper.instance().affix.state.affixStyle).toBe(null);
movePlaceholder(0);
expect(wrapper.instance().affix.state.affixStyle).toBeFalsy();
});
it('support offsetBottom', () => {
@ -96,16 +98,17 @@ describe('Affix Render', () => {
wrapper = mount(<AffixMounter offsetBottom={0} />, {
attachTo: document.getElementById('mounter'),
});
jest.runAllTimers();
scrollTo(0);
expect(wrapper.instance().affix.state.affixStyle).not.toBe(null);
movePlaceholder(300);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
scrollTo(100);
expect(wrapper.instance().affix.state.affixStyle).toBe(null);
movePlaceholder(0);
expect(wrapper.instance().affix.state.affixStyle).toBeFalsy();
scrollTo(0);
expect(wrapper.instance().affix.state.affixStyle).not.toBe(null);
movePlaceholder(300);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
});
it('updatePosition when offsetTop changed', () => {
@ -116,7 +119,7 @@ describe('Affix Render', () => {
});
jest.runAllTimers();
scrollTo(100);
movePlaceholder(-100);
expect(wrapper.instance().affix.state.affixStyle.top).toBe(0);
wrapper.setProps({
offsetTop: 10,

View File

@ -1,40 +1,12 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as PropTypes from 'prop-types';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import { polyfill } from 'react-lifecycles-compat';
import classNames from 'classnames';
import shallowequal from 'shallowequal';
import omit from 'omit.js';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import getScroll from '../_util/getScroll';
import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame';
function getTargetRect(target: HTMLElement | Window | null): ClientRect {
return target !== window
? (target as HTMLElement).getBoundingClientRect()
: ({ top: 0, left: 0, bottom: 0 } as ClientRect);
}
function getOffset(element: HTMLElement, target: HTMLElement | Window | null) {
const elemRect = element.getBoundingClientRect();
const targetRect = getTargetRect(target);
const scrollTop = getScroll(target, true);
const scrollLeft = getScroll(target, false);
const docElem = window.document.body;
const clientTop = docElem.clientTop || 0;
const clientLeft = docElem.clientLeft || 0;
return {
top: elemRect.top - targetRect.top + scrollTop - clientTop,
left: elemRect.left - targetRect.left + scrollLeft - clientLeft,
width: elemRect.width,
height: elemRect.height,
};
}
function noop() {}
import warning from '../_util/warning';
import { addObserveTarget, removeObserveTarget, getTargetRect } from './utils';
function getDefaultTarget() {
return typeof window !== 'undefined' ? window : null;
@ -58,231 +30,168 @@ export interface AffixProps {
className?: string;
}
export interface AffixState {
affixStyle: React.CSSProperties | undefined;
placeholderStyle: React.CSSProperties | undefined;
enum AffixStatus {
None,
Prepare,
}
export default class Affix extends React.Component<AffixProps, AffixState> {
static propTypes = {
offsetTop: PropTypes.number,
offsetBottom: PropTypes.number,
target: PropTypes.func,
export interface AffixState {
affixStyle?: React.CSSProperties;
placeholderStyle?: React.CSSProperties;
status: AffixStatus;
lastAffix: boolean;
}
class Affix extends React.Component<AffixProps, AffixState> {
static defaultProps = {
target: getDefaultTarget,
};
state: AffixState = {
affixStyle: undefined,
placeholderStyle: undefined,
status: AffixStatus.None,
lastAffix: false,
};
placeholderNode: HTMLDivElement;
fixedNode: HTMLDivElement;
private timeout: number;
private eventHandlers: Record<string, any> = {};
private fixedNode: HTMLElement;
private placeholderNode: HTMLElement;
private readonly events = [
'resize',
'scroll',
'touchstart',
'touchmove',
'touchend',
'pageshow',
'load',
];
setAffixStyle(e: Event, affixStyle: React.CSSProperties | null) {
const { onChange = noop, target = getDefaultTarget } = this.props;
const originalAffixStyle = this.state.affixStyle;
const isWindow = target() === window;
if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) {
return;
}
if (shallowequal(affixStyle, originalAffixStyle)) {
return;
}
this.setState({ affixStyle: affixStyle as React.CSSProperties }, () => {
const affixed = !!this.state.affixStyle;
if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) {
onChange(affixed);
}
});
}
setPlaceholderStyle(placeholderStyle: React.CSSProperties | null) {
const originalPlaceholderStyle = this.state.placeholderStyle;
if (shallowequal(placeholderStyle, originalPlaceholderStyle)) {
return;
}
this.setState({ placeholderStyle: placeholderStyle as React.CSSProperties });
}
syncPlaceholderStyle(e: Event) {
const { affixStyle } = this.state;
if (!affixStyle) {
return;
}
this.placeholderNode.style.cssText = '';
this.setAffixStyle(e, {
...affixStyle,
width: this.placeholderNode.offsetWidth,
});
this.setPlaceholderStyle({
width: this.placeholderNode.offsetWidth,
});
}
@throttleByAnimationFrameDecorator()
updatePosition(e: Event) {
const { offsetBottom, offset, target = getDefaultTarget } = this.props;
let { offsetTop } = this.props;
const targetNode = target();
// Backwards support
// Fix: if offsetTop === 0, it will get undefined,
// if offsetBottom is type of number, offsetMode will be { top: false, ... }
offsetTop = typeof offsetTop === 'undefined' ? offset : offsetTop;
const scrollTop = getScroll(targetNode, true);
const affixNode = ReactDOM.findDOMNode(this) as HTMLElement;
const elemOffset = getOffset(affixNode, targetNode);
const elemSize = {
width: this.fixedNode.offsetWidth,
height: this.fixedNode.offsetHeight,
};
const offsetMode = {
top: false,
bottom: false,
};
// Default to `offsetTop=0`.
if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') {
offsetMode.top = true;
offsetTop = 0;
} else {
offsetMode.top = typeof offsetTop === 'number';
offsetMode.bottom = typeof offsetBottom === 'number';
}
const targetRect = getTargetRect(targetNode);
const targetInnerHeight =
(targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight;
// ref: https://github.com/ant-design/ant-design/issues/13662
if (scrollTop >= elemOffset.top - (offsetTop as number) && offsetMode.top) {
// Fixed Top
const width = elemOffset.width;
const top = targetRect.top + (offsetTop as number);
this.setAffixStyle(e, {
position: 'fixed',
top,
left: targetRect.left + elemOffset.left,
width,
});
this.setPlaceholderStyle({
width,
height: elemSize.height,
});
} else if (
scrollTop <=
elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight &&
offsetMode.bottom
) {
// Fixed Bottom
const targetBottomOffet = targetNode === window ? 0 : window.innerHeight - targetRect.bottom;
const width = elemOffset.width;
this.setAffixStyle(e, {
position: 'fixed',
bottom: targetBottomOffet + (offsetBottom as number),
left: targetRect.left + elemOffset.left,
width,
});
this.setPlaceholderStyle({
width,
height: elemOffset.height,
});
} else {
const { affixStyle } = this.state;
if (
e.type === 'resize' &&
affixStyle &&
affixStyle.position === 'fixed' &&
affixNode.offsetWidth
) {
this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth });
} else {
this.setAffixStyle(e, null);
}
this.setPlaceholderStyle(null);
}
if (e.type === 'resize') {
this.syncPlaceholderStyle(e);
}
}
// Event handler
componentDidMount() {
const target = this.props.target || getDefaultTarget;
// Wait for parent component ref has its value
this.timeout = setTimeout(() => {
this.setTargetEventListeners(target);
// Mock Event object.
this.updatePosition({} as Event);
});
const { target } = this.props;
if (target) {
// [Legacy] Wait for parent component ref has its value.
// We should use target as directly element instead of function which makes element check hard.
this.timeout = setTimeout(() => {
addObserveTarget(target(), this);
// Mock Event object.
this.updatePosition({} as Event);
});
}
}
componentWillReceiveProps(nextProps: AffixProps) {
if (this.props.target !== nextProps.target) {
this.clearEventListeners();
this.setTargetEventListeners(nextProps.target!);
// Mock Event object.
this.updatePosition({} as Event);
componentDidUpdate(prevProps: AffixProps) {
const { target } = this.props;
if (prevProps.target !== target) {
removeObserveTarget(this);
if (target) {
addObserveTarget(target(), this);
// Mock Event object.
this.updatePosition({} as Event);
}
}
if (
this.props.offsetTop !== nextProps.offsetTop ||
this.props.offsetBottom !== nextProps.offsetBottom
prevProps.offsetTop !== this.props.offsetTop ||
prevProps.offsetBottom !== this.props.offsetBottom
) {
this.updatePosition({} as Event);
}
this.measure();
}
componentWillUnmount() {
this.clearEventListeners();
clearTimeout(this.timeout);
removeObserveTarget(this);
(this.updatePosition as any).cancel();
}
setTargetEventListeners(getTarget: () => HTMLElement | Window | null) {
const target = getTarget();
if (!target) {
return;
}
this.clearEventListeners();
this.events.forEach(eventName => {
this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition);
});
}
clearEventListeners() {
this.events.forEach(eventName => {
const handler = this.eventHandlers[eventName];
if (handler && handler.remove) {
handler.remove();
}
});
}
saveFixedNode = (node: HTMLDivElement) => {
this.fixedNode = node;
};
savePlaceholderNode = (node: HTMLDivElement) => {
this.placeholderNode = node;
};
saveFixedNode = (node: HTMLDivElement) => {
this.fixedNode = node;
};
// =================== Measure ===================
// Handle realign logic
@throttleByAnimationFrameDecorator()
// @ts-ignore TS6133
updatePosition(e: Event) {
// event param is used before. Keep compatible ts define here.
this.setState({
status: AffixStatus.Prepare,
affixStyle: undefined,
placeholderStyle: undefined,
});
}
measure = () => {
const { status, lastAffix } = this.state;
const { target, offset, offsetBottom, onChange } = this.props;
if (status !== AffixStatus.Prepare || !this.fixedNode || !this.placeholderNode || !target) {
return;
}
let { offsetTop } = this.props;
if (typeof offsetTop === 'undefined') {
offsetTop = offset;
warning(
typeof offset === 'undefined',
'Affix',
'`offset` is deprecated. Please use `offsetTop` instead.',
);
}
if (offsetBottom === undefined && offsetTop === undefined) {
offsetTop = 0;
}
const targetNode = target();
if (!targetNode) {
return;
}
const newState: Partial<AffixState> = {
status: AffixStatus.None,
};
const targetRect = getTargetRect(targetNode);
const placeholderReact = getTargetRect(this.placeholderNode);
if (offsetTop !== undefined && targetRect.top > placeholderReact.top - offsetTop) {
newState.affixStyle = {
position: 'fixed',
top: offsetTop + targetRect.top,
width: placeholderReact.width,
height: placeholderReact.height,
};
newState.placeholderStyle = {
width: placeholderReact.width,
height: placeholderReact.height,
};
} else if (
offsetBottom !== undefined &&
targetRect.bottom < placeholderReact.bottom + offsetBottom
) {
const targetBottomOffset = targetNode === window ? 0 : window.innerHeight - targetRect.bottom;
newState.affixStyle = {
position: 'fixed',
bottom: offsetBottom + targetBottomOffset,
width: placeholderReact.width,
height: placeholderReact.height,
};
newState.placeholderStyle = {
width: placeholderReact.width,
height: placeholderReact.height,
};
}
newState.lastAffix = !!newState.affixStyle;
if (onChange && lastAffix !== newState.lastAffix) {
onChange(newState.lastAffix);
}
this.setState(newState as AffixState);
};
// =================== Render ===================
renderAffix = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls } = this.props;
const { affixStyle, placeholderStyle, status } = this.state;
const { prefixCls, style, children } = this.props;
const className = classNames({
[getPrefixCls('affix', prefixCls)]: this.state.affixStyle,
[getPrefixCls('affix', prefixCls)]: affixStyle,
});
const props = omit(this.props, [
@ -292,11 +201,14 @@ export default class Affix extends React.Component<AffixProps, AffixState> {
'target',
'onChange',
]);
const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style };
const mergedPlaceholderStyle = {
...(status === AffixStatus.None ? placeholderStyle : null),
...style,
};
return (
<div {...props} style={placeholderStyle} ref={this.savePlaceholderNode}>
<div {...props} style={mergedPlaceholderStyle} ref={this.savePlaceholderNode}>
<div className={className} ref={this.saveFixedNode} style={this.state.affixStyle}>
{this.props.children}
{children}
</div>
</div>
);
@ -306,3 +218,7 @@ export default class Affix extends React.Component<AffixProps, AffixState> {
return <ConfigConsumer>{this.renderAffix}</ConfigConsumer>;
}
}
polyfill(Affix);
export default Affix;

75
components/affix/utils.ts Normal file
View File

@ -0,0 +1,75 @@
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import Affix from './';
// ======================== Observer ========================
const TRIGGER_EVENTS = [
'resize',
'scroll',
'touchstart',
'touchmove',
'touchend',
'pageshow',
'load',
];
interface ObserverEntity {
target: HTMLElement | Window;
affixList: Affix[];
eventHandlers: { [eventName: string]: any };
}
let observerEntities: ObserverEntity[] = [];
export function addObserveTarget(target: HTMLElement | Window | null, affix: Affix): void {
if (!target) return;
let entity: ObserverEntity | undefined = observerEntities.find(item => item.target === target);
if (entity) {
entity.affixList.push(affix);
} else {
entity = {
target,
affixList: [affix],
eventHandlers: {},
};
observerEntities.push(entity);
// Add listener
TRIGGER_EVENTS.forEach(eventName => {
entity!.eventHandlers[eventName] = addEventListener(target, eventName, (event: Event) => {
entity!.affixList.forEach(affix => {
affix.updatePosition(event);
});
});
});
}
}
export function removeObserveTarget(affix: Affix): void {
const observerEntity = observerEntities.find(oriObserverEntity => {
const hasAffix = oriObserverEntity.affixList.some(item => item === affix);
if (hasAffix) {
oriObserverEntity.affixList = oriObserverEntity.affixList.filter(item => item !== affix);
}
return hasAffix;
});
if (observerEntity && observerEntity.affixList.length === 0) {
observerEntities = observerEntities.filter(item => item !== observerEntity);
// Remove listener
TRIGGER_EVENTS.forEach(eventName => {
const handler = observerEntity.eventHandlers[eventName];
if (handler && handler.remove) {
handler.remove();
}
});
}
}
export function getTargetRect(target: HTMLElement | Window | null): ClientRect {
return target !== window
? (target as HTMLElement).getBoundingClientRect()
: ({ top: 0, bottom: window.innerHeight } as ClientRect);
}

View File

@ -20,7 +20,7 @@ export interface BadgeProps {
scrollNumberPrefixCls?: string;
className?: string;
status?: 'success' | 'processing' | 'default' | 'error' | 'warning';
text?: string;
text?: React.ReactNode;
offset?: [number | string, number | string];
title?: string;
}

View File

@ -477,6 +477,7 @@ exports[`renders ./components/collapse/demo/extra.md correctly 1`] = `
<i
aria-label="icon: setting"
class="anticon anticon-setting"
tabindex="-1"
>
<svg
aria-hidden="true"
@ -544,6 +545,7 @@ exports[`renders ./components/collapse/demo/extra.md correctly 1`] = `
<i
aria-label="icon: setting"
class="anticon anticon-setting"
tabindex="-1"
>
<svg
aria-hidden="true"
@ -596,6 +598,7 @@ exports[`renders ./components/collapse/demo/extra.md correctly 1`] = `
<i
aria-label="icon: setting"
class="anticon anticon-setting"
tabindex="-1"
>
<svg
aria-hidden="true"

View File

@ -7,7 +7,7 @@ describe('Collapse', () => {
const wrapper = mount(
<Collapse expandIcon={() => null}>
<Collapse.Panel header="header" />
</Collapse>
</Collapse>,
);
expect(wrapper.render()).toMatchSnapshot();
});
@ -17,7 +17,7 @@ describe('Collapse', () => {
<Collapse>
<Collapse.Panel header="header" extra={<button type="button">action</button>} />
<Collapse.Panel header="header" extra={<button type="button">action</button>} />
</Collapse>
</Collapse>,
);
expect(wrapper.render()).toMatchSnapshot();
});

View File

@ -28,15 +28,25 @@ const text = `
it can be found as a welcome guest in many households across the world.
`;
const genExtra = () => (
<Icon
type="setting"
onClick={(event) => {
// If you don't want click extra trigger collapse, you can prevent this:
event.stopPropagation();
}}
/>
);
ReactDOM.render(
<Collapse defaultActiveKey={['1']} onChange={callback}>
<Panel header="This is panel header 1" key="1" extra={<Icon type="setting" />}>
<Panel header="This is panel header 1" key="1" extra={genExtra()}>
<div>{text}</div>
</Panel>
<Panel header="This is panel header 2" key="2" extra={<Icon type="setting" />}>
<Panel header="This is panel header 2" key="2" extra={genExtra()}>
<div>{text}</div>
</Panel>
<Panel header="This is panel header 3" key="3" extra={<Icon type="setting" />}>
<Panel header="This is panel header 3" key="3" extra={genExtra()}>
<div>{text}</div>
</Panel>
</Collapse>,

View File

@ -36,6 +36,7 @@
position: relative;
display: inline-block;
outline: none;
cursor: text;
transition: opacity 0.3s;
&-input {

View File

@ -1,11 +1,14 @@
import * as moment from 'moment';
export function formatDate(value: moment.Moment | undefined | null, format: string | string[]): string {
if (!value) {
return '';
}
if (Array.isArray(format)) {
format = format[0];
}
return value.format(format);
export function formatDate(
value: moment.Moment | undefined | null,
format: string | string[],
): string {
if (!value) {
return '';
}
if (Array.isArray(format)) {
format = format[0];
}
return value.format(format);
}

View File

@ -162,8 +162,7 @@ After wrapped by `getFieldDecorator`, `value`(or other property defined by `valu
#### Special attention
1. `getFieldDecorator` can not be used to decorate stateless component.
2. If you use `react@<15.3.0`, then, you can't use `getFieldDecorator` in stateless component: <https://github.com/facebook/react/pull/6534>
If you use `react@<15.3.0`, then, you can't use `getFieldDecorator` in stateless component: <https://github.com/facebook/react/pull/6534>
#### getFieldDecorator(id, options) parameters

View File

@ -165,8 +165,7 @@ validateFields(['field1', 'field2'], options, (errors, values) => {
#### 特别注意
1. `getFieldDecorator` 不能用于装饰纯函数组件。
2. 如果使用的是 `react@<15.3.0`,则 `getFieldDecorator` 调用不能位于纯函数组件中: <https://github.com/facebook/react/pull/6534>
如果使用的是 `react@<15.3.0`,则 `getFieldDecorator` 调用不能位于纯函数组件中: <https://github.com/facebook/react/pull/6534>
#### getFieldDecorator(id, options) 参数

View File

@ -20,7 +20,7 @@ function fixControlledValue<T>(value: T) {
}
function hasPrefixSuffix(props: InputProps) {
return 'prefix' in props || props.suffix || props.allowClear;
return !!('prefix' in props || props.suffix || props.allowClear);
}
const InputSizes = tuple('small', 'default', 'large');

View File

@ -9,6 +9,16 @@ import calculateNodeHeight, { calculateNodeStyling } from '../calculateNodeHeigh
const { TextArea } = Input;
describe('Input', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
focusTest(Input);
it('should support maxLength', () => {
@ -20,6 +30,33 @@ describe('Input', () => {
const wrapper = mount(<Input />);
wrapper.instance().select();
});
describe('focus trigger warning', () => {
it('not trigger', () => {
const wrapper = mount(<Input suffix="bamboo" />);
wrapper
.find('input')
.instance()
.focus();
wrapper.setProps({
suffix: 'light',
});
expect(errorSpy).not.toBeCalled();
});
it('trigger warning', () => {
const wrapper = mount(<Input />);
wrapper
.find('input')
.instance()
.focus();
wrapper.setProps({
suffix: 'light',
});
expect(errorSpy).toBeCalledWith(
'Warning: [antd: Input] When Input is focused, dynamic add or remove prefix / suffix will make it lose focus caused by dom structure change. Read more: https://ant.design/components/input/#FAQ',
);
});
});
});
focusTest(TextArea);
@ -205,7 +242,10 @@ describe('Input.Password', () => {
it('should keep focus state', () => {
const wrapper = mount(<Input.Password defaultValue="111" autoFocus />);
expect(document.activeElement).toBe(
wrapper.find('input').at(0).getDOMNode()
wrapper
.find('input')
.at(0)
.getDOMNode(),
);
wrapper
.find('.ant-input-password-icon')
@ -216,7 +256,10 @@ describe('Input.Password', () => {
.at(0)
.simulate('click');
expect(document.activeElement).toBe(
wrapper.find('input').at(0).getDOMNode()
wrapper
.find('input')
.at(0)
.getDOMNode(),
);
});
});
@ -306,7 +349,10 @@ describe('Input allowClear', () => {
.at(0)
.simulate('click');
expect(document.activeElement).toBe(
wrapper.find('input').at(0).getDOMNode()
wrapper
.find('input')
.at(0)
.getDOMNode(),
);
});
});

View File

@ -20,6 +20,7 @@
border-right: 0;
}
& + .@{ant-prefix}-input-group-addon,
input + .@{ant-prefix}-input-group-addon {
padding: 0;
border: 0;

View File

@ -9,12 +9,16 @@ export default {
DatePicker,
TimePicker,
Calendar,
global: {
placeholder: 'Lütfen seçiniz',
},
Table: {
filterTitle: 'Menü Filtrele',
filterConfirm: 'Tamam',
filterReset: 'Sıfırla',
selectAll: 'Hepsini Seç',
selectInvert: 'Tersini Seç',
sortTitle: 'Sırala',
},
Modal: {
okText: 'Tamam',
@ -26,6 +30,7 @@ export default {
cancelText: 'İptal',
},
Transfer: {
titles: ['', ''],
searchPlaceholder: 'Arama',
itemUnit: 'Öğe',
itemsUnit: 'Öğeler',
@ -39,4 +44,13 @@ export default {
Empty: {
description: 'Veri Yok',
},
Icon: {
icon: 'icon',
},
Text: {
edit: 'düzenle',
copy: 'kopyala',
copied: 'kopyalandı',
expand: 'genişlet',
},
};

View File

@ -43,4 +43,18 @@ describe('Modal', () => {
const wrapper = mount(<ModalTester footer={null} />);
expect(wrapper.render()).toMatchSnapshot();
});
it('onCancel should be called', () => {
const onCancel = jest.fn();
const wrapper = mount(<Modal onCancel={onCancel} />).instance();
wrapper.handleCancel();
expect(onCancel).toBeCalled();
});
it('onOk should be called', () => {
const onOk = jest.fn();
const wrapper = mount(<Modal onOk={onOk} />).instance();
wrapper.handleOk();
expect(onOk).toBeCalled();
});
});

View File

@ -5,4 +5,4 @@ export function validProgress(progress: number | undefined) {
return 100;
}
return progress;
};
}

View File

@ -23,4 +23,11 @@
}
}
}
&.@{steps-prefix-cls}-small {
.@{steps-prefix-cls}-item {
&-icon {
margin-left: 40px;
}
}
}
}

View File

@ -153,7 +153,12 @@ export default class Table<T> extends React.Component<TableProps<T>, TableState<
const key = this.getRecordKey(item, index);
// Cache checkboxProps
if (!this.CheckboxPropsCache[key]) {
this.CheckboxPropsCache[key] = rowSelection.getCheckboxProps(item);
const checkboxProps = (this.CheckboxPropsCache[key] = rowSelection.getCheckboxProps(item));
warning(
!('checked' in checkboxProps) && !('defaultChecked' in checkboxProps),
'Table',
'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
}
return this.CheckboxPropsCache[key];
};

View File

@ -4,6 +4,16 @@ import Table from '..';
import Checkbox from '../../checkbox';
describe('Table.rowSelection', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
const columns = [
{
title: 'Name',
@ -120,6 +130,10 @@ describe('Table.rowSelection', () => {
checkboxs = wrapper.find('input');
expect(checkboxs.at(1).props().checked).toBe(true);
expect(checkboxs.at(2).props().checked).toBe(true);
expect(errorSpy).toBeCalledWith(
'Warning: [antd: Table] Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
});
it('can be controlled', () => {

View File

@ -1461,7 +1461,86 @@ exports[`renders ./components/table/demo/drag-sorting.md correctly 1`] = `
</thead>
<tbody
class="ant-table-tbody"
/>
>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="1"
index="0"
style="cursor:move"
>
<td
class=""
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left:0px"
/>
John Brown
</td>
<td
class=""
>
32
</td>
<td
class=""
>
New York No. 1 Lake Park
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="2"
index="1"
style="cursor:move"
>
<td
class=""
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left:0px"
/>
Jim Green
</td>
<td
class=""
>
42
</td>
<td
class=""
>
London No. 1 Lake Park
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="3"
index="2"
style="cursor:move"
>
<td
class=""
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left:0px"
/>
Joe Black
</td>
<td
class=""
>
32
</td>
<td
class=""
>
Sidney No. 1 Lake Park
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -904,7 +904,32 @@ exports[`renders ./components/tabs/demo/custom-tab-bar-node.md correctly 1`] = `
<div
class="ant-tabs-nav ant-tabs-nav-animated"
>
<div />
<div>
<div
aria-disabled="false"
aria-selected="true"
class="ant-tabs-tab-active ant-tabs-tab"
role="tab"
>
tab 1
</div>
<div
aria-disabled="false"
aria-selected="false"
class=" ant-tabs-tab"
role="tab"
>
tab 2
</div>
<div
aria-disabled="false"
aria-selected="false"
class=" ant-tabs-tab"
role="tab"
>
tab 3
</div>
</div>
<div
class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"
/>

View File

@ -1,3 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('tabs');
demoTest('tabs', { skip: process.env.REACT === '15' ? ['custom-tab-bar-node.md'] : [] });

View File

@ -40,4 +40,32 @@ describe('TimePicker', () => {
);
expect(wrapper.render()).toMatchSnapshot();
});
it('handleChange should work correctly', done => {
const date = moment('2000-01-01 00:00:00');
const onChange = (value, formattedValue) => {
expect(value).toBe(date);
expect(formattedValue).toBe(date.format('HH:mm:ss'));
done();
};
const wrapper = mount(<TimePicker onChange={onChange} />).instance();
wrapper.handleChange(date);
});
it('handleOpenClose should work correctly', done => {
const onOpenChange = open => {
expect(open).toBe(true);
done();
};
const wrapper = mount(<TimePicker onOpenChange={onOpenChange} />).instance();
wrapper.handleOpenClose({ open: true });
});
it('clearIcon should render correctly', () => {
const clearIcon = <div className="test-clear-icon">test</div>;
const wrapper = mount(<TimePicker clearIcon={clearIcon} />);
expect(wrapper.find('Picker').prop('clearIcon')).toEqual(
<div className="test-clear-icon ant-time-picker-clear">test</div>,
);
});
});

View File

@ -154,13 +154,10 @@ class TimePicker extends React.Component<TimePickerProps, any> {
renderInputIcon(prefixCls: string) {
const { suffixIcon } = this.props;
const clockIcon = (suffixIcon &&
(React.isValidElement<{ className?: string }>(suffixIcon) ? (
(React.isValidElement<{ className?: string }>(suffixIcon) &&
React.cloneElement(suffixIcon, {
className: classNames(suffixIcon.props.className, `${prefixCls}-clock-icon`),
})
) : (
<span className={`${prefixCls}-clock-icon`}>{suffixIcon}</span>
))) || <Icon type="clock-circle" className={`${prefixCls}-clock-icon`} />;
}))) || <Icon type="clock-circle" className={`${prefixCls}-clock-icon`} />;
return <span className={`${prefixCls}-icon`}>{clockIcon}</span>;
}

View File

@ -162,6 +162,7 @@
display: inline-block;
width: 128px;
outline: none;
cursor: text;
transition: opacity 0.3s;
&-input {

View File

@ -42,7 +42,7 @@ Any data whose entries are defined in a hierarchical manner is fit to use this c
| treeCheckable | Whether to show checkbox on the treeNodes | boolean | false |
| treeCheckStrictly | Whether to check nodes precisely (in the `checkable` mode), means parent and child nodes are not associated, and it will make `labelInValue` be true | boolean | false |
| treeData | Data of the treeNodes, manual construction work is no longer needed if this property has been set(ensure the Uniqueness of each value) | array\<{ value, title, children, \[disabled, disableCheckbox, selectable] }> | \[] |
| treeDataSimpleMode | Enable simple mode of treeData. Changes the `treeData` schema to: \[{id:1, pId:0, value:'1', title:"test1",...},...] where pId is parent node's id). It is possible to replace the default `id` and `pId` keys by providing object to `treeDataSimpleMode` | false\|object\<{ id: string, pId: string, rootPId: null }> | false |
| treeDataSimpleMode | Enable simple mode of treeData. Changes the `treeData` schema to: \[{id:1, pId:0, value:'1', title:"test1",...},...] where pId is parent node's id). It is possible to replace the default `id` and `pId` keys by providing object to `treeDataSimpleMode` | false\|object\<{ id: string, pId: string, rootPId: string }> | false |
| treeDefaultExpandAll | Whether to expand all treeNodes by default | boolean | false |
| treeDefaultExpandedKeys | Default expanded treeNodes | string\[] | - |
| treeExpandedKeys | Set expanded keys | string\[] | - |

View File

@ -42,7 +42,7 @@ title: TreeSelect
| treeCheckable | 显示 checkbox | boolean | false |
| treeCheckStrictly | checkable 状态下节点选择完全受控(父子节点选中状态不再关联),会使得 `labelInValue` 强制为 true | boolean | false |
| treeData | treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点value 在整个树范围内唯一) | array\<{value, title, children, \[disabled, disableCheckbox, selectable]}> | \[] |
| treeDataSimpleMode | 使用简单格式的 treeData具体设置参考可设置的类型 (此时 treeData 应变为这样的数据结构: \[{id:1, pId:0, value:'1', title:"test1",...},...], `pId` 是父节点的 id) | false\|Array\<{ id: string, pId: string, rootPId: null }> | false |
| treeDataSimpleMode | 使用简单格式的 treeData具体设置参考可设置的类型 (此时 treeData 应变为这样的数据结构: \[{id:1, pId:0, value:'1', title:"test1",...},...], `pId` 是父节点的 id) | false\|object\<{ id: string, pId: string, rootPId: string }> | false |
| treeDefaultExpandAll | 默认展开所有树节点 | boolean | false |
| treeDefaultExpandedKeys | 默认展开的树节点 | string\[] | - |
| treeExpandedKeys | 设置展开的树节点 | string\[] | - |

View File

@ -3,7 +3,7 @@ import warning from '../_util/warning';
import Base, { BlockProps } from './Base';
interface TextProps extends BlockProps {
ellipsis: boolean;
ellipsis?: boolean;
}
const Text: React.SFC<TextProps> = ({ ellipsis, ...restProps }) => {

View File

@ -8,16 +8,6 @@ import Progress from '../progress';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
const imageTypes: string[] = ['image', 'webp', 'png', 'svg', 'gif', 'jpg', 'jpeg', 'bmp', 'dpg'];
// https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
const previewFile = (file: File | Blob, callback: Function) => {
if (file.type && !imageTypes.includes(file.type)) {
callback('');
}
const reader = new FileReader();
reader.onloadend = () => callback(reader.result);
reader.readAsDataURL(file);
};
const extname = (url: string) => {
if (!url) {
return '';
@ -72,6 +62,16 @@ export default class UploadList extends React.Component<UploadListProps, any> {
return onPreview(file);
};
// https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
previewFile = (file: File | Blob, callback: Function) => {
if (file.type && !imageTypes.includes(file.type)) {
callback('');
}
const reader = new FileReader();
reader.onloadend = () => callback(reader.result);
reader.readAsDataURL(file);
};
componentDidUpdate() {
if (this.props.listType !== 'picture' && this.props.listType !== 'picture-card') {
return;
@ -87,13 +87,9 @@ export default class UploadList extends React.Component<UploadListProps, any> {
) {
return;
}
/*eslint-disable */
file.thumbUrl = '';
/*eslint-enable */
previewFile(file.originFileObj, (previewDataUrl: string) => {
/*eslint-disable */
this.previewFile(file.originFileObj, (previewDataUrl: string) => {
file.thumbUrl = previewDataUrl;
/*eslint-enable */
this.forceUpdate();
});
});

View File

@ -1,6 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';
import Upload from '..';
import UploadList from '../UploadList';
import Form from '../../form';
import { errorRequest, successRequest } from './requests';
import { setup, teardown } from './mock';
@ -349,4 +350,57 @@ describe('Upload List', () => {
wrapper.find(Form).simulate('submit');
expect(errors).toBeNull();
});
it('return when prop onPreview not exists', () => {
const wrapper = mount(<UploadList />).instance();
expect(wrapper.handlePreview()).toBe(undefined);
});
it('previewFile should work correctly', () => {
const callback = jest.fn();
const file = new File([''], 'test.txt', { type: 'text/plain' });
const items = [{ uid: 'upload-list-item', url: '' }];
const wrapper = mount(
<UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />,
).instance();
wrapper.previewFile(file, callback);
expect(callback).toBeCalled();
});
it('extname should work correctly when url not exists', () => {
const items = [{ uid: 'upload-list-item', url: '' }];
const wrapper = mount(
<UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />,
);
expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(2);
});
it('when picture-card is loading, icon should render correctly', () => {
const items = [{ status: 'uploading', uid: 'upload-list-item' }];
const wrapper = mount(
<UploadList listType="picture-card" items={items} locale={{ uploading: 'uploading' }} />,
);
expect(wrapper.find('.ant-upload-list-item-uploading-text').length).toBe(1);
expect(wrapper.find('.ant-upload-list-item-uploading-text').text()).toBe('uploading');
});
it('onPreview should be called, when url exists', () => {
const onPreview = jest.fn();
const items = [{ thumbUrl: 'thumbUrl', url: 'url', uid: 'upload-list-item' }];
const wrapper = mount(
<UploadList
listType="picture-card"
items={items}
locale={{ uploading: 'uploading' }}
onPreview={onPreview}
/>,
);
wrapper.find('.ant-upload-list-item-thumbnail').simulate('click');
expect(onPreview).toBeCalled();
wrapper.find('.ant-upload-list-item-name').simulate('click');
expect(onPreview).toBeCalled();
wrapper.setProps({ items: [{ thumbUrl: 'thumbUrl', uid: 'upload-list-item' }] });
wrapper.find('.ant-upload-list-item-name').simulate('click');
expect(onPreview).toBeCalled();
});
});

View File

@ -30,7 +30,7 @@ if (typeof window !== 'undefined') {
/* eslint-enable global-require */
// Error log statistic
window.addEventListener('error', function(e) {
window.addEventListener('error', function onError(e) {
// Ignore ResizeObserver error
if (e.message === 'ResizeObserver loop limit exceeded') {
e.stopPropagation();