UNPKG

react-components

Version:

React components used by Khan Academy

386 lines (346 loc) 12.5 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.jsx"); * <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({ 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 <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({ 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 <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*/} <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 */} <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} /> </div>; }, }); 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: (arrowSize) => 0, }, right: { tooltipLeft: "-100%", arrowLeft: (arrowSize) => -arrowSize - 2, }, }; const Tooltip = React.createClass({ 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 = <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 = <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 = <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 <div style={{ position: "relative", height: 0, display: this.props.show ? "block" : "none", }} > <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 */} <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)} </div> {arrowBelow} </div> </div>; }, _updateHeight: function() { const height = ReactDOM.findDOMNode(this.refs.tooltipContainer).offsetHeight; if (height !== this.state.height) { this.setState({height}); } }, render: function() { const isTooltipAbove = this.props.verticalPosition === "top"; /* We wrap the entire output in a span so that it displays inline */ return <span> {isTooltipAbove && this._renderToolTipDiv(isTooltipAbove)} {/* We wrap our input in a div so that we can put the tooltip in a div above/below it */} <div style={this.props.targetContainerStyle}> {this.props.children[0]} </div> {!isTooltipAbove && this._renderToolTipDiv()} </span>; }, }); // Sorry. // Apology-Oriented-Programming module.exports = Tooltip;