This commit is contained in:
Guilhem
2025-04-07 08:52:34 +01:00
parent edd9eda156
commit 85434654af
3 changed files with 140 additions and 21 deletions
+78 -3
View File
@@ -4,6 +4,28 @@
id: null,
close: null
})
/**
* Creates a debounced function that delays invoking func until after wait milliseconds
* have elapsed since the last time the debounced function was invoked.
*/
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null
return function (...args: Parameters<T>): void {
if (timeoutId !== null) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
func(...args)
timeoutId = null
}, wait)
}
}
</script>
<script lang="ts">
@@ -16,7 +38,7 @@
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
export let disabled = false
@@ -24,6 +46,8 @@
export let usePointerDownOutside = false
export let closeOnOtherDropdownOpen = true
export let fixedHeight = true
export let openOnHover = false
export let hoverDelay = 200 // Delay for hover effects in milliseconds
const dispatch = createEventDispatcher()
@@ -38,6 +62,7 @@
loop: true,
onOpenChange: ({ curr, next }) => {
if (curr !== next) {
openByHover = false
dispatch('openChange', next)
}
if (closeOnOtherDropdownOpen) {
@@ -53,10 +78,16 @@
}
}
return next
}
},
forceVisible: true
})
let open = false
let hovering = false
let openByHover = false
let hoverTimer: ReturnType<typeof setTimeout> | null = null
let leaveTimer: ReturnType<typeof setTimeout> | null = null
const sync = createSync(states)
$: sync.open(open, (v) => (open = Boolean(v)))
@@ -75,6 +106,29 @@
async function getMenuElements(): Promise<HTMLElement[]> {
return Array.from(document.querySelectorAll('[data-menu]')) as HTMLElement[]
}
const setHovering = (value: boolean) => {
hovering = value
}
const debouncedHoverStart = debounce(() => setHovering(true), hoverDelay)
const debouncedHoverEnd = debounce(() => setHovering(false), hoverDelay)
// Clear any pending timers on component destruction
onDestroy(() => {
if (hoverTimer) clearTimeout(hoverTimer)
if (leaveTimer) clearTimeout(leaveTimer)
})
$: if (openOnHover && hovering && !open) {
open = true
openByHover = true
} else {
if (openByHover && !hovering) {
open = false
openByHover = false
}
}
</script>
<ResolveOpen {open} on:open on:close />
@@ -95,6 +149,14 @@
close()
}
}}
on:mouseenter={() => {
if (leaveTimer) clearTimeout(leaveTimer)
debouncedHoverStart()
}}
on:mouseleave={() => {
if (hoverTimer) clearTimeout(hoverTimer)
debouncedHoverEnd()
}}
data-menu
>
{#if $$slots.buttonReplacement}
@@ -111,7 +173,20 @@
</button>
{#if open}
<div use:melt={$menu} data-menu class="z-[6000]">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
use:melt={$menu}
data-menu
class="z-[6000]"
on:mouseenter={() => {
if (leaveTimer) clearTimeout(leaveTimer)
debouncedHoverStart()
}}
on:mouseleave={() => {
if (hoverTimer) clearTimeout(hoverTimer)
debouncedHoverEnd()
}}
>
<div
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
>
+27 -17
View File
@@ -1261,6 +1261,9 @@
let flowPreviewButtons: FlowPreviewButtons
let isDropdownOpen = false
let tooltipOpen = false
$: tooltipOpen && msgInput && msgInput.focus()
$: console.log('dbg isDropdownOpen', isDropdownOpen)
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -1469,22 +1472,29 @@
Draft
</Button>
<CustomPopover appearTimeout={0} focusEl={msgInput} disablePopup={isDropdownOpen}>
<Button
disabled={loading}
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={async () => {
await handleSaveFlow()
}}
dropdownItems={!newFlow ? dropdownItems : undefined}
on:openChange={({ detail }) => (isDropdownOpen = detail)}
>
Deploy
</Button>
<svelte:fragment slot="overlay">
<div class="flex flex-row gap-2 w-80">
<Button
disabled={loading}
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={async () => {
await handleSaveFlow()
}}
dropdownItems={!newFlow ? dropdownItems : undefined}
on:open={() => {
isDropdownOpen = true
}}
on:close={() => {
isDropdownOpen = false
}}
on:tooltip-open={async ({ detail }) => {
tooltipOpen = true
}}
dropdownOpenOnHover
>
Deploy
<svelte:fragment slot="tooltip">
<div class="flex flex-row gap-2 w-80 bg-surface rounded-md p-4 shadow-md">
<input
type="text"
placeholder="Deployment message"
@@ -1506,7 +1516,7 @@
</Button>
</div>
</svelte:fragment>
</CustomPopover>
</Button>
</div>
</div>
@@ -5,6 +5,7 @@
import Dropdown from '$lib/components/DropdownV2.svelte'
import { getModifierKey, type Item } from '$lib/utils'
import { Loader2, ChevronDown } from 'lucide-svelte'
import { createTooltip, melt } from '@melt-ui/svelte'
export let size: ButtonType.Size = 'md'
export let spacingSize: ButtonType.Size = size
@@ -34,6 +35,8 @@
export let shortCut:
| { key?: string; hide?: boolean; Icon?: any; withoutModifier?: boolean }
| undefined = undefined
export let dropdownOpen: boolean = false
export let dropdownOpenOnHover: boolean = false
type MenuItem = {
label: string
@@ -117,6 +120,24 @@
}
$: lucideIconSize = (iconMap[size] ?? 12) * 1
const {
elements: { trigger, content }
} = createTooltip({
positioning: {
placement: 'bottom-end'
},
openDelay: 0,
closeDelay: 0,
group: true,
portal: 'body',
onOpenChange: ({ curr, next }) => {
if (curr != next) {
dispatch('tooltip-open', next)
}
return next
}
})
</script>
<div
@@ -200,6 +221,7 @@
{...$$restProps}
disabled={disabled || (loading && !clickableWhileLoading)}
{style}
use:melt={$trigger}
>
{#if loading}
<Loader2 class={twMerge('animate-spin', iconOnlyPadding[size])} size={lucideIconSize} />
@@ -232,8 +254,20 @@
</button>
{/if}
<div use:melt={$content} class="z-[20000]">
<slot name="tooltip" />
</div>
{#if dropdownItems && dropdownItems.length > 0}
<Dropdown items={computeDropdowns(dropdownItems)} class="h-auto w-fit" on:openChange>
<Dropdown
items={computeDropdowns(dropdownItems)}
class="h-auto w-fit"
on:openChange
on:open
on:close
bind:open={dropdownOpen}
openOnHover={dropdownOpenOnHover}
>
<svelte:fragment slot="buttonReplacement">
<div
class={twMerge(