reactstrap
Version:
React Bootstrap components
239 lines (217 loc) • 5.57 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { Popper as ReactPopper } from 'react-popper';
import {
getTarget,
targetPropType,
mapToCssModules,
DOMElement,
tagPropType,
} from './utils';
import Fade from './Fade';
function noop() {}
const propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
popperClassName: PropTypes.string,
placement: PropTypes.string,
placementPrefix: PropTypes.string,
arrowClassName: PropTypes.string,
hideArrow: PropTypes.bool,
tag: tagPropType,
isOpen: PropTypes.bool,
cssModule: PropTypes.object,
offset: PropTypes.arrayOf(PropTypes.number),
fallbackPlacements: PropTypes.array,
flip: PropTypes.bool,
container: targetPropType,
target: targetPropType.isRequired,
modifiers: PropTypes.array,
strategy: PropTypes.string,
boundariesElement: PropTypes.oneOfType([PropTypes.string, DOMElement]),
onClosed: PropTypes.func,
fade: PropTypes.bool,
transition: PropTypes.shape(Fade.propTypes),
};
const defaultProps = {
boundariesElement: 'scrollParent',
placement: 'auto',
hideArrow: false,
isOpen: false,
offset: [0, 0],
flip: true,
container: 'body',
modifiers: [],
onClosed: noop,
fade: true,
transition: {
...Fade.defaultProps,
},
};
class PopperContent extends React.Component {
constructor(props) {
super(props);
this.setTargetNode = this.setTargetNode.bind(this);
this.getTargetNode = this.getTargetNode.bind(this);
this.getRef = this.getRef.bind(this);
this.onClosed = this.onClosed.bind(this);
this.state = { isOpen: props.isOpen };
}
static getDerivedStateFromProps(props, state) {
if (props.isOpen && !state.isOpen) {
return { isOpen: props.isOpen };
}
return null;
}
componentDidUpdate() {
if (
this._element &&
this._element.childNodes &&
this._element.childNodes[0] &&
this._element.childNodes[0].focus
) {
this._element.childNodes[0].focus();
}
}
onClosed() {
this.props.onClosed();
this.setState({ isOpen: false });
}
getTargetNode() {
return this.targetNode;
}
getContainerNode() {
return getTarget(this.props.container);
}
getRef(ref) {
this._element = ref;
}
setTargetNode(node) {
this.targetNode = typeof node === 'string' ? getTarget(node) : node;
}
renderChildren() {
const {
cssModule,
children,
isOpen,
flip,
target,
offset,
fallbackPlacements,
placementPrefix,
arrowClassName: _arrowClassName,
hideArrow,
popperClassName: _popperClassName,
tag,
container,
modifiers,
strategy,
boundariesElement,
onClosed,
fade,
transition,
placement,
...attrs
} = this.props;
const arrowClassName = mapToCssModules(
classNames('arrow', _arrowClassName),
cssModule,
);
const popperClassName = mapToCssModules(
classNames(
_popperClassName,
placementPrefix ? `${placementPrefix}-auto` : '',
),
this.props.cssModule,
);
const modifierNames = modifiers.map((m) => m.name);
const baseModifiers = [
{
name: 'offset',
options: {
offset,
},
},
{
name: 'flip',
enabled: flip,
options: {
fallbackPlacements,
},
},
{
name: 'preventOverflow',
options: {
boundary: boundariesElement,
},
},
].filter((m) => !modifierNames.includes(m.name));
const extendedModifiers = [...baseModifiers, ...modifiers];
const popperTransition = {
...Fade.defaultProps,
...transition,
baseClass: fade ? transition.baseClass : '',
timeout: fade ? transition.timeout : 0,
};
return (
<Fade
{...popperTransition}
{...attrs}
in={isOpen}
onExited={this.onClosed}
tag={tag}
>
<ReactPopper
referenceElement={this.targetNode}
modifiers={extendedModifiers}
placement={placement}
strategy={strategy}
>
{({
ref,
style,
placement: popperPlacement,
isReferenceHidden,
arrowProps,
update,
}) => (
<div
ref={ref}
style={style}
className={popperClassName}
data-popper-placement={popperPlacement}
data-popper-reference-hidden={
isReferenceHidden ? 'true' : undefined
}
>
{typeof children === 'function' ? children({ update }) : children}
{!hideArrow && (
<span
ref={arrowProps.ref}
className={arrowClassName}
style={arrowProps.style}
/>
)}
</div>
)}
</ReactPopper>
</Fade>
);
}
render() {
this.setTargetNode(this.props.target);
if (this.state.isOpen) {
return this.props.container === 'inline'
? this.renderChildren()
: ReactDOM.createPortal(
<div ref={this.getRef}>{this.renderChildren()}</div>,
this.getContainerNode(),
);
}
return null;
}
}
PopperContent.propTypes = propTypes;
PopperContent.defaultProps = defaultProps;
export default PopperContent;