UNPKG

react-components

Version:

React components used by Khan Academy

381 lines (341 loc) 12.8 kB
/** * A generic tooltip library for React.js * * This should eventually end up in react-components * * Interface: ({a, b} means one of a or b) * const Tooltip = require("./tooltip.js"); * <Tooltip * className="class-for-tooltip-contents" * horizontalPosition="left" // one of "left", "right" * horizontalAlign="left" // one of "left", "right" * verticalPosition="bottom" // one of "top", "bottom" * arrowSize={10} // arrow size in pixels * borderColor="#ccc" // color of the border for the tooltip * show={true} // whether the tooltip should currently be visible * targetContainerStyle={targetContainerStyle} * > * <TargetElementOfTheTooltip /> * <TooltipContents1 /> * <TooltipContents2 /> * </Tooltip> * * To show/hide the tooltip, the parent component should call the * .show() and .hide() methods of the tooltip when appropriate. * (These are usually set up as handlers of events on the target element.) * * Notes: * className should not specify a border; that is handled by borderColor * so that the arrow and tooltip match */ // __,,--``\\ // _,,-''`` \\ , // '----------_.------'-.___|\__ // _.--''`` `)__ )__ @\__ // ( .. ''---/___,,E/__,E'------` // `-''`'' // Here be dragons. // TODO(joel/aria) fix z-index issues https://s3.amazonaws.com/uploads.hipchat.com/6574/29028/yOApjwmgiMhEZYJ/Screen%20Shot%202014-05-30%20at%203.34.18%20PM.png // z-index: 3 on perseus-formats-tooltip seemed to work const React = require("react"); const ReactDOM = require("react-dom"); const zIndex = 10; const Triangle = React.createClass({displayName: "Triangle", propTypes: { color: React.PropTypes.string.isRequired, left: React.PropTypes.number.isRequired, "top": React.PropTypes.number.isRequired, width: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired, horizontalDirection: React.PropTypes.oneOf( ["left", "right"] ).isRequired, verticalDirection: React.PropTypes.oneOf( ["top", "bottom"] ).isRequired, }, render: function() { let borderLeft; let borderRight; let borderTop; let borderBottom; const hBorder = (this.props.width + "px solid transparent"); if (this.props.horizontalDirection === "right") { borderLeft = hBorder; } else { borderRight = hBorder; } const vBorder = (this.props.height + "px solid " + this.props.color); if (this.props.verticalDirection === "top") { borderTop = vBorder; } else { borderBottom = vBorder; } return React.createElement("div", {style: { display: "block", height: 0, width: 0, position: "absolute", left: this.props.left, "top": this.props["top"], borderLeft: borderLeft, borderRight: borderRight, borderTop: borderTop, borderBottom: borderBottom, }}); }, }); const TooltipArrow = React.createClass({displayName: "TooltipArrow", propTypes: { position: React.PropTypes.string, visibility: React.PropTypes.string, left: React.PropTypes.number, "top": React.PropTypes.number, color: React.PropTypes.string.isRequired, // a css color border: React.PropTypes.string.isRequired, // a css color width: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired, horizontalDirection: React.PropTypes.oneOf( ["left", "right"] ).isRequired, verticalDirection: React.PropTypes.oneOf( ["top", "bottom"] ).isRequired, }, getDefaultProps: function() { return { position: "relative", visibility: "visible", left: 0, "top": 0, }; }, // TODO(aria): Think about adding a box-shadow to the triangle here // See http://css-tricks.com/triangle-with-shadow/ render: function() { //const isRight = (this.props.horizontalDirection === "right"); const isTop = (this.props.verticalDirection === "top"); const frontTopOffset = isTop ? 0 : 1; const borderTopOffset = isTop ? 0 : -1; return React.createElement("div", {style: { display: "block", position: this.props.position, visibility: this.props.visibility, left: this.props.left, "top": this.props["top"], width: this.props.width + 2, height: this.props.height + 1, marginTop: -1, marginBottom: -2, zIndex: zIndex, }}, /* The background triangle used to create the effect of a border around the foreground triangle*/ React.createElement(Triangle, { horizontalDirection: this.props.horizontalDirection, verticalDirection: this.props.verticalDirection, color: this.props.border, left: 0, top: borderTopOffset, width: this.props.width + 2, // one extra for the diagonal height: this.props.height + 2} ), /* The foreground triangle covers all but the left/right edges of the background triangle */ React.createElement(Triangle, { horizontalDirection: this.props.horizontalDirection, verticalDirection: this.props.verticalDirection, color: this.props.color, left: 1, top: frontTopOffset, width: this.props.width, height: this.props.height} ) ); }, }); const VERTICAL_CORNERS = { "top": { "top": "-100%", }, bottom: { "top": 0, }, }; const HORIZONTAL_CORNERS = { left: { targetLeft: 0, }, right: { targetLeft: "100%", }, }; const HORIZONTAL_ALIGNMNENTS = { left: { tooltipLeft: 0, arrowLeft: function(arrowSize) {return 0;}, }, right: { tooltipLeft: "-100%", arrowLeft: function(arrowSize) {return -arrowSize - 2;}, }, }; const Tooltip = React.createClass({displayName: "Tooltip", propTypes: { show: React.PropTypes.bool.isRequired, className: React.PropTypes.string, arrowSize: React.PropTypes.number, borderColor: React.PropTypes.string, verticalPosition: React.PropTypes.oneOf( Object.keys(VERTICAL_CORNERS) ), horizontalPosition: React.PropTypes.oneOf( Object.keys(HORIZONTAL_CORNERS) ), horizontalAlign: React.PropTypes.oneOf( Object.keys(HORIZONTAL_ALIGNMNENTS) ), children: React.PropTypes.arrayOf( React.PropTypes.element ).isRequired, targetContainerStyle: React.PropTypes.any, // style object }, getDefaultProps: function() { return { className: "", arrowSize: 10, borderColor: "#ccc", verticalPosition: "bottom", horizontalPosition: "left", horizontalAlign: "left", targetContainerStyle: {}, }; }, getInitialState: function() { return { height: null, // used for offsetting "top" positioned tooltips }; }, componentDidMount: function() { this._updateHeight(); }, componentWillReceiveProps: function() { // If the contents have changed, reset our measure of the height this.setState({height: null}); }, componentDidUpdate: function() { this._updateHeight(); }, _renderToolTipDiv: function(isTooltipAbove) { const settings = Object.assign({}, HORIZONTAL_CORNERS[this.props.horizontalPosition], HORIZONTAL_ALIGNMNENTS[this.props.horizontalAlign], VERTICAL_CORNERS[this.props.verticalPosition] ); let arrowAbove; let arrowBelow; if (isTooltipAbove) { // We put an absolutely positioned arrow in the correct place arrowAbove = React.createElement(TooltipArrow, { verticalDirection: "top", horizontalDirection: this.props.horizontalAlign, position: "absolute", color: "white", border: this.props.borderColor, left: settings.arrowLeft(this.props.arrowSize), top: -this.props.arrowSize + 2, width: this.props.arrowSize, height: this.props.arrowSize, zIndex: zIndex} ); // And we use a visibility: hidden arrow below to shift up the // content by the correct amount arrowBelow = React.createElement(TooltipArrow, { verticalDirection: "top", horizontalDirection: this.props.horizontalAlign, visibility: "hidden", color: "white", border: this.props.borderColor, left: settings.arrowLeft(this.props.arrowSize), top: -1, width: this.props.arrowSize, height: this.props.arrowSize, zIndex: zIndex} ); } else { arrowAbove = React.createElement(TooltipArrow, { verticalDirection: "bottom", horizontalDirection: this.props.horizontalAlign, color: "white", border: this.props.borderColor, left: settings.arrowLeft(this.props.arrowSize), top: -1, width: this.props.arrowSize, height: this.props.arrowSize, zIndex: zIndex} ); arrowBelow = null; } /* A positioned div below the input to be the parent for our tooltip */ return React.createElement("div", {style: { position: "relative", height: 0, display: this.props.show ? "block" : "none", }}, React.createElement("div", {ref: "tooltipContainer", className: "tooltipContainer", style: { position: "absolute", // height must start out undefined, not null, so that // we can measure the actual height with jquery. // This is used to position the tooltip with top: -100% // when in verticalPosition: "top" mode height: this.state.height || undefined, left: settings.targetLeft, }}, arrowAbove, /* The contents of the tooltip */ React.createElement("div", {className: this.props.className, ref: "tooltipContent", style: { position: "relative", top: settings["top"], left: settings.tooltipLeft, border: "1px solid " + this.props.borderColor, WebkitBoxShadow: "0 1px 3px " + this.props.borderColor, MozBoxShadow: "0 1px 3px " + this.props.borderColor, boxShadow: "0 1px 3px " + this.props.borderColor, zIndex: zIndex - 1, } }, this.props.children.slice(1) ), arrowBelow ) ); }, _updateHeight: function() { const height = ReactDOM.findDOMNode(this.refs.tooltipContainer).offsetHeight; if (height !== this.state.height) { this.setState({height:height}); } }, render: function() { const isTooltipAbove = this.props.verticalPosition === "top"; /* We wrap the entire output in a span so that it displays inline */ return React.createElement("span", null, isTooltipAbove && this._renderToolTipDiv(isTooltipAbove), /* We wrap our input in a div so that we can put the tooltip in a div above/below it */ React.createElement("div", {style: this.props.targetContainerStyle}, this.props.children[0] ), !isTooltipAbove && this._renderToolTipDiv() ); }, }); // Sorry. // Apology-Oriented-Programming module.exports = Tooltip;