Skip to content

Instantly share code, notes, and snippets.

@prabeengiri
Last active September 15, 2016 08:39
Show Gist options
  • Select an option

  • Save prabeengiri/0031486f9e556d40c71f04acedc0e636 to your computer and use it in GitHub Desktop.

Select an option

Save prabeengiri/0031486f9e556d40c71f04acedc0e636 to your computer and use it in GitHub Desktop.
Custom ReactJs/Redux PopOver.

Introduction:

Custom PopOver Component.

Usage:

import {PopOver} from '../PopOver';

<PopOver placement="bottom" hideOnBlur>
  <PopOver.Triggers className="popOverTrigger">POPOVR-TRIGGER</PopOver.Triggers>
  <PopOver.Content className={classNames(styles.sharePopOver)}>
    popOverContent
  </PopOver.Content>
</PopOver>

Options

 placement: Default is 'top'. It can be one of these 'top', 'bottom', 'right', 'left'
 contentWidth: Default is 250.
 hideOnBlur: This will hide the popOver content on blur if provided. 
export PopOver from './PopOver.js';
import React, {Component, PropTypes} from 'react';
import classNames from 'classnames';
import Trigger from './PopOverTrigger.js';
import Content from './PopOverContent.js';
class PopOver extends Component {
static propTypes = {
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
contentWidth: PropTypes.number,
hideOnBlur: PropTypes.bool,
// Check if there is 'Trigger' and 'Content' Child or not.
children: (props, propName, componentName) => {
let triggerChildFound = false;
let contentChildFound = false;
props.children.some((item) => {
try {
if (item.type.name === 'Trigger') {
triggerChildFound = true;
} else if (item.type.name === 'Content') {
contentChildFound = true;
}
return triggerChildFound && contentChildFound;
} catch (exp) {
throw new Error('PopOver should have valid React Component. ' +
'It should have "PopOver.Trigger" and "PopOver.Content" child Component');
}
});
if (!(triggerChildFound && contentChildFound)) {
return new Error(
'`' + componentName + '` ' +
'should have a child of the PopOver.Trigger and PopOver.Content child'
);
}
}
};
componentDidMount() {
if (!this.props.hideOnBlur) {
const popOverBackDrop = document.createElement('div');
popOverBackDrop.classList.add('popOverBackDrop');
document.body.appendChild(popOverBackDrop);
}
}
triggerClick = (triggerEvent, triggerNode) => {
this.refs.popOverContent.toggleDisplay(triggerEvent, triggerNode);
}
render = () => {
const styles = require('./PopOver.scss');
// This is to establish communication between childs (Trigger and Content).
// As Trigger child triggers the click event and that event handler has to be propagated to parent (PopOver)
// and then to next child (Content).
const childrenWithProps = React.Children.map(this.props.children, (child) => {
try {
if (child.type.name === 'Trigger') {
// Cannot mutate original child component.
return React.cloneElement(child, {
onClickCallback: this.triggerClick
});
} else if (child.type.name === 'Content') {
// Adding ref attribute in order to obtain node object
return React.cloneElement(child, {
ref: 'popOverContent',
placement: this.props.placement,
hideOnBlur: this.props.hideOnBlur
});
}
return child;
} catch (exp) {
throw new Error('PopOver should have valid React Component. ' +
'It should have "PopOver.Trigger" and "PopOver.Content" child Component');
}
});
return (
<div className={classNames(styles.popOver)} ref="popOverWrapper">
{childrenWithProps}
</div>
);
}
}
PopOver.Trigger = Trigger;
PopOver.Content = Content;
export default PopOver;
@import "../../../../theme/variables";
$arrowHeight: 20px;
.popOver {
position: relative;
.popOverContent {
visibility:hidden;
box-sizing: border-box;
position: absolute;
background-color: $light-gray;
padding:10px;
border-radius:8px;
font-weight:bold;
font-size:18px;
z-index: 2000;
&::before {
content: "";
width: 0;
height: 0;
font-size: 0;
line-height: 0;
position: absolute;
}
&:global(.top) {
margin-top: -1 * $arrowHeight;
&::before {
bottom: $arrowHeight * -1;
left: 50%;
border-left: 24px solid transparent;
border-right: 0px solid transparent;
border-top: $arrowHeight solid $light-gray;
}
}
&:global(.bottom) {
margin-top: $arrowHeight;
&::before {
top: $arrowHeight * -1;
left: 50%;
border-left: 24px solid transparent;
border-right: 0px solid transparent;
border-bottom: $arrowHeight solid $light-gray;
}
}
&:global(.left) {
&::before {
top: 50%;
right: $arrowHeight * -1;
border-right: 24px solid transparent;
border-top: 17px solid $light-gray;
}
}
&:global(.right) {
&::before {
top: 38%;
left: -1 * $arrowHeight;
border-bottom: $arrowHeight solid $light-gray;
border-left: 24px solid transparent;
}
}
&:global(.show) {
visibility: visible;
opacity: 1;
transition: opacity 2s linear;
}
&:global(.hidden) {
visibility: hidden;
opacity: 0;
transition: visibility 0s 2s, opacity 2s linear;
}
}
}
import React, {Component, PropTypes} from 'react';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
/**
* PopOverContent Component.
*/
export default class Content extends Component {
static propTypes = {
show: PropTypes.bool,
className: PropTypes.string,
contentWidth: PropTypes.number,
children: PropTypes.any,
placement: PropTypes.string,
hideOnBlur: PropTypes.bool
};
constructor() {
super();
this.state = {
show: false
};
}
setPosition = (triggerNode) => {
const popOverContentNode = this.getNodeObject();
const placement = this.getPlacement(popOverContentNode);
const position = this.getPosition(placement, popOverContentNode, triggerNode);
popOverContentNode.style.top = position.top + 'px';
popOverContentNode.style.left = position.left + 'px';
popOverContentNode.classList.add(placement);
}
getPosition = (placement, contentNode, triggerNode) => {
if ( placement === 'top' ) {
return {left: -1 * (contentNode.offsetWidth / 2 - triggerNode.offsetWidth / 2), top: -1 * (contentNode.offsetHeight) };
} else if (placement === 'bottom') {
return {left: -1 * (contentNode.offsetWidth / 2 - triggerNode.offsetWidth / 2), top: (triggerNode.offsetHeight) };
} else if (placement === 'left') {
return {left: contentNode.offsetWidth * -1, top: -1 * (contentNode.offsetHeight / 2 - triggerNode.offsetHeight / 2) };
}
// If placement is right
return {left: triggerNode.scrollWidth, top: -1 * (contentNode.offsetHeight / 2 - triggerNode.offsetHeight / 2) };
}
getNodeObject() {
return ReactDOM.findDOMNode(this);
}
static getViewPortPos() {
const width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
return {width, height};
}
/**
* Dynamically set the placement if popover content gets hidden.
* @param popOverContentNode
* @returns {*|string}
*/
getPlacement = (popOverContentNode) => {
const viewPort = Content.getViewPortPos();
const nodeRect = popOverContentNode.getBoundingClientRect();
let placement = this.props.placement || 'top';
if (placement === 'top' && (nodeRect.top + nodeRect.height < 0)) {
placement = 'bottom';
} else if (placement === 'bottom' && (nodeRect.top + nodeRect.height > viewPort.height)) {
placement = 'top';
} else if (placement === 'right' && (nodeRect.left + nodeRect.width > viewPort.width)) {
placement = 'left';
} else if (placement === 'left' && (nodeRect.left < 0)) {
placement = 'right';
}
return placement;
}
toggleDisplay = (triggerEvent, triggerNode) => {
this.setState({show: !this.state.show}, () => {
if (this.state.show) {
this.setPosition(triggerNode);
} else {
this.resetPosition();
}
const windowClickHandler = (evt) => {
const target = evt.srcElement || evt.originalTarget;
if (!this.findClosestParent(target, '.popOverTrigger')) {
this.setState({show: false});
}
document.body.removeEventListener('click', windowClickHandler, false);
};
if (this.state.show && this.props.hideOnBlur) {
document.body.addEventListener('click', windowClickHandler, false);
}
});
}
findClosestParent = (el, sel) => {
let ele = el;
while ((ele) && !((ele.matches || ele.matchesSelector).call(ele, sel))) {
ele = ele.parentElement;
}
return ele;
}
resetPosition() {
const popOverContentNode = this.getNodeObject();
popOverContentNode.style.left = 0;
popOverContentNode.style.top = 0;
}
render = () => {
const {className} = this.props;
const styles = require('./PopOver.scss');
const displayClass = this.state.show ? 'show' : 'hide';
const style = {width: this.props.contentWidth || 250};
return (
<div className={classNames(styles.popOverContent, className, displayClass, '_popOverContent')} ref="popOverContent" style={style}>{this.props.children}</div>
);
}
}
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
/**
* This Component triggers the event, and toggles the display of the
* popover content.
*/
export default class Trigger extends Component {
static propTypes = {
children: PropTypes.any,
onClickCallback: PropTypes.func
};
constructor() {
super();
}
onClick = (event) => {
event.preventDefault();
this.props.onClickCallback(event, ReactDOM.findDOMNode(this));
}
render = () => {
return (
<div onClick={this.onClick} ref="popOverTrigger" className="popOverTrigger">{this.props.children}</div>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment