Skip to content

Commit ec9a957

Browse files
huoxin233claude
andcommitted
fix(tooltip): resolve native tooltip flash, async teardown leaks, and dynamic text
This overhauls the Tooltip component to handle dynamic updates properly: - Initializes tooltip via options `title` instead of `data-original-title` attribute, removing the native browser tooltip fallback flash. - Updates `.tooltip-inner` in-place when text dynamically changes to avoid race conditions with asynchronous teardown. - Calls `tooltip.show()` after text updates to force recalculation of absolute coordinates, preventing vertical misalignment. - Strips `.fade` class immediately during DOM changes to enforce synchronous cleanup and prevent stranded tooltips. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1761fbe commit ec9a957

1 file changed

Lines changed: 40 additions & 3 deletions

File tree

framework/core/js/src/common/components/Tooltip.tsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default class Tooltip extends Component<TooltipAttrs> {
104104
private oldVisibility: boolean | undefined;
105105

106106
private shouldRecreateTooltip: boolean = false;
107+
private shouldUpdateText: boolean = false;
107108
private shouldChangeTooltipVisibility: boolean = false;
108109

109110
view(vnode: Mithril.Vnode<TooltipAttrs, this>) {
@@ -124,10 +125,10 @@ export default class Tooltip extends Component<TooltipAttrs> {
124125

125126
const realText = this.getRealText();
126127

127-
// We need to recreate the tooltip if the text has changed
128+
// We need to update the tooltip text if it has changed
128129
if (realText !== this.oldText) {
129130
this.oldText = realText;
130-
this.shouldRecreateTooltip = true;
131+
this.shouldUpdateText = true;
131132
}
132133

133134
if (tooltipVisible !== this.oldVisibility) {
@@ -182,10 +183,20 @@ export default class Tooltip extends Component<TooltipAttrs> {
182183

183184
this.checkDomNodeChanged();
184185
this.recreateTooltip();
186+
187+
if (this.shouldUpdateText) {
188+
this.updateText();
189+
this.shouldUpdateText = false;
190+
}
185191
}
186192

187193
private recreateTooltip() {
188194
if (this.shouldRecreateTooltip && this.childDomNode !== null) {
195+
const tooltip = $(this.childDomNode).data('bs.tooltip');
196+
if (tooltip && tooltip.$tip) {
197+
tooltip.$tip.removeClass('fade');
198+
}
199+
189200
$(this.childDomNode).tooltip(
190201
'destroy',
191202
// @ts-expect-error We don't want this arg to be part of the public API. It only exists to prevent deprecation warnings when using `$.tooltip` in this component.
@@ -201,6 +212,20 @@ export default class Tooltip extends Component<TooltipAttrs> {
201212
}
202213
}
203214

215+
private updateText() {
216+
if (this.childDomNode === null) return;
217+
218+
const realText = this.getRealText();
219+
this.childDomNode.setAttribute('data-original-title', realText);
220+
this.childDomNode.setAttribute('aria-label', realText);
221+
222+
const tooltip = $(this.childDomNode).data('bs.tooltip');
223+
if (tooltip && tooltip.$tip && tooltip.$tip.hasClass('in')) {
224+
tooltip.$tip.find('.tooltip-inner')[this.attrs.html ? 'html' : 'text'](realText);
225+
tooltip.show();
226+
}
227+
}
228+
204229
private updateVisibility() {
205230
if (this.childDomNode === null) return;
206231

@@ -236,12 +261,13 @@ export default class Tooltip extends Component<TooltipAttrs> {
236261
const trigger = typeof tooltipVisible === 'boolean' ? 'manual' : classList('hover', [showOnFocus && 'focus']);
237262

238263
const realText = this.getRealText();
239-
this.childDomNode.setAttribute('title', realText);
240264
this.childDomNode.setAttribute('aria-label', realText);
265+
this.childDomNode.removeAttribute('title');
241266

242267
// https://getbootstrap.com/docs/3.3/javascript/#tooltips-options
243268
$(this.childDomNode).tooltip(
244269
{
270+
title: realText,
245271
html,
246272
delay,
247273
placement: position,
@@ -268,6 +294,17 @@ export default class Tooltip extends Component<TooltipAttrs> {
268294
const domNode = (this.firstChild as Mithril.VnodeDOM<any, any>).dom as HTMLElement;
269295

270296
if (domNode && !domNode.isSameNode(this.childDomNode)) {
297+
if (this.childDomNode) {
298+
const tooltip = $(this.childDomNode).data('bs.tooltip');
299+
if (tooltip && tooltip.$tip) {
300+
tooltip.$tip.removeClass('fade');
301+
}
302+
$(this.childDomNode).tooltip(
303+
'destroy',
304+
// @ts-expect-error We don't want this arg to be part of the public API. It only exists to prevent deprecation warnings when using `$.tooltip` in this component.
305+
'DANGEROUS_tooltip_jquery_fn_deprecation_exempt'
306+
);
307+
}
271308
this.childDomNode = domNode;
272309
this.shouldRecreateTooltip = true;
273310
}

0 commit comments

Comments
 (0)