-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathwidget-tooltip.ts
More file actions
103 lines (85 loc) · 3.26 KB
/
Copy pathwidget-tooltip.ts
File metadata and controls
103 lines (85 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
const TOOLTIP_ATTR = 'data-deck-widget-tooltip';
const OFFSET = 8;
const widgetTooltips = new WeakMap<HTMLElement, WidgetTooltip>();
/** Updates delegated tooltip handling after a widget renders its HTML. */
export function updateWidgetTooltip(rootElement: HTMLElement): void {
for (const target of rootElement.querySelectorAll(`[${TOOLTIP_ATTR}][title]`)) {
target.removeAttribute('title');
}
let tooltip = widgetTooltips.get(rootElement);
if (!tooltip) {
tooltip = new WidgetTooltip();
widgetTooltips.set(rootElement, tooltip);
}
tooltip.update(rootElement);
}
class WidgetTooltip {
private element: HTMLDivElement | null = null;
private listenerRoot: HTMLElement | null = null;
update(root: HTMLElement): void {
this.hide();
if (this.listenerRoot === root) return;
this.listenerRoot = root;
root.addEventListener('pointerover', this.onPointerOver);
root.addEventListener('pointerout', this.onPointerOut);
root.addEventListener('focusin', this.onFocusIn);
root.addEventListener('focusout', this.onFocusOut);
root.addEventListener('keydown', this.onKeyDown);
}
private hide(): void {
this.element?.remove();
this.element = null;
}
private onPointerOver = (event: PointerEvent): void => {
const target = this.getTarget(event.target);
if (target) this.show(target);
};
private onPointerOut = (event: PointerEvent): void => {
const target = this.getTarget(event.target);
if (target && !(event.relatedTarget instanceof Node && target.contains(event.relatedTarget))) {
this.hide();
}
};
private onFocusIn = (event: FocusEvent): void => {
const target = this.getTarget(event.target);
if (target?.matches(':focus-visible')) this.show(target);
};
private onFocusOut = (): void => this.hide();
private onKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') this.hide();
};
private getTarget(target: EventTarget | null): HTMLElement | null {
const anchor = target instanceof Element ? target.closest(`[${TOOLTIP_ATTR}]`) : null;
return anchor instanceof HTMLElement ? anchor : null;
}
private show(anchor: HTMLElement): void {
const text = anchor.getAttribute(TOOLTIP_ATTR);
const root = this.listenerRoot;
if (!text || !root) return;
this.hide();
const tooltip = document.createElement('div');
tooltip.className = 'deck-widget-tooltip';
tooltip.setAttribute('role', 'tooltip');
tooltip.append(document.createTextNode(text));
root.append(tooltip);
this.element = tooltip;
this.position(anchor, tooltip, root);
}
private position(anchor: HTMLElement, tooltip: HTMLDivElement, root: HTMLElement): void {
const anchorRect = anchor.getBoundingClientRect();
const rootRect = root.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const right = anchorRect.right + OFFSET;
const left =
right + tooltipRect.width <= window.innerWidth
? right
: anchorRect.left - tooltipRect.width - OFFSET;
tooltip.style.left = `${left - rootRect.left}px`;
tooltip.style.top = `${
anchorRect.top - rootRect.top + (anchorRect.height - tooltipRect.height) / 2
}px`;
}
}