@apliteni/apliteni-ui 0.34.0 → 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 +1 -1
- package/src/components/tooltip.js +115 -8
- package/src/styles/layout.css +21 -4
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
212
|
-
*
|
|
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
|
}
|
package/src/styles/layout.css
CHANGED
|
@@ -333,9 +333,11 @@
|
|
|
333
333
|
.ui-app--topbar > .ui-app__rail > .ui-app__head { height: var(--ui-app-band); padding-bottom: 0; }
|
|
334
334
|
.ui-app--topbar > .ui-app__rail > .ui-app__foot { height: var(--ui-app-band); padding-top: 0; }
|
|
335
335
|
|
|
336
|
-
/* Drawn as a field, behaving as a button. The
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
/* Drawn as a field, behaving as a button. The hairline and the radius are the
|
|
337
|
+
field's — src/styles/input.css:20 `.ui-input,` sets both — because what it looks
|
|
338
|
+
like is the point. The STEP is not, and that is the one thing this control does
|
|
339
|
+
differently from every other field in the kit: the reason is on the declaration
|
|
340
|
+
below. It gives way before anything else on the band.
|
|
339
341
|
why: docs/specification.md#the-page-shell */
|
|
340
342
|
.ui-app__search {
|
|
341
343
|
display: flex;
|
|
@@ -350,13 +352,28 @@
|
|
|
350
352
|
font-size: var(--text-sm);
|
|
351
353
|
text-align: start;
|
|
352
354
|
color: var(--muted);
|
|
353
|
-
|
|
355
|
+
/* --surface, not the sunken --surface-2 every other field in the kit takes:
|
|
356
|
+
those sit on a card and this one sits on the band, and the band IS --bg, the
|
|
357
|
+
bottom of the ladder — so a sunken fill has nowhere to go but down into its
|
|
358
|
+
own ground. Measured in light, the field was 1.10:1 BELOW the band and its
|
|
359
|
+
key cap 1.04:1 against the field it sits in; they are 1.08 above and 1.14 now.
|
|
360
|
+
Dark trades the other way and is stated rather than hidden: the cap goes
|
|
361
|
+
1.29 -> 1.16, because --surface-3 and --surface are adjacent rungs there.
|
|
362
|
+
Artur chose this over three alternatives on #318; the survey of what seven
|
|
363
|
+
other systems do is docs/reviews/318-search-field.md.
|
|
364
|
+
why: docs/specification.md#elevation */
|
|
365
|
+
background: var(--surface);
|
|
354
366
|
border: 1px solid var(--border);
|
|
355
367
|
border-radius: var(--radius-md);
|
|
356
368
|
cursor: pointer;
|
|
357
369
|
transition: border-color var(--dur-med) var(--ease), color var(--dur-med) var(--ease);
|
|
358
370
|
}
|
|
359
371
|
.ui-app__search:hover { border-color: var(--border-strong); color: var(--strong); }
|
|
372
|
+
/* Every other control on the band takes the kit's ring. This one is a <button>
|
|
373
|
+
without `.ui-btn`, so base.css's shared rule never reached it and focus fell
|
|
374
|
+
through to the browser's own square outline, drawn around a 12px radius.
|
|
375
|
+
why: docs/specification.md#the-focus-ring */
|
|
376
|
+
.ui-app__search:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
|
|
360
377
|
/* The same glyph the palette draws in its own search box, at the same box and the
|
|
361
378
|
same stroke: the trigger and the thing it opens are not two search marks.
|
|
362
379
|
src/styles/command-palette.css:107 `.ui-cmdk__search-ic svg` */
|