Skip to content

Instantly share code, notes, and snippets.

@lou
Last active February 10, 2023 15:09
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lou/571b7c0e7797860d6c555a9fdc0496f9 to your computer and use it in GitHub Desktop.
Save lou/571b7c0e7797860d6c555a9fdc0496f9 to your computer and use it in GitHub Desktop.
/*
* Usage:
* <PopoverStickOnHover
* component={<div>Holy guacamole! I'm Sticky.</div>}
* placement="top"
* onMouseEnter={() => { }}
* delay={200}
* >
* <div>Show the sticky tooltip</div>
* </PopoverStickOnHover>
*/
import React from 'react'
import PropTypes from 'prop-types'
import { Overlay, Popover } from 'react-bootstrap'
export default class PopoverStickOnHover extends React.Component {
constructor(props) {
super(props)
this.handleMouseEnter = this.handleMouseEnter.bind(this)
this.handleMouseLeave = this.handleMouseLeave.bind(this)
this.state = {
showPopover: false,
}
}
handleMouseEnter() {
const { delay, onMouseEnter } = this.props
this.setTimeoutConst = setTimeout(() => {
this.setState({ showPopover: true }, () => {
if (onMouseEnter) {
onMouseEnter()
}
})
}, delay);
}
handleMouseLeave() {
clearTimeout(this.setTimeoutConst)
this.setState({ showPopover: false })
}
componentWillUnmount() {
if (this.setTimeoutConst) {
clearTimeout(this.setTimeoutConst)
}
}
render() {
let { component, children, placement } = this.props
const child = React.Children.map(children, (child) => (
React.cloneElement(child, {
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
ref: (node) => {
this._child = node
const { ref } = child
if (typeof ref === 'function') {
ref(node);
}
}
})
))[0]
return(
<React.Fragment>
{child}
<Overlay
show={this.state.showPopover}
placement={placement}
target={this._child}
shouldUpdatePosition={true}
>
<Popover
onMouseEnter={() => {
this.setState({ showPopover: true })
}}
onMouseLeave={this.handleMouseLeave}
id='popover'
>
{component}
</Popover>
</Overlay>
</React.Fragment>
)
}
}
PopoverStickOnHover.defaultProps = {
delay: 0
}
PopoverStickOnHover.propTypes = {
delay: PropTypes.number,
onMouseEnter: PropTypes.func,
children: PropTypes.element.isRequired,
component: PropTypes.node.isRequired
}
@JoziGila
Copy link

JoziGila commented Jan 6, 2019

Blessed be!

@richardmcnamara
Copy link

richardmcnamara commented Apr 10, 2020

In case you want a version using Hooks:

import PropTypes from 'prop-types';
import { Overlay, Popover } from 'react-bootstrap';

function PopoverStickOnHover({ delay, onMouseEnter, children, component, placement }) {
    const [showPopover, setShowPopover] = useState(false);
    const childNode = useRef(null);
    let setTimeoutConst = null;

    useEffect(() => {
        return () => {
            if (setTimeoutConst) {
                clearTimeout(setTimeoutConst);
            }
        };
    });

    const handleMouseEnter = () => {
        setTimeoutConst = setTimeout(() => {
            setShowPopover(true);
            onMouseEnter();
        }, delay);
    };

    const handleMouseLeave = () => {
        clearTimeout(setTimeoutConst);
        setShowPopover(false);
    };

    const displayChild = React.Children.map(children, child =>
        React.cloneElement(child, {
            onMouseEnter: handleMouseEnter,
            onMouseLeave: handleMouseLeave,
            ref: node => {
                childNode.current = node;
                const { ref } = child;
                if (typeof ref === 'function') {
                    ref(node);
                }
            }
        })
    )[0];

    return (
        <>
            {displayChild}
            <Overlay
                show={showPopover}
                placement={placement}
                target={childNode}
                shouldUpdatePosition
            >
                <Popover
                    onMouseEnter={() => {
                        setShowPopover(true);
                    }}
                    onMouseLeave={handleMouseLeave}
                    id="popover"
                >
                    {component}
                </Popover>
            </Overlay>
        </>
    );
}

PopoverStickOnHover.propTypes = {
    children: PropTypes.element.isRequired,
    delay: PropTypes.number,
    onMouseEnter: PropTypes.func,
    component: PropTypes.node.isRequired,
    placement: PropTypes.string.isRequired
};

PopoverStickOnHover.defaultProps = {
    delay: 0,
    onMouseEnter: () => {}
};

export default PopoverStickOnHover;

@amits97
Copy link

amits97 commented Aug 19, 2020

Thank you so much for this!

@iamsarthakjoshi
Copy link

Life saver. Thanks!

@grayinteractive
Copy link

Works great! Thank you!

@dreamerchandra
Copy link

With overlaytrigger


const DefaultMore = ({ ref, onMouseEnter, onMouseLeave }) => (<span className='text-primary' onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> +more  </span>)

const DefaultFirst = ({ firstChild }) => {
  const { props: { children: text } } = firstChild;

  return (<span>{text.slice(0, 10)}...</span>)
}
const DEFAULT_DELAY = 400;

export default function Overlay ({ props, children, placement = "right", trigger = ["hover", "focus"], More = DefaultMore, First = DefaultFirst, delay = DEFAULT_DELAY }) {


  const [FirstChild, ...rest] = children;
  const [showPopover, setShowPopover] = useState(false);

  let setTimeoutConst = null;

  useEffect(() => {
    return () => {
      if (setTimeoutConst) {
        clearTimeout(setTimeoutConst);
      }
    };
  });

  const handleMouseEnter = () => {
    setTimeoutConst = setTimeout(() => {
      setShowPopover(true);
    }, delay);
  };

  const handleMouseLeave = () => {
    clearTimeout(setTimeoutConst);
    setShowPopover(false);
  };

  const Pop = ({ ref, ...triggerHandler }) => {
    return (
      <Popover id="popover-basic" ref={ref} {...triggerHandler} onMouseEnter={() => { setShowPopover(true) }} onMouseLeave={() => setShowPopover(false)}>
        <ul className='hover' >
          {children}
        </ul>
      </Popover>
    )
  };
  Pop.displayName = 'Pop'

  return (
    <>

      {rest.length ? <First firstChild={FirstChild} /> : <>{children}</>}
      {
        rest.length ? <OverlayTrigger show={showPopover} onExiting={() => console.log('exiting')} trigger={trigger} placement={placement} overlay={Pop} className="text-main" delay={{ hide: 400 }}>
          {({ ref, ...triggerHandler }) => (
            <span variant="outline" ref={ref} className="pl-2 cursor-pointer" style={{ border: 'none' }} {...triggerHandler}>
              <More onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}/>
            </span>
          )}
        </OverlayTrigger> : null
      }
    </>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment