thousight thousight - 3 months ago 129
CSS Question

React Bootstrap OverlayTrigger with trigger="focus" bug work around

In iOS safari, OverlayTrigger with trigger="focus" isn't able to dismiss when tapping outside. Here is my code:

<OverlayTrigger
trigger="focus"
placement="right"
overlay={ <Popover id="popoverID" title="Popover Title">
What a popover...
</Popover> } >
<a bsStyle="default" className="btn btn-default btn-circle" role="Button" tabIndex={18}>
<div className="btn-circle-text">?</div>
</a>
</OverlayTrigger>


I know that this is a known bug for Bootstrap cuz this doesn't even work on their own website in iOS, but does anyone know any method to go around it? It would be the best if it is something that doesn't require jQuery, but jQuery solution is welcome. Thanks.

Answer

OK, since no one else gives me a work around, I worked on this problem with my co-worker together for 3 days, and we came up with this heavy solution:

THE PROBLEM:

With trigger="focus", Bootstrap Popover/Tooltip can be dismissed when CLICKING outside the Popover/Tooltip, but not TOUCHING. Android browsers apparently changes touches to clicks automatically, so things are fine on Android. But iOS safari and browsers that is based on iOS safari (iOS chrome, iOS firefox, etc...) don't do that.

THE FIX:

We found out that in React Bootstrap, the Overlay component actually lets you customize when to show the Popover/Tooltip, so we built this component InfoOverlay based on Overlay. And to handle clicking outside the component, we need to add event listeners for both the Popover/Tooltip and window to handle both 'mousedown' and 'touchstart'. Also, this method would make the Popover have its smallest width all the time because of the padding-right of the component is initially 0px, and we make based on the width of some parent component so that it is responsive based on the parent component. And the code looks like this:

import React, { Component, PropTypes as PT } from 'react';
import {Popover, Overlay} from 'react-bootstrap';

export default class InfoOverlay extends Component {

    static propTypes = {
        PopoverId: PT.string,
        PopoverTitle: PT.string,
        PopoverContent: PT.node,
        // You need to add this prop and pass it some numbers 
        // if you need to  customize the arrowOffsetTop, it's sketchy...
        arrowOffsetTop: PT.number,
        // This is to be able to select the parent component
        componentId: PT.string  
    }

    constructor(props) {
        super(props);
        this.state = {
            showPopover: false,
            popoverClicked: false
        };
    }

    componentDidMount() {
        // Here are the event listeners and an algorithm
        // so that clicking popover would not dismiss itself
        const popover = document.getElementById('popoverTrigger');
        if (popover) {
            popover.addEventListener('mousedown', () => {
                this.setState({
                    popoverClicked: true
                });
            });
            popover.addEventListener('touchstart', () => {
                this.setState({
                    popoverClicked: true
                });
            });
        }
        window.addEventListener('mousedown', () => {
            if (!this.state.popoverClicked) {
                this.setState({
                    showPopover: false
                });
            } else {
                this.setState({
                    popoverClicked: false
                });
            }
        });
        window.addEventListener('touchstart', () => {
            if (!this.state.popoverClicked) {
                this.setState({
                    showPopover: false
                });
            } else {
                this.setState({
                    popoverClicked: false
                });
            }
        });

        // this is to resize padding-right when window resizes
        window.onresize = ()=>{
            this.setState({});
        };
    }

    // This function sets the style and more importantly, padding-right
    getStyle() {
        if (document.getElementById(this.props.componentId) && document.getElementById('popoverTrigger')) {
            const offsetRight = document.getElementById(this.props.componentId).offsetWidth - document.getElementById('popoverTrigger').offsetLeft - 15;
            return (
                {display: 'inline-block', position: 'absolute', 'paddingRight': offsetRight + 'px'}
            );
        }
        return (
            {display: 'inline-block', position: 'absolute'}
        );
    }

    overlayOnClick() {
        this.setState({
            showPopover: !(this.state.showPopover)
        });
    }

    render() {
        const customPopover = (props) => {
            return (
                {/* The reason why Popover is wrapped by another
                    invisible Popover is so that we can customize
                    the arrowOffsetTop, it's sketchy... */}
                <div id="customPopover">
                    <Popover style={{'visibility': 'hidden', 'width': '100%'}}>
                        <Popover {...props} arrowOffsetTop={props.arrowOffsetTop + 30} id={this.props.PopoverId} title={this.props.PopoverTitle} style={{'marginLeft': '25px', 'marginTop': '-25px', 'visibility': 'visible'}}>
                            {this.props.PopoverContent}
                        </Popover>
                    </Popover>
                </div>
            );
        };

        return (
            <div id="popoverTrigger" style={this.getStyle()}>
                <a bsStyle="default" className="btn btn-default btn-circle" onClick={this.overlayOnClick.bind(this)} role="Button" tabIndex={13}>
                    <div id="info-button" className="btn-circle-text">?</div>
                </a>
                <Overlay
                    show={this.state.showPopover}
                    placement="right"
                    onHide={()=>{this.setState({showPopover: false});}}
                    container={this}>
                    {customPopover(this.props)}
                </Overlay>
            </div>
        );
    }
}

In the end, this is a heavy work around because it is a big amount of code for a fix, and you can probably feel your site is slowed down by a tiny bit because of the 4 event listeners. And the best solution is just tell Bootstrap to fix this problem...

Comments