@syware/jlib 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/molecules/jl-tooltip.css?raw
|
|
2
|
-
var jl_tooltip_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n}\n\n/* Ink on the page's ground: dark on a light theme, light on a dark one. */\n.bubble {\n max-width: min(260px, calc(100vw - 16px));\n padding: 6px 9px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-text);\n color: var(--jl-bg);\n font-size: var(--jl-font-xs);\n font-weight: 550;\n line-height: 1.4;\n box-shadow: var(--jl-shadow);\n pointer-events: auto;\n}\n\n.arrow {\n position: absolute;\n width: 8px;\n height: 8px;\n border-radius: 1px;\n background: inherit;\n rotate: 45deg;\n}\n\n.bubble[data-placement^=\"top\"] .arrow {\n top: 100%;\n left: var(--arrow, 50%);\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"bottom\"] .arrow {\n bottom: 100%;\n left: var(--arrow, 50%);\n translate: -50% 50%;\n}\n\n.bubble[data-placement^=\"left\"] .arrow {\n top: var(--arrow, 50%);\n left: 100%;\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"right\"] .arrow {\n top: var(--arrow, 50%);\n right: 100%;\n translate: 50% -50%;\n}\n";
|
|
2
|
+
var jl_tooltip_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n /* A long press shows the bubble: no callout over it, and (below) no text selected under the finger. */\n -webkit-touch-callout: none;\n}\n\n@media (pointer: coarse) {\n ::slotted(*) {\n -webkit-user-select: none;\n user-select: none;\n }\n}\n\n/* Ink on the page's ground: dark on a light theme, light on a dark one. */\n.bubble {\n max-width: min(260px, calc(100vw - 16px));\n padding: 6px 9px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-text);\n color: var(--jl-bg);\n font-size: var(--jl-font-xs);\n font-weight: 550;\n line-height: 1.4;\n box-shadow: var(--jl-shadow);\n pointer-events: auto;\n}\n\n.arrow {\n position: absolute;\n width: 8px;\n height: 8px;\n border-radius: 1px;\n background: inherit;\n rotate: 45deg;\n}\n\n.bubble[data-placement^=\"top\"] .arrow {\n top: 100%;\n left: var(--arrow, 50%);\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"bottom\"] .arrow {\n bottom: 100%;\n left: var(--arrow, 50%);\n translate: -50% 50%;\n}\n\n.bubble[data-placement^=\"left\"] .arrow {\n top: var(--arrow, 50%);\n left: 100%;\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"right\"] .arrow {\n top: var(--arrow, 50%);\n right: 100%;\n translate: 50% -50%;\n}\n";
|
|
3
3
|
export { jl_tooltip_default as default };
|
|
@@ -2,7 +2,13 @@ import { JlElement } from '../base/jl-element';
|
|
|
2
2
|
import type { Placement } from '../types/floating.types';
|
|
3
3
|
/**
|
|
4
4
|
* A short hint in a dark bubble by its trigger (the slotted element): it shows on hover and on keyboard focus after
|
|
5
|
-
* a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description.
|
|
5
|
+
* a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description. While
|
|
6
|
+
* the pointer or the focus is on it, the trigger's own `title` (a button's `hint`) is set aside, so the browser's
|
|
7
|
+
* tooltip does not show over the bubble.
|
|
8
|
+
*
|
|
9
|
+
* Touch screens have no hover: pressing and holding a trigger shows the bubble (and the press does not act), and
|
|
10
|
+
* it stays a moment after the finger lifts; a trigger that does nothing on a tap (a word, an icon) shows or hides
|
|
11
|
+
* it on a tap.
|
|
6
12
|
*/
|
|
7
13
|
export declare class JlTooltip extends JlElement {
|
|
8
14
|
static readonly selector = "jl-tooltip";
|
|
@@ -31,6 +37,25 @@ export declare class JlTooltip extends JlElement {
|
|
|
31
37
|
/** Hides the bubble now. */
|
|
32
38
|
hide(): void;
|
|
33
39
|
private close;
|
|
40
|
+
/** A trigger that acts on a tap (a button, a link, a field), as opposed to a word or an icon. */
|
|
41
|
+
private isInteractive;
|
|
42
|
+
private pressStart;
|
|
43
|
+
private pressMove;
|
|
44
|
+
/** The finger lifted, or the page took it to scroll: a bubble the press showed stays a moment, then goes. */
|
|
45
|
+
private pressEnd;
|
|
46
|
+
/** The system's own long press: the bubble shows now, and the menu it would open does not, but over a link. */
|
|
47
|
+
private pressHeld;
|
|
48
|
+
private openFromPress;
|
|
49
|
+
private swallowLongPress;
|
|
50
|
+
private hideBubble;
|
|
51
|
+
/**
|
|
52
|
+
* Sets aside the native `title` of the trigger and of its inner control: the bubble says it, and the browser's
|
|
53
|
+
* own tooltip would show over it a moment later. The control keeps its name for screen readers (a button's hint
|
|
54
|
+
* is its `aria-label` too).
|
|
55
|
+
*/
|
|
56
|
+
private holdTitles;
|
|
57
|
+
/** Gives the titles back, unless the trigger wrote new ones meanwhile. */
|
|
58
|
+
private restoreTitles;
|
|
34
59
|
private trigger;
|
|
35
60
|
private isOpen;
|
|
36
61
|
private scheduleShow;
|
|
@@ -14,10 +14,22 @@ var HIDE_GRACE_MS = 120;
|
|
|
14
14
|
var GAP = 9;
|
|
15
15
|
var ARROW_MARGIN = 10;
|
|
16
16
|
var DESCRIPTION = "aria-description";
|
|
17
|
+
var TITLE = "title";
|
|
18
|
+
var TOUCH = "touch";
|
|
19
|
+
var LONG_PRESS_MS = 500;
|
|
20
|
+
var MOVE_TOLERANCE_PX = 10;
|
|
21
|
+
var TOUCH_LINGER_MS = 1500;
|
|
22
|
+
var INTERACTIVE = "a[href], button, input, select, textarea, summary, [role=\"button\"], [role=\"link\"]";
|
|
17
23
|
var sideOf = (placement) => placement.split("-")[0];
|
|
18
24
|
/**
|
|
19
25
|
* A short hint in a dark bubble by its trigger (the slotted element): it shows on hover and on keyboard focus after
|
|
20
|
-
* a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description.
|
|
26
|
+
* a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description. While
|
|
27
|
+
* the pointer or the focus is on it, the trigger's own `title` (a button's `hint`) is set aside, so the browser's
|
|
28
|
+
* tooltip does not show over the bubble.
|
|
29
|
+
*
|
|
30
|
+
* Touch screens have no hover: pressing and holding a trigger shows the bubble (and the press does not act), and
|
|
31
|
+
* it stays a moment after the finger lifts; a trigger that does nothing on a tap (a word, an icon) shows or hides
|
|
32
|
+
* it on a tap.
|
|
21
33
|
*/
|
|
22
34
|
var JlTooltip = class extends JlElement {
|
|
23
35
|
static selector = "jl-tooltip";
|
|
@@ -35,7 +47,15 @@ var JlTooltip = class extends JlElement {
|
|
|
35
47
|
_state = {
|
|
36
48
|
described: null,
|
|
37
49
|
following: new AbortController(),
|
|
50
|
+
heldTitles: [],
|
|
38
51
|
hideTimer: 0,
|
|
52
|
+
press: {
|
|
53
|
+
down: false,
|
|
54
|
+
opened: false,
|
|
55
|
+
timer: 0,
|
|
56
|
+
x: 0,
|
|
57
|
+
y: 0
|
|
58
|
+
},
|
|
39
59
|
showTimer: 0
|
|
40
60
|
};
|
|
41
61
|
_refs = this.useRefs((ref) => ({
|
|
@@ -58,11 +78,34 @@ var JlTooltip = class extends JlElement {
|
|
|
58
78
|
onConnect() {
|
|
59
79
|
this.describeTrigger();
|
|
60
80
|
this.on(this._refs.slot, "slotchange", () => this.describeTrigger());
|
|
61
|
-
this.on(this, "pointerenter", () =>
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
this.on(this, "pointerenter", (event) => {
|
|
82
|
+
if (event.pointerType === TOUCH) return;
|
|
83
|
+
this.holdTitles();
|
|
84
|
+
this.scheduleShow();
|
|
85
|
+
});
|
|
86
|
+
this.on(this, "pointerleave", (event) => {
|
|
87
|
+
if (event.pointerType !== TOUCH) this.scheduleHide();
|
|
88
|
+
});
|
|
89
|
+
this.on(this, "pointerdown", (event) => {
|
|
90
|
+
if (event.pointerType === TOUCH) {
|
|
91
|
+
this.pressStart(event);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.hideBubble();
|
|
95
|
+
});
|
|
96
|
+
this.on(this, "pointermove", (event) => this.pressMove(event));
|
|
97
|
+
this.on(this, "pointerup", () => this.pressEnd());
|
|
98
|
+
this.on(this, "pointercancel", () => this.pressEnd());
|
|
99
|
+
this.on(this, "contextmenu", (event) => this.pressHeld(event));
|
|
100
|
+
this.addEventListener("click", (event) => this.swallowLongPress(event), {
|
|
101
|
+
capture: true,
|
|
102
|
+
signal: this.killSignal
|
|
103
|
+
});
|
|
64
104
|
this.on(this, "focusin", () => {
|
|
65
|
-
if (deepActiveElement(document)?.matches(":focus-visible") === true)
|
|
105
|
+
if (deepActiveElement(document)?.matches(":focus-visible") === true) {
|
|
106
|
+
this.holdTitles();
|
|
107
|
+
this.scheduleShow();
|
|
108
|
+
}
|
|
66
109
|
});
|
|
67
110
|
this.on(this, "focusout", () => this.close());
|
|
68
111
|
}
|
|
@@ -78,12 +121,93 @@ var JlTooltip = class extends JlElement {
|
|
|
78
121
|
this.whenRendered(() => this.close());
|
|
79
122
|
}
|
|
80
123
|
close() {
|
|
124
|
+
this.hideBubble();
|
|
125
|
+
this.restoreTitles();
|
|
126
|
+
}
|
|
127
|
+
/** A trigger that acts on a tap (a button, a link, a field), as opposed to a word or an icon. */
|
|
128
|
+
isInteractive() {
|
|
129
|
+
return accessibleTarget(this.trigger()).matches(INTERACTIVE);
|
|
130
|
+
}
|
|
131
|
+
pressStart(event) {
|
|
132
|
+
const press = this._state.press;
|
|
133
|
+
window.clearTimeout(press.timer);
|
|
134
|
+
press.opened = false;
|
|
135
|
+
if (!this.isInteractive()) {
|
|
136
|
+
if (this.isOpen()) {
|
|
137
|
+
this.close();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.open();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
press.down = true;
|
|
144
|
+
press.x = event.clientX;
|
|
145
|
+
press.y = event.clientY;
|
|
146
|
+
press.timer = window.setTimeout(() => this.openFromPress(), LONG_PRESS_MS);
|
|
147
|
+
}
|
|
148
|
+
pressMove(event) {
|
|
149
|
+
const press = this._state.press;
|
|
150
|
+
const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
|
|
151
|
+
if (press.down && moved > MOVE_TOLERANCE_PX) window.clearTimeout(press.timer);
|
|
152
|
+
}
|
|
153
|
+
/** The finger lifted, or the page took it to scroll: a bubble the press showed stays a moment, then goes. */
|
|
154
|
+
pressEnd() {
|
|
155
|
+
const press = this._state.press;
|
|
156
|
+
window.clearTimeout(press.timer);
|
|
157
|
+
press.down = false;
|
|
158
|
+
if (!press.opened) return;
|
|
159
|
+
window.clearTimeout(this._state.hideTimer);
|
|
160
|
+
this._state.hideTimer = window.setTimeout(() => this.close(), TOUCH_LINGER_MS);
|
|
161
|
+
}
|
|
162
|
+
/** The system's own long press: the bubble shows now, and the menu it would open does not, but over a link. */
|
|
163
|
+
pressHeld(event) {
|
|
164
|
+
if (!this._state.press.down || !this.isInteractive()) return;
|
|
165
|
+
this.openFromPress();
|
|
166
|
+
if (!accessibleTarget(this.trigger()).matches("a[href]")) event.preventDefault();
|
|
167
|
+
}
|
|
168
|
+
openFromPress() {
|
|
169
|
+
window.clearTimeout(this._state.press.timer);
|
|
170
|
+
this._state.press.opened = true;
|
|
171
|
+
this.open();
|
|
172
|
+
}
|
|
173
|
+
swallowLongPress(event) {
|
|
174
|
+
const press = this._state.press;
|
|
175
|
+
if (!press.opened) return;
|
|
176
|
+
press.opened = false;
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
event.stopPropagation();
|
|
179
|
+
}
|
|
180
|
+
hideBubble() {
|
|
81
181
|
window.clearTimeout(this._state.showTimer);
|
|
82
182
|
window.clearTimeout(this._state.hideTimer);
|
|
83
183
|
this._state.following.abort();
|
|
84
184
|
const bubble = this._refs.bubble;
|
|
85
185
|
if (bubble.matches(":popover-open")) bubble.hidePopover();
|
|
86
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Sets aside the native `title` of the trigger and of its inner control: the bubble says it, and the browser's
|
|
189
|
+
* own tooltip would show over it a moment later. The control keeps its name for screen readers (a button's hint
|
|
190
|
+
* is its `aria-label` too).
|
|
191
|
+
*/
|
|
192
|
+
holdTitles() {
|
|
193
|
+
if (this.text.get() === "" || this._state.heldTitles.length > 0) return;
|
|
194
|
+
const trigger = this.trigger();
|
|
195
|
+
const elements = [.../* @__PURE__ */ new Set([trigger, accessibleTarget(trigger)])];
|
|
196
|
+
this._state.heldTitles = elements.flatMap((element) => {
|
|
197
|
+
const title = element.getAttribute(TITLE);
|
|
198
|
+
if (title === null) return [];
|
|
199
|
+
element.removeAttribute(TITLE);
|
|
200
|
+
return [{
|
|
201
|
+
element,
|
|
202
|
+
title
|
|
203
|
+
}];
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Gives the titles back, unless the trigger wrote new ones meanwhile. */
|
|
207
|
+
restoreTitles() {
|
|
208
|
+
for (const { element, title } of this._state.heldTitles) if (!element.hasAttribute(TITLE)) element.setAttribute(TITLE, title);
|
|
209
|
+
this._state.heldTitles = [];
|
|
210
|
+
}
|
|
87
211
|
trigger() {
|
|
88
212
|
return this._refs.slot.assignedElements({ flatten: true })[0] ?? this;
|
|
89
213
|
}
|
|
@@ -117,6 +241,9 @@ var JlTooltip = class extends JlElement {
|
|
|
117
241
|
passive: true,
|
|
118
242
|
signal: following.signal
|
|
119
243
|
});
|
|
244
|
+
document.addEventListener("pointerdown", (event) => {
|
|
245
|
+
if (!event.composedPath().includes(this)) this.close();
|
|
246
|
+
}, { signal: following.signal });
|
|
120
247
|
document.addEventListener("keydown", (event) => {
|
|
121
248
|
if (event.key === "Escape") this.close();
|
|
122
249
|
}, { signal: following.signal });
|