refac
This commit is contained in:
1 parent
0671b7aa2b
commit
7bfc4bb2c2
1 file changed
+66
-18
@@ -14,8 +14,34 @@
|
|||||||
export let sideOffset = 8;
|
export let sideOffset = 8;
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
let triggerEl;
|
let triggerEl: HTMLElement | null = null;
|
||||||
let contentEl;
|
let contentEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
function trigger(node: HTMLElement) {
|
||||||
|
triggerEl = (node.firstElementChild as HTMLElement | null) || node;
|
||||||
|
|
||||||
|
async function handleClick(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
await openSub(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (!['Enter', ' ', 'ArrowRight'].includes(event.key)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
await openSub(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener('click', handleClick);
|
||||||
|
node.addEventListener('keydown', handleKeydown);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener('click', handleClick);
|
||||||
|
node.removeEventListener('keydown', handleKeydown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function positionContent() {
|
function positionContent() {
|
||||||
if (!triggerEl || !contentEl) return;
|
if (!triggerEl || !contentEl) return;
|
||||||
@@ -29,7 +55,7 @@
|
|||||||
contentEl.style.paddingRight = '0';
|
contentEl.style.paddingRight = '0';
|
||||||
|
|
||||||
// Inherit min-width from parent dropdown container (apply to inner content)
|
// Inherit min-width from parent dropdown container (apply to inner content)
|
||||||
const innerContent = contentEl.firstElementChild;
|
const innerContent = contentEl.firstElementChild as HTMLElement | null;
|
||||||
const parentContainer = triggerEl.closest('[class*="rounded"]')?.parentElement;
|
const parentContainer = triggerEl.closest('[class*="rounded"]')?.parentElement;
|
||||||
if (parentContainer && innerContent) {
|
if (parentContainer && innerContent) {
|
||||||
const parentWidth = parentContainer.offsetWidth;
|
const parentWidth = parentContainer.offsetWidth;
|
||||||
@@ -71,28 +97,48 @@
|
|||||||
contentEl.style.top = `${top}px`;
|
contentEl.style.top = `${top}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMouseEnter() {
|
async function openSub(focus = false) {
|
||||||
open = true;
|
open = true;
|
||||||
await tick();
|
await tick();
|
||||||
positionContent();
|
positionContent();
|
||||||
// Re-position after transition starts rendering real dimensions
|
// Re-position after transition starts rendering real dimensions
|
||||||
setTimeout(positionContent, 50);
|
setTimeout(positionContent, 50);
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
contentEl?.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseLeave(event) {
|
async function handleMouseEnter() {
|
||||||
|
await openSub();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContentKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== 'Escape') return;
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
open = false;
|
||||||
|
triggerEl?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave(event: MouseEvent) {
|
||||||
|
const relatedTarget = event.relatedTarget as Node | null;
|
||||||
|
|
||||||
// Don't close if moving to the sub-content (including its bridge padding)
|
// Don't close if moving to the sub-content (including its bridge padding)
|
||||||
if (contentEl?.contains(event.relatedTarget)) return;
|
if (relatedTarget && contentEl?.contains(relatedTarget)) return;
|
||||||
if (triggerEl?.contains(event.relatedTarget)) return;
|
if (relatedTarget && triggerEl?.contains(relatedTarget)) return;
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContentMouseLeave(event) {
|
function handleContentMouseLeave(event: MouseEvent) {
|
||||||
if (triggerEl?.contains(event.relatedTarget)) return;
|
const relatedTarget = event.relatedTarget as Node | null;
|
||||||
if (contentEl?.contains(event.relatedTarget)) return;
|
|
||||||
|
if (relatedTarget && triggerEl?.contains(relatedTarget)) return;
|
||||||
|
if (relatedTarget && contentEl?.contains(relatedTarget)) return;
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function portal(node) {
|
function portal(node: HTMLElement) {
|
||||||
document.body.appendChild(node);
|
document.body.appendChild(node);
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -107,19 +153,21 @@
|
|||||||
<svelte:window on:scroll|capture={positionContent} on:resize={positionContent} />
|
<svelte:window on:scroll|capture={positionContent} on:resize={positionContent} />
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div use:trigger class="w-full" on:mouseenter={handleMouseEnter} on:mouseleave={handleMouseLeave}>
|
||||||
bind:this={triggerEl}
|
|
||||||
class="w-full"
|
|
||||||
on:mouseenter={handleMouseEnter}
|
|
||||||
on:mouseleave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- Outer wrapper: positioned flush with trigger, invisible padding bridges the gap -->
|
<!-- Outer wrapper: positioned flush with trigger, invisible padding bridges the gap -->
|
||||||
<div use:portal bind:this={contentEl} on:mouseleave={handleContentMouseLeave}>
|
<div
|
||||||
|
use:portal
|
||||||
|
bind:this={contentEl}
|
||||||
|
role="menu"
|
||||||
|
tabindex="-1"
|
||||||
|
on:mouseleave={handleContentMouseLeave}
|
||||||
|
on:keydown={handleContentKeydown}
|
||||||
|
>
|
||||||
<!-- Inner content: visual styles and transition -->
|
<!-- Inner content: visual styles and transition -->
|
||||||
<div transition:flyAndScale>
|
<div transition:flyAndScale>
|
||||||
<DropdownMenu className={contentClass} style="max-width: {maxWidth}px;">
|
<DropdownMenu className={contentClass} style="max-width: {maxWidth}px;">
|
||||||
|
|||||||
Reference in new issue
Block a user