Skip to content

Commit 0d7447e

Browse files
shinokadamrh1997
authored andcommitted
Make Popper close automaticially after 3secs on touch devices
When triggering a Tooltip/Speeddial/Popover on a touch device it will close after 3secs (or "closeOnTouchDelay" ms) when set to trigger="hover". Furthermore Svelte 5 will now behave identical to Svelte 4 (instead of two touches a single one is required)
1 parent 6542fdb commit 0d7447e

File tree

3 files changed

+30
-16
lines changed

3 files changed

+30
-16
lines changed

src/lib/speed-dial/SpeedDial.svelte

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
setContext<SpeedCtxType>('speed-dial', { pill, tooltip, textOutside });
4747
4848
let divClass: string;
49-
$: divClass = twMerge(defaultClass, 'group', $$props.class);
49+
$: divClass = twMerge(defaultClass, $$props.class);
5050
5151
let poperClass: string;
5252
$: poperClass = twMerge(popperDefaultClass, ['top', 'bottom'].includes(placement.split('-')[0]) && 'flex-col');
@@ -59,7 +59,7 @@
5959
{#if gradient}
6060
<GradientButton {pill} {name} aria-controls={id} aria-expanded={open} {...$$restProps} class="!p-3">
6161
<slot name="icon">
62-
<svg aria-hidden="true" class="w-8 h-8 transition-transform group-hover:rotate-45" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
62+
<svg aria-hidden="true" class="w-8 h-8 transition-transform" class:rotate-45={open} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
6363
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
6464
</svg>
6565
</slot>
@@ -68,7 +68,7 @@
6868
{:else}
6969
<Button {pill} {name} aria-controls={id} aria-expanded={open} {...$$restProps} class="!p-3">
7070
<slot name="icon">
71-
<svg aria-hidden="true" class="w-8 h-8 transition-transform group-hover:rotate-45" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
71+
<svg aria-hidden="true" class="w-8 h-8 transition-transform" class:rotate-45={open} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
7272
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
7373
</svg>
7474
</slot>

src/lib/utils/Popper.svelte

+23-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
strategy?: 'absolute' | 'fixed';
1818
open?: boolean;
1919
yOnly?: boolean;
20+
closeOnTouchDelay?: number;
2021
}
2122
2223
export let activeContent: boolean = false;
@@ -29,6 +30,7 @@
2930
export let strategy: 'absolute' | 'fixed' = 'absolute';
3031
export let open: boolean = false;
3132
export let yOnly: boolean = false;
33+
export let closeOnTouchDelay: number = 3000;
3234
// extra floating UI middleware list
3335
export let middlewares: Middleware[] = [dom.flip(), dom.shift()];
3436
@@ -52,6 +54,7 @@
5254
let arrowEl: HTMLElement | null;
5355
let contentEl: HTMLElement;
5456
let triggerEls: HTMLElement[] = [];
57+
let timer: number|undefined = undefined;
5558
5659
const showHandler = (ev: Event) => {
5760
if (referenceEl === undefined) console.error('trigger undefined');
@@ -60,24 +63,32 @@
6063
if (open) return; // If the popper is already open after the reference element has changed
6164
}
6265
63-
open = ev.type === 'click' ? !open : true;
66+
setTimeout(() => {
67+
open = ev.type === 'click' ? !open : true;
68+
}, (ev as PointerEvent).pointerType === "touch" ? 300 : 0)
6469
};
6570
6671
const hasHover = (el: Element) => el.matches(':hover');
6772
const hasFocus = (el: Element) => el.contains(document.activeElement);
6873
const px = (n: number | undefined) => (n ? `${n}px` : '');
6974
7075
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) {
7280
const elements = [referenceEl, floatingEl, ...triggerEls].filter(Boolean);
7381
// Add a delay before hiding the floating element to account for hoverable elements.
7482
// This ensures that the floating element does not hide immediately when the mouse
7583
// 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))) {
7889
open = false;
7990
}
80-
}, 100);
91+
}, closeDelay) as unknown as number;
8192
} else {
8293
open = false;
8394
}
@@ -130,8 +141,8 @@
130141
['focusin', showHandler, focusable],
131142
['focusout', hideHandler, focusable],
132143
['click', showHandler, clickable],
133-
['mouseenter', showHandler, hoverable],
134-
['mouseleave', hideHandler, hoverable]
144+
['pointerenter', showHandler, hoverable],
145+
['pointerleave', hideHandler, hoverable]
135146
];
136147
137148
if (triggeredBy) triggerEls = [...document.querySelectorAll<HTMLElement>(triggeredBy)];
@@ -152,13 +163,13 @@
152163
console.error(`Popup reference not found: '${reference}'`);
153164
} else {
154165
if (focusable) referenceEl.addEventListener('focusout', hideHandler);
155-
if (hoverable) referenceEl.addEventListener('mouseleave', hideHandler);
166+
if (hoverable) referenceEl.addEventListener('pointerleave', hideHandler);
156167
}
157168
} else {
158169
referenceEl = triggerEls[0];
159170
}
160171
161-
if (clickable) document.addEventListener('click', closeOnClickOutside);
172+
document.addEventListener('click', closeOnClickOutside);
162173
163174
return () => {
164175
// This is onDestroy function
@@ -170,7 +181,7 @@
170181
171182
if (referenceEl) {
172183
referenceEl.removeEventListener('focusout', hideHandler);
173-
referenceEl.removeEventListener('mouseleave', hideHandler);
184+
referenceEl.removeEventListener('pointerleave', hideHandler);
174185
}
175186
176187
document.removeEventListener('click', closeOnClickOutside);
@@ -184,7 +195,7 @@
184195
function closeOnClickOutside(event: MouseEvent) {
185196
if (open) {
186197
if (!event.composedPath().includes(floatingEl) && !triggerEls.some((el) => event.composedPath().includes(el))) {
187-
hideHandler(event);
198+
open = false;
188199
}
189200
}
190201
}
@@ -232,4 +243,5 @@
232243
@prop export let open: boolean = false;
233244
@prop export let yOnly: boolean = false;
234245
@prop export let middlewares: Middleware[] = [dom.flip(), dom.shift()];
246+
@prop export let closeOnTouchDelay: number = 3000;
235247
-->

src/routes/docs/components/tooltip.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,14 @@ The positioning of the tooltip element relative to the triggering element (eg. b
104104
```svelte example class="flex items-end gap-2 h-32" hideResponsiveButtons
105105
<script>
106106
import { Tooltip, Button } from 'flowbite-svelte';
107+
let actions = "";
107108
</script>
108109
109-
<Button id="hover">Tooltip hover</Button>
110-
<Button id="click">Tooltip click</Button>
110+
<Button id="hover" on:click={() => (actions = "Clicked Hover-Tooltip Button\n" + actions)}>Tooltip hover</Button>
111+
<Button id="click" on:click={() => (actions = "Clicked Click-Tooltip Button\n" + actions)}>Tooltip click</Button>
111112
<Tooltip triggeredBy="#hover">Hover tooltip content</Tooltip>
112113
<Tooltip trigger="click" triggeredBy="#click">Click tooltip content</Tooltip>
114+
<pre style="height: 4rem; overflow:hidden;">{actions}</pre>
113115
```
114116

115117
## Disable arrow

0 commit comments

Comments
 (0)