/**
* @file Overlay
* @description
* @author fex
*/
import {
Position as BasePosition,
Portal,
RootCloseWrapper
} from 'react-overlays';
import {findDOMNode} from 'react-dom';
import React from 'react';
import {calculatePosition, getContainer, ownerDocument} from '../utils/dom';
import {autobind, noop} from '../utils/helper';
import {resizeSensor, getComputedStyle} from '../utils/resize-sensor';
// @ts-ignore
BasePosition.propTypes.placement = () => null;
// @ts-ignore
class Position extends BasePosition {
props: any;
_lastTarget: any;
resizeDispose: Array<() => void>;
watchedTarget: any;
setState: (state: any) => void;
updatePosition(target: any) {
this._lastTarget = target;
if (!target) {
return this.setState({
positionLeft: 0,
positionTop: 0,
arrowOffsetLeft: null,
arrowOffsetTop: null
});
}
const watchTargetSizeChange = this.props.watchTargetSizeChange;
const overlay = findDOMNode(this as any) as HTMLElement;
const container = getContainer(
this.props.container,
ownerDocument(this).body
);
if (
(!this.watchedTarget || this.watchedTarget !== target) &&
getComputedStyle(target, 'position') !== 'static'
) {
this.resizeDispose?.forEach(fn => fn());
this.watchedTarget = target;
this.resizeDispose = [
watchTargetSizeChange !== false
? resizeSensor(target, () => this.updatePosition(target))
: noop,
resizeSensor(overlay, () => this.updatePosition(target))
];
}
this.setState(
calculatePosition(
this.props.placement,
overlay,
target,
container,
this.props.containerPadding
)
);
}
componentWillUnmount() {
this.resizeDispose?.forEach(fn => fn());
}
}
interface OverlayProps {
placement?: string;
show?: boolean;
transition?: React.ReactType;
containerPadding?: number;
shouldUpdatePosition?: boolean;
rootClose?: boolean;
onHide?(props: any, ...args: any[]): any;
container?: React.ReactNode | Function;
target?: React.ReactNode | Function;
watchTargetSizeChange?: boolean;
onEnter?(node: HTMLElement): any;
onEntering?(node: HTMLElement): any;
onEntered?(node: HTMLElement): any;
onExit?(node: HTMLElement): any;
onExiting?(node: HTMLElement): any;
onExited?(node: HTMLElement): any;
}
interface OverlayState {
exited: boolean;
}
export default class Overlay extends React.Component<
OverlayProps,
OverlayState
> {
static defaultProps = {
placement: 'auto'
};
constructor(props: OverlayProps) {
super(props as any);
this.state = {
exited: !props.show
};
}
componentWillReceiveProps(nextProps: OverlayProps) {
if (nextProps.show) {
this.setState({exited: false});
} else if (!nextProps.transition) {
// Otherwise let handleHidden take care of marking exited.
this.setState({exited: true});
}
}
@autobind
onHiddenListener(node: HTMLElement) {
this.setState({exited: true});
if (this.props.onExited) {
this.props.onExited(node);
}
}
render() {
const {
container,
containerPadding,
target,
placement,
shouldUpdatePosition,
rootClose,
children,
watchTargetSizeChange,
transition: Transition,
...props
} = this.props;
const mountOverlay = props.show || (Transition && !this.state.exited);
if (!mountOverlay) {
// Don't bother showing anything if we don't have to.
return null;
}
let child = children;
// Position is be inner-most because it adds inline styles into the child,
// which the other wrappers don't forward correctly.
child = (
// @ts-ignore
{child}
);
if (Transition) {
let {onExit, onExiting, onEnter, onEntering, onEntered} = props;
// This animates the child node by injecting props, so it must precede
// anything that adds a wrapping div.
child = (
{child}
);
}
// This goes after everything else because it adds a wrapping div.
if (rootClose) {
child = (
// @ts-ignore
{child}
);
}
// @ts-ignore
return {child};
}
}