From ee15c869ec955c70ef84618aec70bf2d15935bad Mon Sep 17 00:00:00 2001 From: esoh Date: Wed, 16 Jan 2019 15:53:09 -0800 Subject: [PATCH] feat(Popover): add default toggleable fade support (#1364) (#1364) adds propType `fade=true` to PopperContent used in Tooltip and Popover. Default hide delay is changed from 250 to 0 to match bootstrap. popperClassName, used for displaying the popper, is separated from classNames. BREAKING CHANGE: Popover and Tooltip will now fade in and out (like bootstrap's default). To get the previous behavior use fade={false} Closes #363 --- src/Popover.js | 5 +- src/PopperContent.js | 60 +++++++++++++++++---- src/Tooltip.js | 5 +- src/TooltipPopoverWrapper.js | 32 +++++++++-- src/__tests__/TooltipPopoverWrapper.spec.js | 23 ++++++++ 5 files changed, 106 insertions(+), 19 deletions(-) diff --git a/src/Popover.js b/src/Popover.js index cd1153e1e..afbb7ed00 100644 --- a/src/Popover.js +++ b/src/Popover.js @@ -11,8 +11,7 @@ const defaultProps = { const Popover = (props) => { const popperClasses = classNames( 'popover', - 'show', - props.className + 'show' ); const classes = classNames( @@ -24,7 +23,7 @@ const Popover = (props) => { return ( ); diff --git a/src/PopperContent.js b/src/PopperContent.js index 9f523f906..8d4051990 100644 --- a/src/PopperContent.js +++ b/src/PopperContent.js @@ -4,10 +4,13 @@ import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { Arrow, Popper as ReactPopper } from 'react-popper'; import { getTarget, targetPropType, mapToCssModules, DOMElement, tagPropType } from './utils'; +import Fade from './Fade'; + +function noop() { } const propTypes = { children: PropTypes.node.isRequired, - className: PropTypes.string, + popperClassName: PropTypes.string, placement: PropTypes.string, placementPrefix: PropTypes.string, arrowClassName: PropTypes.string, @@ -22,6 +25,9 @@ const propTypes = { target: targetPropType.isRequired, modifiers: PropTypes.object, boundariesElement: PropTypes.oneOfType([PropTypes.string, DOMElement]), + onClosed: PropTypes.func, + fade: PropTypes.bool, + transition: PropTypes.shape(Fade.propTypes), }; const defaultProps = { @@ -34,6 +40,11 @@ const defaultProps = { flip: true, container: 'body', modifiers: {}, + onClosed: noop, + fade: true, + transition: { + ...Fade.defaultProps, + } }; const childContextTypes = { @@ -48,7 +59,8 @@ class PopperContent extends React.Component { this.setTargetNode = this.setTargetNode.bind(this); this.getTargetNode = this.getTargetNode.bind(this); this.getRef = this.getRef.bind(this); - this.state = {}; + this.onClosed = this.onClosed.bind(this); + this.state = { isOpen: props.isOpen }; } getChildContext() { @@ -60,6 +72,13 @@ class PopperContent extends React.Component { }; } + static getDerivedStateFromProps(props, state) { + if (props.isOpen && !state.isOpen) { + return { isOpen: props.isOpen }; + } + else return null; + } + componentDidUpdate() { if (this._element && this._element.childNodes && this._element.childNodes[0] && this._element.childNodes[0].focus) { this._element.childNodes[0].focus(); @@ -89,6 +108,11 @@ class PopperContent extends React.Component { return data; } + onClosed() { + this.props.onClosed(); + this.setState({ isOpen: false }); + } + renderChildren() { const { cssModule, @@ -101,11 +125,14 @@ class PopperContent extends React.Component { placementPrefix, arrowClassName: _arrowClassName, hideArrow, - className, + popperClassName: _popperClassName, tag, container, modifiers, boundariesElement, + onClosed, + fade, + transition, ...attrs } = this.props; const arrowClassName = mapToCssModules(classNames( @@ -114,7 +141,7 @@ class PopperContent extends React.Component { ), cssModule); const placement = (this.state.placement || attrs.placement).split('-')[0]; const popperClassName = mapToCssModules(classNames( - className, + _popperClassName, placementPrefix ? `${placementPrefix}-${placement}` : placement ), this.props.cssModule); @@ -130,18 +157,33 @@ class PopperContent extends React.Component { ...modifiers, }; + const popperTransition = { + ...Fade.defaultProps, + ...transition, + baseClass: fade ? transition.baseClass : '', + timeout: fade ? transition.timeout : 0, + } + return ( - - {children} - {!hideArrow && } - + + + {children} + {!hideArrow && } + + ); } render() { this.setTargetNode(getTarget(this.props.target)); - if (this.props.isOpen) { + if (this.state.isOpen) { return this.props.container === 'inline' ? this.renderChildren() : ReactDOM.createPortal((
{this.renderChildren()}
), this.getContainerNode()); diff --git a/src/Tooltip.js b/src/Tooltip.js index e78efb0d7..40d254e03 100644 --- a/src/Tooltip.js +++ b/src/Tooltip.js @@ -12,8 +12,7 @@ const defaultProps = { const Tooltip = (props) => { const popperClasses = classNames( 'tooltip', - 'show', - props.className + 'show' ); const classes = classNames( @@ -25,7 +24,7 @@ const Tooltip = (props) => { return ( ); diff --git a/src/TooltipPopoverWrapper.js b/src/TooltipPopoverWrapper.js index df86443e8..21a398e0d 100644 --- a/src/TooltipPopoverWrapper.js +++ b/src/TooltipPopoverWrapper.js @@ -21,6 +21,7 @@ export const propTypes = { className: PropTypes.string, innerClassName: PropTypes.string, arrowClassName: PropTypes.string, + popperClassName: PropTypes.string, cssModule: PropTypes.object, toggle: PropTypes.func, autohide: PropTypes.bool, @@ -37,11 +38,12 @@ export const propTypes = { PropTypes.object ]), trigger: PropTypes.string, + fade: PropTypes.bool, }; const DEFAULT_DELAYS = { show: 0, - hide: 250 + hide: 0 }; const defaultProps = { @@ -51,6 +53,7 @@ const defaultProps = { delay: DEFAULT_DELAYS, toggle: function () {}, trigger: 'click', + fade: true, }; function isInDOMSubtree(element, subtreeRoot) { @@ -76,6 +79,8 @@ class TooltipPopoverWrapper extends React.Component { this.hide = this.hide.bind(this); this.onEscKeyDown = this.onEscKeyDown.bind(this); this.getRef = this.getRef.bind(this); + this.onClosed = this.onClosed.bind(this); + this.state = { isOpen: props.isOpen }; } componentDidMount() { @@ -86,11 +91,21 @@ class TooltipPopoverWrapper extends React.Component { this.removeTargetEvents(); } + static getDerivedStateFromProps(props, state) { + if (props.isOpen && !state.isOpen) { + return { isOpen: props.isOpen }; + } + else return null; + } + onMouseOverTooltipContent() { if (this.props.trigger.indexOf('hover') > -1 && !this.props.autohide) { if (this._hideTimeout) { this.clearHideTimeout(); } + if (this.state.isOpen && !this.props.isOpen) { + this.toggle(); + } } } @@ -274,8 +289,12 @@ class TooltipPopoverWrapper extends React.Component { return this.props.toggle(e); } + onClosed() { + this.setState({ isOpen: false }); + } + render() { - if (!this.props.isOpen) { + if (!this.state.isOpen) { return null; } @@ -292,20 +311,22 @@ class TooltipPopoverWrapper extends React.Component { placement, placementPrefix, arrowClassName, + popperClassName, container, modifiers, offset, + fade, } = this.props; const attributes = omit(this.props, Object.keys(propTypes)); - const popperClasses = mapToCssModules(className, cssModule); + const popperClasses = mapToCssModules(popperClassName, cssModule); const classes = mapToCssModules(innerClassName, cssModule); return (
{ { attachTo: container } ); + jest.runTimersToTime(150); + expect(document.getElementsByClassName('tooltip').length).toBe(0); + + wrapper.setProps({ isOpen: true }); + jest.runTimersToTime(150); + expect(document.getElementsByClassName('tooltip').length).toBe(1); + + wrapper.setProps({ isOpen: false }); + jest.runTimersToTime(150); + expect(document.getElementsByClassName('tooltip').length).toBe(0); + wrapper.detach(); + }); + + it('should toggle isOpen', () => { + const wrapper = mount( + + Tooltip Content + , + { attachTo: container } + ); + expect(document.getElementsByClassName('tooltip').length).toBe(0); wrapper.setProps({ isOpen: true }); + jest.runTimersToTime(0); // slight async delay for getDerivedStateFromProps to update isOpen expect(document.getElementsByClassName('tooltip').length).toBe(1); wrapper.setProps({ isOpen: false }); + jest.runTimersToTime(0); expect(document.getElementsByClassName('tooltip').length).toBe(0); wrapper.detach(); });