Svelte: QoL fixes for hover popovers (#62974)

- Adds a hoverDelay parameter to Popover so each consumer doesn't need to do this manually
- Makes hovers only show when the mouse comes to rest
- Closes the popover when the window loses focus so we don't miss a mouseleave event and have popovers get stuck open
- Adds a delay to popover closing when the mouse leaves one of the target elements so there is room for error when moving the mouse from the trigger to the popover
- Adds preloading back to the file tree because the popover now does not create the element until the delay has passed
- Makes it so clicking the target closes the popover when hover is enabled.
This commit is contained in:
Camden Cheek 2024-05-30 09:00:27 -06:00 committed by GitHub
parent 4e903e5a55
commit 819b2d0e68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 8 deletions

View File

@ -8,20 +8,28 @@
/**
* Show the popover when hovering over the trigger.
*/
export let showOnHover = false
export let showOnHover: boolean = false
export let hoverDelay: number = 500
export let hoverCloseDelay: number = 150
let isOpen = false
let trigger: HTMLElement | null
let target: HTMLElement | undefined
let popoverContainer: HTMLElement | null
let delayTimer: ReturnType<typeof setTimeout>
function toggle(open?: boolean): void {
isOpen = open === undefined ? !isOpen : open
}
function close(): void {
clearTimeout(delayTimer)
toggle(false)
}
function handleClickOutside(event: { detail: HTMLElement }): void {
if (event.detail !== trigger && !trigger?.contains(event.detail)) {
isOpen = false
if (!showOnHover && event.detail !== trigger && !trigger?.contains(event.detail)) {
toggle(false)
}
}
@ -33,19 +41,29 @@
trigger = node
function handleMouseEnterTrigger(): void {
isOpen = true
clearTimeout(delayTimer)
delayTimer = setTimeout(() => toggle(true), hoverDelay)
}
function handleMouseLeaveTrigger(event: MouseEvent): void {
// It should be possible to move the mouse from the trigger to the popover without closing it
if (event.relatedTarget && !popoverContainer?.contains(event.relatedTarget as Node)) {
isOpen = false
clearTimeout(delayTimer)
delayTimer = setTimeout(() => toggle(false), hoverCloseDelay)
}
}
function handleMouseMoveTrigger(): void {
clearTimeout(delayTimer)
delayTimer = setTimeout(() => toggle(true), hoverDelay)
}
if (showOnHover) {
node.addEventListener('mouseenter', handleMouseEnterTrigger)
node.addEventListener('mouseleave', handleMouseLeaveTrigger)
node.addEventListener('mousemove', handleMouseMoveTrigger)
node.addEventListener('click', close)
window.addEventListener('blur', close)
}
return {
@ -53,6 +71,9 @@
trigger = null
node.removeEventListener('mouseenter', handleMouseEnterTrigger)
node.removeEventListener('mouseleave', handleMouseLeaveTrigger)
node.removeEventListener('mousemove', handleMouseMoveTrigger)
node.removeEventListener('click', close)
window.removeEventListener('blur', close)
},
}
}
@ -62,15 +83,23 @@
function handleMouseLeavePopover(event: MouseEvent): void {
// It should be possible to move the mouse from the popover to the trigger without closing it
if (event.relatedTarget && !trigger?.contains(event.relatedTarget as Node)) {
toggle(false)
delayTimer = setTimeout(() => toggle(false), hoverCloseDelay)
}
}
function handleMouseEnterPopover(): void {
// When the mouse enters the popover, cancel any pending close events
clearTimeout(delayTimer)
}
if (showOnHover) {
node.addEventListener('mouseenter', handleMouseEnterPopover)
node.addEventListener('mouseleave', handleMouseLeavePopover)
}
return {
destroy() {
popoverContainer = null
node.removeEventListener('mouseenter', handleMouseEnterPopover)
node.removeEventListener('mouseleave', handleMouseLeavePopover)
},
}

View File

@ -12,7 +12,7 @@
import { getSidebarFileTreeStateForRepo } from '$lib/repo/stores'
import { replaceRevisionInURL } from '$lib/shared'
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
import { createForwardStore, delay } from '$lib/utils'
import { createForwardStore } from '$lib/utils'
import { Alert } from '$lib/wildcard'
export let repoName: string
@ -131,6 +131,8 @@
tabindex={-1}
data-go-up={isRoot ? true : undefined}
use:registerTrigger
on:mouseover={/* Preload */ () =>
fetchPopoverData({ repoName, revision, filePath: entry.path })}
>
{#if entry.isDirectory}
<Icon svgPath={getDirectoryIconPath(entry, expanded)} inline />
@ -140,7 +142,7 @@
{isRoot ? '..' : entry.name}
</a>
<svelte:fragment slot="content">
{#await delay(fetchPopoverData({ repoName, revision, filePath: entry.path }), 300) then entry}
{#await fetchPopoverData({ repoName, revision, filePath: entry.path }) then entry}
<FilePopover {repoName} {revision} {entry} />
{/await}
</svelte:fragment>