diff --git a/src/lib/components/common/DropdownSub.svelte b/src/lib/components/common/DropdownSub.svelte
index 8e4b35d717..daa15766d0 100644
--- a/src/lib/components/common/DropdownSub.svelte
+++ b/src/lib/components/common/DropdownSub.svelte
@@ -14,8 +14,34 @@
export let sideOffset = 8;
let open = false;
- let triggerEl;
- let contentEl;
+ let triggerEl: HTMLElement | null = null;
+ 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() {
if (!triggerEl || !contentEl) return;
@@ -29,7 +55,7 @@
contentEl.style.paddingRight = '0';
// 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;
if (parentContainer && innerContent) {
const parentWidth = parentContainer.offsetWidth;
@@ -71,28 +97,48 @@
contentEl.style.top = `${top}px`;
}
- async function handleMouseEnter() {
+ async function openSub(focus = false) {
open = true;
await tick();
positionContent();
// Re-position after transition starts rendering real dimensions
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)
- if (contentEl?.contains(event.relatedTarget)) return;
- if (triggerEl?.contains(event.relatedTarget)) return;
+ if (relatedTarget && contentEl?.contains(relatedTarget)) return;
+ if (relatedTarget && triggerEl?.contains(relatedTarget)) return;
open = false;
}
- function handleContentMouseLeave(event) {
- if (triggerEl?.contains(event.relatedTarget)) return;
- if (contentEl?.contains(event.relatedTarget)) return;
+ function handleContentMouseLeave(event: MouseEvent) {
+ const relatedTarget = event.relatedTarget as Node | null;
+
+ if (relatedTarget && triggerEl?.contains(relatedTarget)) return;
+ if (relatedTarget && contentEl?.contains(relatedTarget)) return;
open = false;
}
- function portal(node) {
+ function portal(node: HTMLElement) {
document.body.appendChild(node);
return {
destroy() {
@@ -107,19 +153,21 @@