|
17 | 17 | strategy?: 'absolute' | 'fixed';
|
18 | 18 | open?: boolean;
|
19 | 19 | yOnly?: boolean;
|
| 20 | + closeOnTouchDelay?: number; |
20 | 21 | }
|
21 | 22 |
|
22 | 23 | export let activeContent: boolean = false;
|
|
29 | 30 | export let strategy: 'absolute' | 'fixed' = 'absolute';
|
30 | 31 | export let open: boolean = false;
|
31 | 32 | export let yOnly: boolean = false;
|
| 33 | + export let closeOnTouchDelay: number = 3000; |
32 | 34 | // extra floating UI middleware list
|
33 | 35 | export let middlewares: Middleware[] = [dom.flip(), dom.shift()];
|
34 | 36 |
|
|
52 | 54 | let arrowEl: HTMLElement | null;
|
53 | 55 | let contentEl: HTMLElement;
|
54 | 56 | let triggerEls: HTMLElement[] = [];
|
| 57 | + let timer: number|undefined = undefined; |
55 | 58 |
|
56 | 59 | const showHandler = (ev: Event) => {
|
57 | 60 | if (referenceEl === undefined) console.error('trigger undefined');
|
|
60 | 63 | if (open) return; // If the popper is already open after the reference element has changed
|
61 | 64 | }
|
62 | 65 |
|
63 |
| - open = ev.type === 'click' ? !open : true; |
| 66 | + setTimeout(() => { |
| 67 | + open = ev.type === 'click' ? !open : true; |
| 68 | + }, (ev as PointerEvent).pointerType === "touch" ? 300 : 0) |
64 | 69 | };
|
65 | 70 |
|
66 | 71 | const hasHover = (el: Element) => el.matches(':hover');
|
67 | 72 | const hasFocus = (el: Element) => el.contains(document.activeElement);
|
68 | 73 | const px = (n: number | undefined) => (n ? `${n}px` : '');
|
69 | 74 |
|
70 | 75 | const hideHandler = (ev: Event) => {
|
71 |
| - if (activeContent && hoverable) { |
| 76 | + const isTouch = ((ev as PointerEvent).pointerType ?? "mouse") == "touch"; |
| 77 | + if (isTouch && closeOnTouchDelay == -1) |
| 78 | + return; // keep touch devices open until tap outside |
| 79 | + if ((isTouch || activeContent) && hoverable) { |
72 | 80 | const elements = [referenceEl, floatingEl, ...triggerEls].filter(Boolean);
|
73 | 81 | // Add a delay before hiding the floating element to account for hoverable elements.
|
74 | 82 | // This ensures that the floating element does not hide immediately when the mouse
|
75 | 83 | // moves from the reference element to the floating element.
|
76 |
| - setTimeout(() => { |
77 |
| - if (ev.type === 'mouseleave' && !elements.some(hasHover)) { |
| 84 | + const closeDelay = isTouch ? closeOnTouchDelay : 100; |
| 85 | + clearTimeout(timer); |
| 86 | + timer = setTimeout(() => { |
| 87 | + if ((ev.type === 'mouseleave' || ev.type === 'pointerleave') && |
| 88 | + (isTouch || !elements.some(hasHover))) { |
78 | 89 | open = false;
|
79 | 90 | }
|
80 |
| - }, 100); |
| 91 | + }, closeDelay) as unknown as number; |
81 | 92 | } else {
|
82 | 93 | open = false;
|
83 | 94 | }
|
|
130 | 141 | ['focusin', showHandler, focusable],
|
131 | 142 | ['focusout', hideHandler, focusable],
|
132 | 143 | ['click', showHandler, clickable],
|
133 |
| - ['mouseenter', showHandler, hoverable], |
134 |
| - ['mouseleave', hideHandler, hoverable] |
| 144 | + ['pointerenter', showHandler, hoverable], |
| 145 | + ['pointerleave', hideHandler, hoverable] |
135 | 146 | ];
|
136 | 147 |
|
137 | 148 | if (triggeredBy) triggerEls = [...document.querySelectorAll<HTMLElement>(triggeredBy)];
|
|
152 | 163 | console.error(`Popup reference not found: '${reference}'`);
|
153 | 164 | } else {
|
154 | 165 | if (focusable) referenceEl.addEventListener('focusout', hideHandler);
|
155 |
| - if (hoverable) referenceEl.addEventListener('mouseleave', hideHandler); |
| 166 | + if (hoverable) referenceEl.addEventListener('pointerleave', hideHandler); |
156 | 167 | }
|
157 | 168 | } else {
|
158 | 169 | referenceEl = triggerEls[0];
|
159 | 170 | }
|
160 | 171 |
|
161 |
| - if (clickable) document.addEventListener('click', closeOnClickOutside); |
| 172 | + document.addEventListener('click', closeOnClickOutside); |
162 | 173 |
|
163 | 174 | return () => {
|
164 | 175 | // This is onDestroy function
|
|
170 | 181 |
|
171 | 182 | if (referenceEl) {
|
172 | 183 | referenceEl.removeEventListener('focusout', hideHandler);
|
173 |
| - referenceEl.removeEventListener('mouseleave', hideHandler); |
| 184 | + referenceEl.removeEventListener('pointerleave', hideHandler); |
174 | 185 | }
|
175 | 186 |
|
176 | 187 | document.removeEventListener('click', closeOnClickOutside);
|
|
184 | 195 | function closeOnClickOutside(event: MouseEvent) {
|
185 | 196 | if (open) {
|
186 | 197 | if (!event.composedPath().includes(floatingEl) && !triggerEls.some((el) => event.composedPath().includes(el))) {
|
187 |
| - hideHandler(event); |
| 198 | + open = false; |
188 | 199 | }
|
189 | 200 | }
|
190 | 201 | }
|
|
232 | 243 | @prop export let open: boolean = false;
|
233 | 244 | @prop export let yOnly: boolean = false;
|
234 | 245 | @prop export let middlewares: Middleware[] = [dom.flip(), dom.shift()];
|
| 246 | +@prop export let closeOnTouchDelay: number = 3000; |
235 | 247 | -->
|
0 commit comments