Tooltip mixin for React
I recently integrated tooltips into our encounter.io editor which is being rendered by React.
On a static webpage, you can use data attributes and at the very bottom of the webpage run the tooltip library. It scans the DOM for elements which have these special attributes defined, and attaches the necessary event handlers on them. That work well as long as the page stays more or less static. But if the rendering toolkit starts destroying elements underneath the tooltip library, it may happen that the tooltips are not properly disposed of.
Therefore in web applications tooltips should be fully integrated into the rendering loop, so they are aware of elements which are being created and destroyed. It may even make sense to enforce the constraint that only one tooltip may be active at any given time.
I created a TooltipMixin
for React which does exactly that. The mixin can
be attached to any component. It adds the necessary event handlers and makes
sure that only one tooltip is ever visible.
It is only about 50 lines of code, with parts of it specific to the tooltip library in use. This particular mixin was created to work with http://darsa.in/tooltip/.
The source code
We create two global variables which represent the tooltip, and the component which owns it currently.
var gTooltip = new Tooltip();
var gTooltipOwner = null;
The mixin uses the component lifecycle callbacks to
track mouse events and activate the tooltip. On mouseenter
the component
asserts ownership of the tooltip, and on update refreshes its content.
The mixin requires that the component implements a tooltipContent
function.
It must return a React component which will be used as the tooltip content.
var TooltipMixin = {
componentDidMount: function() {
var el = this.getDOMNode();
el.addEventListener('mouseenter', this.mouseenter, false);
el.addEventListener('mouseleave', this.mouseleave, false);
},
componentDidUpdate: function() {
// We only care if the tooltip is shown and we are the owner.
if (!gTooltip.hidden && gTooltipOwner === this && this.tooltipContent) {
this.update(this.tooltipContent());
}
},
componentWillUnmount: function() {
var el = this.getDOMNode();
el.removeEventListener('mouseenter', this.mouseenter);
el.removeEventListener('mouseleave', this.mouseleave);
},
mouseenter: function() {
// Assert ownership on mouseenter
gTooltipOwner = this;
if (this.tooltipContent) {
this.update(this.tooltipContent());
} else {
console.warn("Component has TooltipMixin but does not provide tooltipContent()");
}
},
mouseleave: function() {
// Hide the tooltip only if we are still the owner.
if (gTooltipOwner === this) {
gTooltip.detach().hide();
gTooltipOwner = null;
}
},
update: function(content) {
var el = this.getDOMNode();
React.renderComponent(content, gTooltip.element, function() {
// Need to tell the tooltip that its contents have changed so
// it can reposition itself correctly.
gTooltip.attach(el).show().updateSize().place('top');
});
}
}
Here is a sample how to use the mixin. The EmWithTooltip
component is
basically an em
element which shows some tooltip upon hover.
var EmWithTooltip = React.createClass({
mixins: [TooltipMixin],
render: function() {
return React.DOM.em({}, this.props.children);
}
tooltipContent: function() {
return React.DOM.div({}, "This must be very important");
}
});