atomiks / tippyjs

Tooltip, popover, dropdown, and menu library
https://atomiks.github.io/tippyjs/
MIT License
11.92k stars 520 forks source link

How to use with react ? #199

Closed nilaybrahmbhatt closed 6 years ago

nilaybrahmbhatt commented 6 years ago
ivarkallejarv commented 6 years ago

Check out https://github.com/tvkhoa/react-tippy

atomiks commented 6 years ago

Unfortunately react-tippy hasn't been updated since September.

I recommend someone make a new wrapper for React? If I knew how, I would provide an offical one for people to use...

tiagostutz commented 6 years ago

Maybe you don't need a "reactized" version of Tippy. It looks like a good idea, but you end up having outdated react libs. In my opion, view-only libs could be used directly as pure JS code, mixed into your React component. The tricky thing here is that you have to put this code in the correct lifecycle methods of the react component as you can't use document.onload when using React.

I'm using Tippy here with React like this:

componentDidMount() {
      window.tippy(window.document.querySelector('#tippedObj`)
      ...
}

You could use within componentDidUpdate method too...

Hope it helps! 👍

jburghardt commented 6 years ago

https://codesandbox.io/s/50ky74y6yn

jburghardt commented 6 years ago

With react being so popular, maybe a "how to use with react" in the official docs would be good.

amertak commented 6 years ago

I am now working on a wrapper.

Have something like this.

import React, { Fragment, Component } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import tippy from 'tippy.js'
import classNames from 'classnames'

const PUBLIC_TOOLTIP_NAME = 'data-sl-tooltip-id'

export default class Tooltip extends Component {
    static propTypes = {
        // Simple text to show
        text: PropTypes.string,
        // React component to render inside tooltip
        component: PropTypes.object,
        // If no children is provided, default icon is shown, you can add className using this prop
        iconClassName: PropTypes.string,
        // If no children is provided, default icon is shown, you can add style using this prop
        iconStyle: PropTypes.object,
        // The events on the reference element which cause the tooltip to show
        trigger: PropTypes.oneOf(['mouseenter focus', 'click', 'manual']),
        // If true, the tooltip's position will be updated on each animation frame so
        // the tooltip will stick to its reference element if it moves
        sticky: PropTypes.bool,
        // The placement of the tooltip in relation to its reference
        placement: PropTypes.string, // 'bottom', 'left', 'right', 'top-start', 'top-end', etc.
        // If true, multiple tooltips can be on the page when triggered by clicks
        multiple: PropTypes.bool,
        // If true, the tooltip becomes interactive and won't close when hovered over or clicked
        interactive: PropTypes.bool,
        // The maximum width of the tooltip. Add units such as px or rem
        // Avoid exceeding 300px due to mobile devices, or don't specify it at all
        maxWidth: PropTypes.string,
        // Offsets the tooltip popper in 2 dimensions. Similar to the distance option,
        // but applies to the parent popper element instead of the tooltip
        offset: PropTypes.string, // '50, 20' = 50px x-axis offset, 20px y-axis offset
        // Delays showing/hiding a tooltip after a trigger event was fired, in ms
        // Number or Array [show, hide] e.g. [100, 500]
        delay: PropTypes.array,
        // How far the tooltip is from its reference element in pixels
        distance: PropTypes.number,
        // The transition duration
        // Number or Array [show, hide]
        duration: PropTypes.array,
        // The type of animation to use
        // 'shift-away', 'shift-toward', 'fade', 'scale', 'perspective'
        animation: PropTypes.oneOf(['shift-away', 'shift-toward', 'fade', 'scale', 'perspective']),
        // Whether to display the arrow. Disables the animateFill option
        arrow: PropTypes.bool,
        // Transforms the arrow element to make it larger, wider, skinnier, offset, etc.
        // CSS syntax: 'scaleX(0.5)', 'scale(2)', 'translateX(5px)' etc.
        arrowTransform: PropTypes.string,
        // The type of arrow. 'sharp' is a triangle and 'round' is an SVG shape
        arrowType: PropTypes.oneOf(['sharp', 'round']),
        // If true, the tooltip will flip (change its placement) if there is not enough
        // room in the viewport to display it
        flip: PropTypes.bool,
        // If true, whenever the title attribute on the reference changes, the tooltip
        // will automatically be updated
        dynamicTitle: PropTypes.bool,
    }
    static defaultProps = {
        trigger: 'mouseenter focus',
        sticky: false,
        placement: 'top',
        multiple: false,
        interactive: false,
        maxWidth: '300px',
        offset: '',
        delay: [500, 200],
        distance: 16,
        duration: [200, 200],
        animation: 'fade',
        arrow: false,
        arrowTransform: '',
        arrowType: 'sharp',
        flip: false,
        dynamicTitle: false,
    }
    state = { id: `sl_${Date.now()}_${Math.round(Math.random() * 1000000000)}` }
    componentDidMount() {
        tippy(`[${PUBLIC_TOOLTIP_NAME}="${this.state.id}"]`, this.getTippyProperties())
    }
    getTippyProperties() {
        const defaults = {
            iconStyle: {},
            iconClassName: '',
            dynamicTitle: this.props.dynamicTitle,
            animateFill: true,
            createPopperInstanceOnInit: true,
            trigger: this.props.trigger,
            sticky: this.props.sticky,
            placement: this.props.placement,
            performance: true,
            multiple: this.props.multiple,
            interactive: this.props.interactive,
            maxWidth: this.props.maxWidth,
            offset: this.props.offset,
            delay: this.props.delay,
            distance: this.props.distance,
            duration: this.props.duration,
            animation: this.props.animation,
            arrow: this.props.arrow,
            arrowTransform: this.props.arrowTransform,
            arrowType: this.props.arrowType,
            flip: this.props.flip,
        }

        if (this.props.component && this.htmlElement) {
            const newHTMLElement = this.htmlElement.cloneNode(true)
            newHTMLElement.style.display = 'block'

            defaults.html = newHTMLElement
        }
        return defaults
    }
    getProps = hasTitle =>
        Object.assign({}, { [PUBLIC_TOOLTIP_NAME]: this.state.id }, hasTitle ? { title: this.props.text } : {})
    renderChildren(hasTitle = true) {
        if (!this.props.children) {
            return (
                <i
                    {...this.getProps(hasTitle)}
                    className={classNames('icon', 'icon-question', this.props.iconClassName)}
                    style={{ marginLeft: 8, ...this.props.iconStyle }}
                />
            )
        }

        return this.props.children(this.getProps(hasTitle))
    }
    renderPortal() {
        return ReactDOM.createPortal(
            <div style={{ display: 'none' }} ref={c => (this.htmlElement = c)}>
                {this.props.component}
            </div>,
            window.document.body,
        )
    }
    render() {
        if (this.props.component) {
            return (
                <Fragment>
                    {this.renderChildren(false)}
                    {this.renderPortal()}
                </Fragment>
            )
        }

        return this.renderChildren()
    }
}

Then in app

<Tooltip text="Tooltip" trigger="click">
       {tooltipProps => (
                 <div {...tooltipProps}>
                 Click me
        </div>
    )}
</Tooltip>

or

<Tooltip component={<div>My cool component</div>} trigger="click">
       {tooltipProps => (
                 <div {...tooltipProps}>
                 Click me
        </div>
    )}
</Tooltip>
jburghardt commented 6 years ago

that seem way too overkill

amertak commented 6 years ago

Maybe :) It has some bugs, but I guess I prefer more robust solution.

Tainan404 commented 6 years ago

You can use it how component.

https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-html5/imports/ui/components/tooltip/component.jsx

atomiks commented 6 years ago

I've made two component wrapper examples for React and Hyperapp linked as gists.

https://github.com/atomiks/tippyjs#component--library-wrappers

bernatfortet commented 6 years ago

Hi @atomiks thank you for creating the wrappers. It really helps.

react-tippy had a useContext flag that dealt with Redux/Router issues with the context of a Component.

Is this something that you could look into and update the wrapper examples?

@tvkhoa made the improvement int he 1.01v => https://github.com/tvkhoa/react-tippy/commit/e324e62001e77cd1b24046093e39b40fc07b7e16

Thanks

atomiks commented 6 years ago

v3 (in beta, stable releasing soon): https://www.npmjs.com/package/@tippy.js/react

mehmetnyarar commented 6 years ago

I have created a component called Tooltip as below and placed in my Root component. Therefore all tooltips appear correctly with a default layout. And whenever a dynamic component is to be loaded which requires Tippy, I import Tooltip component and then just call Tooltip.rebuild() in the componentDidMount method. It works but I'm not sure whether it's a good practice or not to import tippy dynamically like this.

class Tooltip extends React.Component<Props, State> {
  static rebuild() {
    import('tippy.js').then(({ default: tippy }) => {
      tippy('[title]', tippyOptions);
    });
  }

  componentDidMount() {
    Tooltip.rebuild();
  }

  render() {
    return null;
  }
}

export default Tooltip;
// Root.jsx render:
<div className="root">
  {!auth && <TopBar />}
  <Header />
  {renderRoutes(routes)}
  {!auth && <Footer />}
  <Toastr />
  <Tooltip />
</div>