I know it's been a while but I've seen this question around a lot and I just struggled with it myself so I'll try to answer it even if I'm certainly too late.
Arnaud's answer was actually the right answer, despite being a bit short.
Your first snippet of code was right. The problem lies in mxGraph and not your implementation.
The problem arises when your graph is contained in something else in your page, because mxGraph adds the tooltip to the body instead of the graph container.
To avoid that you can override init() function of mxTooltipHandler like this :
mxTooltipHandler.prototype.init = function() {
if (document.body != null) {
const container = document.getElementById("graphcontainer");
this.div = document.createElement("div");
this.div.className = "mxTooltip";
this.div.style.visibility = "hidden";
container.appendChild(this.div);
mxEvent.addGestureListeners(
this.div,
mxUtils.bind(this, function(evt) {
this.hideTooltip();
})
);
}
};
Here I used "graphcontainer" but use the id of your graph container.
This will allow the tooltip to show. But it will most likely be in the wrong place. To avaoid that I also overrode the reset function like this :
mxTooltipHandler.prototype.reset = function(
me,
restart,
state
) {
if (!this.ignoreTouchEvents || mxEvent.isMouseEvent(me.getEvent())) {
this.resetTimer();
state = state != null ? state : this.getStateForEvent(me);
if (
restart &&
this.isEnabled() &&
state != null &&
(this.div == null || this.div.style.visibility == "hidden")
) {
var node = me.getSource();
if(!node.attributes.x){
return
}
var x = parseInt(node.attributes.x.nodeValue);
var y = parseInt(node.attributes.y.nodeValue);
var stateSource =
me.isSource(state.shape) || me.isSource(state.text);
this.thread = window.setTimeout(
mxUtils.bind(this, function() {
if (
!this.graph.isEditing() &&
!this.graph.popupMenuHandler.isMenuShowing() &&
!this.graph.isMouseDown
) {
var tip = this.graph.getTooltip(state, node, x, y);
this.show(tip, x, y);
this.state = state;
this.node = node;
this.stateSource = stateSource;
}
}),
this.delay
);
}
}
};
I'm not sure it's the best way to achieve this, but it worked for me.