@apliteni/apliteni-ui 0.34.1 → 0.34.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.34.1",
3
+ "version": "0.34.2",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -9,7 +9,8 @@
9
9
  // wireTooltip(container);
10
10
  //
11
11
  // A mark carries `data-tip-value`, with `data-tip-label` and `data-tip-detail`;
12
- // a `[data-tip-anchor]` inside it is where the readout opens.
12
+ // a `[data-tip-anchor]` inside it is where the readout opens. Under a finger
13
+ // there is no hover, so a tap is the switch: see Pointer kinds below.
13
14
  // why: docs/specification.md#the-hover-readout
14
15
  import { esc } from './index.js';
15
16
 
@@ -157,7 +158,7 @@ export function showTooltip(host, mark) {
157
158
  host.__tipDismissed = null;
158
159
  tip.__tipHost = host;
159
160
  tip.classList.add('is-open');
160
- wireEscape(host.ownerDocument);
161
+ wireDocument(host.ownerDocument);
161
162
  }
162
163
 
163
164
  function releaseMark(host) {
@@ -177,15 +178,60 @@ export function hideTooltip(host) {
177
178
  host.__tipDismissed = null;
178
179
  }
179
180
 
181
+ // A tap that closes a readout dismisses that mark the way Escape does, so a
182
+ // chart sampling its own marks does not bring it straight back. Which is true
183
+ // of the tap that lands on the mark and of the tap that lands on the host's
184
+ // ground beside it: both are the reader putting the readout away.
185
+ function dismiss(host) {
186
+ const mark = host.__tipMark;
187
+ close(host);
188
+ host.__tipDismissed = mark;
189
+ }
190
+
191
+ // ---- Pointer kinds -------------------------------------------------------
192
+
193
+ // A finger has no hover. It arrives already pressing, so the pointerover under
194
+ // a tap is the tap itself: a readout opened on it would open on the way to
195
+ // whatever the mark does when pressed, and go again the moment the finger
196
+ // lifts. Under a coarse pointer the tap is the switch instead — one tap opens
197
+ // the readout, the next closes it, and a tap anywhere else closes it too.
198
+
199
+ /** Whether the device reports no fine pointer at all: a phone, a tablet. */
200
+ function coarse(doc) {
201
+ try {
202
+ return doc.defaultView?.matchMedia?.('(pointer: coarse)')?.matches === true;
203
+ } catch { return false; }
204
+ }
205
+
206
+ // A pointer event says which kind of pointer it is; a click and a focus do not,
207
+ // so the last kind seen is remembered for them. A keystroke clears it, because
208
+ // a keyboard on a tablet is a third way in and focus has to open the readout
209
+ // again after a tap. Before any of that, the media query answers.
210
+ //
211
+ // A pen is on the tap's side of that line with the finger. It presses a screen
212
+ // rather than resting over one, the tablets it comes with report `(pointer:
213
+ // coarse)`, and a hover a pen does offer is a few millimetres of one nothing
214
+ // here is placed against — so a stylus that fell to the hover path would get
215
+ // the flash this component exists to remove. A mouse is the fine pointer.
216
+ function touching(doc, e) {
217
+ if (e?.pointerType) doc.__tipTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
218
+ return doc.__tipTouch ?? coarse(doc);
219
+ }
220
+
180
221
  // Escape dismisses every readout the kit is showing without the pointer having
181
222
  // to move — the reader it covers something for. One rendered open and never
182
223
  // shown is a picture, and stays. The dismissed mark is remembered until another
183
224
  // mark shows or the pointer leaves, so crossing the gap between marks and coming
184
225
  // back does not return the readout to the mark it was dismissed from.
185
- function wireEscape(doc) {
186
- if (doc.__tipEscapeWired) return;
187
- doc.__tipEscapeWired = true;
226
+ //
227
+ // The same document is where the kind of pointer in play is tracked, and where
228
+ // a tap that lands outside a readout's host takes that readout down.
229
+ function wireDocument(doc) {
230
+ if (doc.__tipDocWired) return;
231
+ doc.__tipDocWired = true;
188
232
  doc.addEventListener('keydown', (e) => {
233
+ doc.__tipTouch = false;
234
+ doc.__tipTapping = false;
189
235
  if (e.key !== 'Escape') return;
190
236
  doc.querySelectorAll('[data-tip].is-open').forEach((tip) => {
191
237
  const host = tip.__tipHost;
@@ -195,6 +241,38 @@ function wireEscape(doc) {
195
241
  host.__tipDismissed = mark;
196
242
  });
197
243
  });
244
+ // A tap lands focus on the mark on its way to the click that decides, so the
245
+ // focus between a pointerdown and its click is the tap's own. Focus that no
246
+ // tap brought is not, and opens the readout under a coarse pointer as it
247
+ // always did — a reader swiping through the marks with a screen reader gets
248
+ // the value and the `aria-describedby` that announces it.
249
+ doc.addEventListener('pointerdown', (e) => { touching(doc, e); doc.__tipTapping = true; }, true);
250
+ doc.addEventListener('pointercancel', () => { doc.__tipTapping = false; }, true);
251
+ // Captured, so a host the tap lands in has already lost every other host's
252
+ // readout by the time it opens its own.
253
+ doc.addEventListener('click', (e) => {
254
+ if (e.__tipDismissal) return;
255
+ doc.__tipTapping = false;
256
+ if (!touching(doc)) return;
257
+ doc.querySelectorAll('[data-tip].is-open').forEach((tip) => {
258
+ const host = tip.__tipHost;
259
+ if (host && !host.contains(e.target)) hideTooltip(host);
260
+ });
261
+ }, true);
262
+ }
263
+
264
+ // An opening tap is stopped on its way to the mark, and stopping it takes it
265
+ // away from the document as well — where other overlays keep the click listener
266
+ // that dismisses them, so a dropdown panel would stand open behind the readout
267
+ // the tap just opened. The tap is handed to the document itself instead. Its
268
+ // target is the root element, which every dismissal listener hears and no
269
+ // delegated trigger matches, since no trigger's attribute is on the root.
270
+ function dismissOverlays(doc) {
271
+ const view = doc.defaultView;
272
+ if (!view || !doc.documentElement) return;
273
+ const passed = new view.MouseEvent('click', { bubbles: true, cancelable: true });
274
+ passed.__tipDismissal = true;
275
+ doc.documentElement.dispatchEvent(passed);
198
276
  }
199
277
 
200
278
  // The readout is placed in px from its host, so the host has to be the box it is
@@ -208,8 +286,10 @@ function anchorHost(host) {
208
286
 
209
287
  /**
210
288
  * Wire every `[data-tip-host]` under root: a pointer resting on a mark, or focus
211
- * landing on one, shows the readout; leaving the marks hides it. A host with no
212
- * readout of its own is given one here, once, never on hover. Safe to call again.
289
+ * landing on one, shows the readout; leaving the marks hides it. Under a coarse
290
+ * pointer nothing rests, so a tap on a mark opens the readout and a second tap —
291
+ * or a tap anywhere else — closes it. A host with no readout of its own is given
292
+ * one here, once, never on hover. Safe to call again.
213
293
  */
214
294
  export function wireTooltip(root = document) {
215
295
  const hosts = [...root.querySelectorAll('[data-tip-host]')];
@@ -217,6 +297,8 @@ export function wireTooltip(root = document) {
217
297
  hosts.forEach((host) => {
218
298
  if (host.__tipWired) return;
219
299
  host.__tipWired = true;
300
+ const doc = host.ownerDocument;
301
+ wireDocument(doc);
220
302
  anchorHost(host);
221
303
  if (!tipOf(host)) host.insertAdjacentHTML('beforeend', tooltip());
222
304
  const markOf = (t) => {
@@ -225,16 +307,41 @@ export function wireTooltip(root = document) {
225
307
  };
226
308
  host.addEventListener('pointerover', (e) => {
227
309
  anchorHost(host);
310
+ if (touching(doc, e)) return;
228
311
  const mark = markOf(e.target);
229
312
  if (!mark) close(host);
230
313
  else if (mark !== host.__tipMark) showTooltip(host, mark);
231
314
  });
232
- host.addEventListener('pointerleave', () => hideTooltip(host));
315
+ host.addEventListener('pointerleave', (e) => { if (!touching(doc, e)) hideTooltip(host); });
233
316
  host.addEventListener('focusin', (e) => {
234
317
  anchorHost(host);
318
+ // A tap lands focus on its way to the click that decides, so under a
319
+ // finger the readout waits for that tap rather than opening twice. Focus
320
+ // arriving on its own under the same pointer is a reader reaching the
321
+ // mark another way, and opens it.
322
+ if (touching(doc) && doc.__tipTapping) return;
235
323
  const mark = markOf(e.target);
236
324
  if (mark) showTooltip(host, mark);
237
325
  });
238
326
  host.addEventListener('focusout', (e) => { if (!markOf(e.relatedTarget)) hideTooltip(host); });
327
+ // The tap. Captured, so the tap that opens the readout is spent opening it
328
+ // and never reaches the mark's own click — a chart drilling down on a bar
329
+ // does not drill down on the tap that asked what the bar says. The tap that
330
+ // closes the readout is let through, so the drill-down is one tap further.
331
+ // What the opening tap is stopped from reaching, it is handed to: see
332
+ // dismissOverlays(), so an open overlay closes under it as under any tap.
333
+ host.addEventListener('click', (e) => {
334
+ if (!touching(doc)) return;
335
+ anchorHost(host);
336
+ const mark = markOf(e.target);
337
+ if (!mark || mark === host.__tipMark) { dismiss(host); return; }
338
+ e.preventDefault();
339
+ e.stopPropagation();
340
+ dismissOverlays(doc);
341
+ // A tap is deliberate, so it ends a dismissal rather than being refused by
342
+ // one. What a dismissal stops is the readout coming back on its own.
343
+ host.__tipDismissed = null;
344
+ showTooltip(host, mark);
345
+ }, true);
239
346
  });
240
347
  }