react-components
Version:
React components used by Khan Academy
381 lines (341 loc) • 12.8 kB
JavaScript
/**
* 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;