@inglorious/ui 7.1.5 → 7.1.6
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": "@inglorious/ui",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.6",
|
|
4
4
|
"description": "A design system for Inglorious Web with render-function components and customizable themes",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,24 +21,72 @@ import {
|
|
|
21
21
|
|
|
22
22
|
const NO_FOCUSED_INDEX = -1
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Window in which an emulated click is treated as the tail of a pointer
|
|
26
|
+
* interaction that already handled the gesture.
|
|
27
|
+
*/
|
|
28
|
+
const GHOST_CLICK_MS = 700
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Last pointer activation per control element, used to stop the click that
|
|
32
|
+
* browsers synthesise after a tap from toggling the dropdown a second time.
|
|
33
|
+
* @type {WeakMap<Element, number>}
|
|
34
|
+
*/
|
|
35
|
+
const pointerActivations = new WeakMap()
|
|
25
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Pending outside-pointer listeners, keyed by combobox id. Each entry owns both
|
|
39
|
+
* the deferred timer and the listener so both can be torn down on unmount.
|
|
40
|
+
* @type {Map<string|number, { timer: any, listener: (event: Event) => void }>}
|
|
41
|
+
*/
|
|
42
|
+
const outsideClicks = new Map()
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attach (or tear down) the document listener that closes the dropdown when the
|
|
46
|
+
* user interacts outside of it.
|
|
47
|
+
*
|
|
48
|
+
* This listens for `pointerdown` rather than `click` on purpose. Mobile browsers
|
|
49
|
+
* synthesise `click` asynchronously after `touchend`, and drop it entirely when
|
|
50
|
+
* the touch drifts into a scroll gesture or the target is replaced meanwhile.
|
|
51
|
+
* A suppressed click leaves the listener armed, so it then closes the *next*
|
|
52
|
+
* dropdown on the tap that opened it and the combobox looks stuck. `pointerdown`
|
|
53
|
+
* is dispatched synchronously for mouse, touch and pen, so there is no race.
|
|
54
|
+
*
|
|
55
|
+
* The registration is deferred by a macrotask because the pointerdown that opens
|
|
56
|
+
* the dropdown is already travelling towards the document. The downside is that
|
|
57
|
+
* the dropdown can unmount before the timer runs, which would leave a listener
|
|
58
|
+
* bound to a detached node behind; tearing the pending timer down on unmount
|
|
59
|
+
* avoids that.
|
|
60
|
+
* @param {ComboboxProps} props
|
|
61
|
+
* @param {Element|null|undefined} el dropdown element, or undefined on unmount
|
|
62
|
+
*/
|
|
26
63
|
function trackOutsideClick(props, el) {
|
|
27
|
-
const
|
|
64
|
+
const key = props.id
|
|
65
|
+
const previous = outsideClicks.get(key)
|
|
66
|
+
|
|
28
67
|
if (previous) {
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
clearTimeout(previous.timer)
|
|
69
|
+
document.removeEventListener("pointerdown", previous.listener)
|
|
70
|
+
outsideClicks.delete(key)
|
|
31
71
|
}
|
|
32
72
|
|
|
33
73
|
if (!el) return
|
|
34
74
|
|
|
35
|
-
|
|
75
|
+
// Resolve the root so the control and the dropdown both count as inside.
|
|
76
|
+
const root = el.closest(".iw-combobox") ?? el
|
|
36
77
|
const listener = (event) => {
|
|
78
|
+
outsideClicks.delete(key)
|
|
79
|
+
// composedPath is captured at dispatch time, so it still reports the
|
|
80
|
+
// combobox as an ancestor even when the pressed node was removed from the
|
|
81
|
+
// DOM while the event was bubbling.
|
|
37
82
|
if (!event.composedPath().includes(root)) props.onClose?.()
|
|
38
83
|
}
|
|
39
84
|
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
document.addEventListener("pointerdown", listener, { once: true })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
outsideClicks.set(key, { timer, listener })
|
|
42
90
|
}
|
|
43
91
|
|
|
44
92
|
export const Combobox = {
|
|
@@ -92,7 +140,22 @@ export const Combobox = {
|
|
|
92
140
|
Array.isArray(props.selectedValue) &&
|
|
93
141
|
props.selectedValue.length,
|
|
94
142
|
})}"
|
|
95
|
-
@
|
|
143
|
+
@pointerdown=${(event) => {
|
|
144
|
+
// Touch and pen only: the mouse keeps using click, so a mouse press
|
|
145
|
+
// never runs the toggle twice.
|
|
146
|
+
if (props.isDisabled) return
|
|
147
|
+
if (event.pointerType === "mouse") return
|
|
148
|
+
pointerActivations.set(event.currentTarget, Date.now())
|
|
149
|
+
props.onToggle?.()
|
|
150
|
+
}}
|
|
151
|
+
@click=${(event) => {
|
|
152
|
+
if (props.isDisabled) return
|
|
153
|
+
// Swallow the click a browser synthesises after a tap that the
|
|
154
|
+
// pointerdown above already handled.
|
|
155
|
+
const lastActivation = pointerActivations.get(event.currentTarget) ?? 0
|
|
156
|
+
if (Date.now() - lastActivation < GHOST_CLICK_MS) return
|
|
157
|
+
props.onToggle?.()
|
|
158
|
+
}}
|
|
96
159
|
>
|
|
97
160
|
${when(
|
|
98
161
|
props.isMulti,
|
|
@@ -109,6 +172,7 @@ export const Combobox = {
|
|
|
109
172
|
html`<button
|
|
110
173
|
class="iw-combobox-clear"
|
|
111
174
|
type="button"
|
|
175
|
+
@pointerdown=${(event) => event.stopPropagation()}
|
|
112
176
|
@click=${(event) => {
|
|
113
177
|
event.stopPropagation()
|
|
114
178
|
props.onClear?.()
|
|
@@ -184,6 +248,7 @@ export const Combobox = {
|
|
|
184
248
|
|
|
185
249
|
return html`<span
|
|
186
250
|
class="iw-combobox-multi-value-tag"
|
|
251
|
+
@pointerdown=${(event) => event.stopPropagation()}
|
|
187
252
|
@click=${(event) => event.stopPropagation()}
|
|
188
253
|
>
|
|
189
254
|
${Chip.render({
|
|
@@ -239,7 +304,9 @@ export const Combobox = {
|
|
|
239
304
|
@input=${(event) => props.onSearchChange?.(event.target.value)}
|
|
240
305
|
@keydown=${(event) => handleKeyDown(props, event)}
|
|
241
306
|
${ref((el) => {
|
|
242
|
-
if (el && props.isOpen
|
|
307
|
+
if (el && props.isOpen && shouldFocusSearch()) {
|
|
308
|
+
queueMicrotask(() => el.focus())
|
|
309
|
+
}
|
|
243
310
|
})}
|
|
244
311
|
/>`
|
|
245
312
|
},
|
|
@@ -327,6 +394,25 @@ export const Combobox = {
|
|
|
327
394
|
},
|
|
328
395
|
}
|
|
329
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Whether opening the dropdown should move focus into the search field.
|
|
399
|
+
*
|
|
400
|
+
* Focusing it is what lets keyboard users start typing straight away, but on a
|
|
401
|
+
* touch device it raises the on-screen keyboard, which covers the very option
|
|
402
|
+
* list the dropdown was opened to read. Coarse pointers therefore open the
|
|
403
|
+
* dropdown without focusing the search field.
|
|
404
|
+
* @returns {boolean}
|
|
405
|
+
*/
|
|
406
|
+
function shouldFocusSearch() {
|
|
407
|
+
if (
|
|
408
|
+
typeof window === "undefined" ||
|
|
409
|
+
typeof window.matchMedia !== "function"
|
|
410
|
+
) {
|
|
411
|
+
return true
|
|
412
|
+
}
|
|
413
|
+
return window.matchMedia("(hover: hover) and (pointer: fine)").matches
|
|
414
|
+
}
|
|
415
|
+
|
|
330
416
|
/**
|
|
331
417
|
* Handle key navigation.
|
|
332
418
|
* @param {ComboboxProps} props
|