Skip to content

Commit 2774c5e

Browse files
committed
Fix potential overflow issue with HTML tooltip (#311)
1 parent 1073587 commit 2774c5e

File tree

2 files changed

+95
-58
lines changed

2 files changed

+95
-58
lines changed

experiments/dkimHeader.d.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
1+
interface DKIMTooltipElement extends HTMLElement {
2+
_target: DKIMTooltipTarget | void
3+
_warningsBox: HTMLElement | void
4+
_value: HTMLElement | void
5+
_dkimOnmouseenter: (ev: MouseEvent) => void
6+
_dkimOnmouseleave: (ev: MouseEvent) => void
7+
}
8+
9+
interface DKIMTooltipTarget extends HTMLElement {
10+
_dkimTooltip?: HTMLElement;
11+
}
12+
113
interface DKIMHeaderFieldElement extends HTMLDivElement {
214
_dkimValue: XULElement
315
_dkimWarningIcon: XULElement
4-
_dkimWarningTooltip: DKIMTooltipElement
16+
_dkimWarningTooltip: DKIMWarningsTooltipXULElement
517
_arhDkim: { box: XULElement, value: XULElement }
618
_arhDmarc: { box: XULElement, value: XULElement }
719
_arhSpf: { box: XULElement, value: XULElement }
820
}
921

10-
interface DKIMTooltipElement extends XULElement {
11-
_value: XULElement | void
22+
interface DKIMWarningsTooltipXULElement extends XULElement {
1223
_warningsBox: XULElement | void
13-
_dkimOnmouseenter: (ev: MouseEvent) => void
14-
_dkimOnmouseleave: (ev: MouseEvent) => void
1524
}
1625

1726
interface DKIMFaviconElement extends XULElement {

experiments/dkimHeader.js

+81-53
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class DKIMWarningsTooltipXUL {
8181
return;
8282
}
8383

84-
/** @type {DKIMTooltipElement} */
84+
/** @type {DKIMWarningsTooltipXULElement} */
8585
// @ts-expect-error
8686
this.element = document.createXULElement("tooltip");
8787

@@ -154,9 +154,6 @@ class DKIMTooltip {
154154
this.element.style.position = "absolute";
155155
this.element.style.zIndex = "99";
156156

157-
// Bottom Tooltip
158-
this.element.style.top = "calc(100% + 10px)";
159-
160157
this.element.style.backgroundColor = "var(--arrowpanel-background)";
161158
this.element.style.color = "var(--arrowpanel-color)";
162159
this.element.style.borderStyle = "solid";
@@ -171,20 +168,30 @@ class DKIMTooltip {
171168
}
172169

173170
/**
174-
* Add the tooltip to the given parent.
171+
* Add the tooltip to the given target.
175172
*
176173
* @protected
177-
* @param {HTMLElement} parent
174+
* @param {DKIMTooltipTarget} target
178175
*/
179-
add(parent) {
180-
const parentSyle = parent.ownerDocument.defaultView?.getComputedStyle(parent);
181-
if (parentSyle?.position === "static") {
182-
parent.style.position = "relative";
176+
add(target) {
177+
if (target._dkimTooltip) {
178+
throw new Error("DKIM: A DKIMTooltip already exist on target");
179+
}
180+
if (this.element._target) {
181+
throw new Error("DKIM: The DKIMTooltip was already added to a target");
183182
}
184183

185-
parent.appendChild(this.element);
186-
parent.addEventListener("mouseenter", this.element._dkimOnmouseenter);
187-
parent.addEventListener("mouseleave", this.element._dkimOnmouseleave);
184+
target._dkimTooltip = this.element;
185+
this.element._target = target;
186+
187+
// The tooltip is added to the body instead of the target
188+
// to avoid potential issues with overflow.
189+
// See also
190+
// - https://stackoverflow.com/questions/36531708/why-does-position-absolute-make-page-to-overflow
191+
// - https://stackoverflow.com/questions/6421966/css-overflow-x-visible-and-overflow-y-hidden-causing-scrollbar-issue
192+
target.ownerDocument.body.appendChild(this.element);
193+
target.addEventListener("mouseenter", this.element._dkimOnmouseenter);
194+
target.addEventListener("mouseleave", this.element._dkimOnmouseleave);
188195
}
189196

190197
/**
@@ -193,10 +200,11 @@ class DKIMTooltip {
193200
* @protected
194201
*/
195202
remove() {
196-
const parent = this.element.parentElement;
197-
if (parent) {
198-
parent.removeEventListener("mouseenter", this.element._dkimOnmouseenter);
199-
parent.removeEventListener("mouseleave", this.element._dkimOnmouseleave);
203+
const target = this.element._target;
204+
if (target) {
205+
target.removeEventListener("mouseenter", this.element._dkimOnmouseenter);
206+
target.removeEventListener("mouseleave", this.element._dkimOnmouseleave);
207+
delete target._dkimTooltip;
200208
}
201209
this.element.remove();
202210
}
@@ -206,10 +214,24 @@ class DKIMTooltip {
206214
* @param {MouseEvent} _event
207215
*/
208216
static #mouseEnter(tooltip, _event) {
209-
if (tooltip.element.parentElement?.title) {
210-
tooltip.element.parentElement.dataset.titleBackup = tooltip.element.parentElement.title;
211-
tooltip.element.parentElement.title = "";
217+
const target = tooltip.element._target;
218+
if (!target) {
219+
throw new Error("DKIM: mouseEnter event called for a DKIMTooltip that has no target");
212220
}
221+
222+
// Avoid title being shown together with tooltip
223+
if (target?.title) {
224+
target.dataset.titleBackup = target.title;
225+
target.title = "";
226+
}
227+
228+
// Calculate and set tooltip position
229+
const clientRect = target.getBoundingClientRect();
230+
const tooltipSpaceToTarget = 10;
231+
tooltip.element.style.top = `${clientRect.bottom + tooltipSpaceToTarget}px`;
232+
tooltip.element.style.left = `${clientRect.left}px`;
233+
234+
// Show tooltip
213235
tooltip.element.style.visibility = "visible";
214236
}
215237

@@ -218,10 +240,14 @@ class DKIMTooltip {
218240
* @param {MouseEvent} _event
219241
*/
220242
static #mouseLeave(tooltip, _event) {
243+
// Hide tooltip
221244
tooltip.element.style.visibility = "hidden";
222-
if (tooltip.element.parentElement?.dataset.titleBackup) {
223-
tooltip.element.parentElement.title = tooltip.element.parentElement.dataset.titleBackup;
224-
tooltip.element.parentElement.dataset.titleBackup = "";
245+
246+
// Restore title
247+
const target = tooltip.element._target;
248+
if (target?.dataset.titleBackup) {
249+
target.title = target.dataset.titleBackup;
250+
target.dataset.titleBackup = "";
225251
}
226252
}
227253
}
@@ -294,7 +320,7 @@ class DkimResultTooltip extends DKIMTooltip {
294320
*
295321
* @param {string[]} warnings
296322
*/
297-
set warnings(warnings) {
323+
set warnings(warnings) {
298324
if (!this.element._warningsBox) {
299325
throw Error("Underlying element of DkimResultTooltip does not contain _warningsBox");
300326
}
@@ -326,15 +352,28 @@ class DkimResultTooltip extends DKIMTooltip {
326352
}
327353

328354
/**
329-
* Get all tooltips under the given document or parent.
355+
* Try getting the tooltip of a target.
356+
*
357+
* @param {DKIMTooltipTarget} target
358+
* @returns {DkimResultTooltip|null}
359+
*/
360+
static get(target) {
361+
if (target._dkimTooltip) {
362+
return new DkimResultTooltip(target._dkimTooltip.ownerDocument, target._dkimTooltip);
363+
}
364+
return null;
365+
}
366+
367+
/**
368+
* Get all tooltips in the given document.
330369
*
331-
* @param {Document|Element} searchRoot
370+
* @param {Document} document
332371
* @returns {DkimResultTooltip[]}
333372
*/
334-
static get(searchRoot) {
373+
static getAll(document) {
335374
// eslint-disable-next-line no-extra-parens
336375
const elements = /** @type {HTMLElement[]} */ (
337-
Array.from(searchRoot.getElementsByClassName(DkimResultTooltip.#class)));
376+
Array.from(document.getElementsByClassName(DkimResultTooltip.#class)));
338377
const tooltips = [];
339378
for (const element of elements) {
340379
tooltips.push(new DkimResultTooltip(element.ownerDocument, element));
@@ -343,33 +382,31 @@ class DkimResultTooltip extends DKIMTooltip {
343382
}
344383

345384
/**
346-
* Add the tooltip for the given parent.
385+
* Add a tooltip to the given target.
347386
*
348-
* @param {HTMLElement} parent
387+
* @param {DKIMTooltipTarget} target
349388
* @returns {DkimResultTooltip}
350389
*/
351-
static add(parent) {
352-
const existingTooltip = DkimResultTooltip.get(parent)[0];
390+
static add(target) {
391+
const existingTooltip = DkimResultTooltip.get(target);
353392
if (existingTooltip) {
354393
console.warn("DKIM: DkimResultTooltip already exist and will be reused");
355394
return existingTooltip;
356395
}
357396

358-
const tooltip = new DkimResultTooltip(parent.ownerDocument);
359-
tooltip.add(parent);
397+
const tooltip = new DkimResultTooltip(target.ownerDocument);
398+
tooltip.add(target);
360399
return tooltip;
361400
}
362401

363402
/**
364-
* Remove the tooltip from the given parent.
403+
* Remove an existing tooltip from the given target.
365404
*
366-
* @param {HTMLElement} parent
405+
* @param {HTMLElement} target
367406
*/
368-
static remove(parent) {
369-
const tooltips = DkimResultTooltip.get(parent);
370-
for (const tooltip of tooltips) {
371-
tooltip.remove();
372-
}
407+
static remove(target) {
408+
const tooltip = DkimResultTooltip.get(target);
409+
tooltip?.remove();
373410
}
374411

375412
static #class = "DkimResultTooltip";
@@ -727,8 +764,9 @@ class DkimFavicon {
727764
if (element) {
728765
// @ts-expect-error
729766
this.element = element;
730-
this._dkimTooltipFrom = DkimResultTooltip.get(this.element)[0];
767+
this._dkimTooltipFrom = DkimResultTooltip.get(this.element);
731768
if (!this._dkimTooltipFrom) {
769+
console.warn("DKIM: DkimResultTooltip for DkimFavicon not found - will recreate it");
732770
this._dkimTooltipFrom = DkimResultTooltip.add(this.element);
733771
}
734772
return;
@@ -821,16 +859,6 @@ class DkimFavicon {
821859
// TB <102
822860
expandedFromBox.prepend(favicon.element);
823861
}
824-
825-
// Add the tooltip used by the favicon and the from address.
826-
// As the tooltip is reused, it can not be defined directly under the favicon.
827-
if ("longEmailAddresses" in expandedFromBox) {
828-
// TB <102
829-
expandedFromBox.longEmailAddresses.prepend(favicon._dkimTooltipFrom.element);
830-
} else {
831-
// TB >=102
832-
expandedFromBox.prepend(favicon._dkimTooltipFrom.element);
833-
}
834862
}
835863

836864
/**
@@ -1226,7 +1254,7 @@ this.dkimHeader = class extends ExtensionCommon.ExtensionAPI {
12261254
const favicon = DkimFavicon.get(document);
12271255
favicon.setFaviconUrl(faviconUrl);
12281256

1229-
const resultTooltips = DkimResultTooltip.get(document);
1257+
const resultTooltips = DkimResultTooltip.getAll(document);
12301258
for (const tooltip of resultTooltips) {
12311259
tooltip.value = result;
12321260
tooltip.warnings = warnings;

0 commit comments

Comments
 (0)