Skip to content

Commit

Permalink
Make Popper close automaticially after 3secs on touch devices
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
shinokada authored and mrh1997 committed Nov 6, 2024
1 parent 6542fdb commit 0d7447e
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 16 deletions.
6 changes: 3 additions & 3 deletions src/lib/speed-dial/SpeedDial.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
setContext<SpeedCtxType>('speed-dial', { pill, tooltip, textOutside });
let divClass: string;
$: divClass = twMerge(defaultClass, 'group', $$props.class);
$: divClass = twMerge(defaultClass, $$props.class);
let poperClass: string;
$: poperClass = twMerge(popperDefaultClass, ['top', 'bottom'].includes(placement.split('-')[0]) && 'flex-col');
Expand All @@ -59,7 +59,7 @@
{#if gradient}
<GradientButton {pill} {name} aria-controls={id} aria-expanded={open} {...$$restProps} class="!p-3">
<slot name="icon">
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</slot>
Expand All @@ -68,7 +68,7 @@
{:else}
<Button {pill} {name} aria-controls={id} aria-expanded={open} {...$$restProps} class="!p-3">
<slot name="icon">
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</slot>
Expand Down
34 changes: 23 additions & 11 deletions src/lib/utils/Popper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
strategy?: 'absolute' | 'fixed';
open?: boolean;
yOnly?: boolean;
closeOnTouchDelay?: number;
}
export let activeContent: boolean = false;
Expand All @@ -29,6 +30,7 @@
export let strategy: 'absolute' | 'fixed' = 'absolute';
export let open: boolean = false;
export let yOnly: boolean = false;
export let closeOnTouchDelay: number = 3000;
// extra floating UI middleware list
export let middlewares: Middleware[] = [dom.flip(), dom.shift()];
Expand All @@ -52,6 +54,7 @@
let arrowEl: HTMLElement | null;
let contentEl: HTMLElement;
let triggerEls: HTMLElement[] = [];
let timer: number|undefined = undefined;
const showHandler = (ev: Event) => {
if (referenceEl === undefined) console.error('trigger undefined');
Expand All @@ -60,24 +63,32 @@
if (open) return; // If the popper is already open after the reference element has changed
}
open = ev.type === 'click' ? !open : true;
setTimeout(() => {
open = ev.type === 'click' ? !open : true;
}, (ev as PointerEvent).pointerType === "touch" ? 300 : 0)
};
const hasHover = (el: Element) => el.matches(':hover');
const hasFocus = (el: Element) => el.contains(document.activeElement);
const px = (n: number | undefined) => (n ? `${n}px` : '');
const hideHandler = (ev: Event) => {
if (activeContent && hoverable) {
const isTouch = ((ev as PointerEvent).pointerType ?? "mouse") == "touch";
if (isTouch && closeOnTouchDelay == -1)
return; // keep touch devices open until tap outside
if ((isTouch || activeContent) && hoverable) {
const elements = [referenceEl, floatingEl, ...triggerEls].filter(Boolean);
// Add a delay before hiding the floating element to account for hoverable elements.
// This ensures that the floating element does not hide immediately when the mouse
// moves from the reference element to the floating element.
setTimeout(() => {
if (ev.type === 'mouseleave' && !elements.some(hasHover)) {
const closeDelay = isTouch ? closeOnTouchDelay : 100;
clearTimeout(timer);
timer = setTimeout(() => {
if ((ev.type === 'mouseleave' || ev.type === 'pointerleave') &&
(isTouch || !elements.some(hasHover))) {
open = false;
}
}, 100);
}, closeDelay) as unknown as number;
} else {
open = false;
}
Expand Down Expand Up @@ -130,8 +141,8 @@
['focusin', showHandler, focusable],
['focusout', hideHandler, focusable],
['click', showHandler, clickable],
['mouseenter', showHandler, hoverable],
['mouseleave', hideHandler, hoverable]
['pointerenter', showHandler, hoverable],
['pointerleave', hideHandler, hoverable]
];
if (triggeredBy) triggerEls = [...document.querySelectorAll<HTMLElement>(triggeredBy)];
Expand All @@ -152,13 +163,13 @@
console.error(`Popup reference not found: '${reference}'`);
} else {
if (focusable) referenceEl.addEventListener('focusout', hideHandler);
if (hoverable) referenceEl.addEventListener('mouseleave', hideHandler);
if (hoverable) referenceEl.addEventListener('pointerleave', hideHandler);
}
} else {
referenceEl = triggerEls[0];
}
if (clickable) document.addEventListener('click', closeOnClickOutside);
document.addEventListener('click', closeOnClickOutside);
return () => {
// This is onDestroy function
Expand All @@ -170,7 +181,7 @@
if (referenceEl) {
referenceEl.removeEventListener('focusout', hideHandler);
referenceEl.removeEventListener('mouseleave', hideHandler);
referenceEl.removeEventListener('pointerleave', hideHandler);
}
document.removeEventListener('click', closeOnClickOutside);
Expand All @@ -184,7 +195,7 @@
function closeOnClickOutside(event: MouseEvent) {
if (open) {
if (!event.composedPath().includes(floatingEl) && !triggerEls.some((el) => event.composedPath().includes(el))) {
hideHandler(event);
open = false;
}
}
}
Expand Down Expand Up @@ -232,4 +243,5 @@
@prop export let open: boolean = false;
@prop export let yOnly: boolean = false;
@prop export let middlewares: Middleware[] = [dom.flip(), dom.shift()];
@prop export let closeOnTouchDelay: number = 3000;
-->
6 changes: 4 additions & 2 deletions src/routes/docs/components/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ The positioning of the tooltip element relative to the triggering element (eg. b
```svelte example class="flex items-end gap-2 h-32" hideResponsiveButtons
<script>
import { Tooltip, Button } from 'flowbite-svelte';
let actions = "";
</script>
<Button id="hover">Tooltip hover</Button>
<Button id="click">Tooltip click</Button>
<Button id="hover" on:click={() => (actions = "Clicked Hover-Tooltip Button\n" + actions)}>Tooltip hover</Button>
<Button id="click" on:click={() => (actions = "Clicked Click-Tooltip Button\n" + actions)}>Tooltip click</Button>
<Tooltip triggeredBy="#hover">Hover tooltip content</Tooltip>
<Tooltip trigger="click" triggeredBy="#click">Click tooltip content</Tooltip>
<pre style="height: 4rem; overflow:hidden;">{actions}</pre>
```

## Disable arrow
Expand Down

0 comments on commit 0d7447e

Please sign in to comment.