@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", () => this.scheduleShow());
62
- this.on(this, "pointerleave", () => this.scheduleHide());
63
- this.on(this, "pointerdown", () => this.close());
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) this.scheduleShow();
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syware/jlib",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A tree-shakeable component library on Jadis and native Web Components, organised in atomic design",
5
5
  "keywords": [
6
6
  "web-components",