Skip to content

Commit 44b95fe

Browse files
huoxin233claude
andauthored
[1.x] fix(tooltip): resolve native tooltip flash, async teardown leaks, and dynamic text (#4675)
* 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> * fix(admin): blur visibility toggle after click to dismiss tooltip After toggling announcement visibility, the button retains browser focus, which keeps the tooltip visible even after the cursor moves away. Call blur() to release focus and allow the tooltip to dismiss naturally on mouse-out. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert "fix(admin): blur visibility toggle after click to dismiss tooltip" This reverts commit 0cf01e7. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 86b9c86 commit 44b95fe

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)