mirror of
https://github.com/ant-design/ant-design.git
synced 2025-01-21 08:29:18 +08:00
Merge pull request #15279 from ant-design/master
Sync master changes to feature branch
This commit is contained in:
commit
057f215061
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
75
components/affix/utils.ts
Normal 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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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>,
|
||||
|
@ -36,6 +36,7 @@
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
cursor: text;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&-input {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) 参数
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -20,6 +20,7 @@
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
& + .@{ant-prefix}-input-group-addon,
|
||||
input + .@{ant-prefix}-input-group-addon {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -5,4 +5,4 @@ export function validProgress(progress: number | undefined) {
|
||||
return 100;
|
||||
}
|
||||
return progress;
|
||||
};
|
||||
}
|
||||
|
@ -23,4 +23,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&.@{steps-prefix-cls}-small {
|
||||
.@{steps-prefix-cls}-item {
|
||||
&-icon {
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
};
|
||||
|
@ -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', () => {
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -1,3 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('tabs');
|
||||
demoTest('tabs', { skip: process.env.REACT === '15' ? ['custom-tab-bar-node.md'] : [] });
|
||||
|
@ -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>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -162,6 +162,7 @@
|
||||
display: inline-block;
|
||||
width: 128px;
|
||||
outline: none;
|
||||
cursor: text;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&-input {
|
||||
|
@ -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\[] | - |
|
||||
|
@ -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\[] | - |
|
||||
|
@ -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 }) => {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user