diff --git a/dist/chunks/chunk.JZGJWX35.js b/dist/chunks/chunk.JZGJWX35.js index 593bac0b7708fc0e908f9f8de650e45f8b3610ec..66e09365025b0e593e38d8410c8019d660081e16 100644 --- a/dist/chunks/chunk.JZGJWX35.js +++ b/dist/chunks/chunk.JZGJWX35.js @@ -29,6 +29,7 @@ var WaDropdownItem = class extends WebAwesomeElement { constructor() { super(...arguments); this.hasSlotController = new HasSlotController(this, "[default]", "start", "end"); + this.submenuGeneration = 0; this.active = false; this.variant = "default"; this.size = "m"; @@ -40,7 +41,10 @@ var WaDropdownItem = class extends WebAwesomeElement { this.submenuOpen = false; this.hasSubmenu = false; this.handleSlotChange = () => { - this.hasSubmenu = this.hasSlotController.test("submenu"); + const hasSubmenu = this.hasSlotController.test("submenu"); + // Retire the branch before losing the old submenu state and popup. + if (!hasSubmenu) void this.closeSubmenu(); + this.hasSubmenu = hasSubmenu; this.updateHasSubmenuState(); if (this.hasSubmenu) { this.setAttribute("aria-haspopup", "menu"); @@ -67,7 +71,6 @@ var WaDropdownItem = class extends WebAwesomeElement { /** Handles pointer enter to open the submenu on hover */ this.handlePointerEnter = (event) => { if (event.pointerType === "mouse" && this.hasSubmenu && !this.disabled) { - this.notifyParentOfOpening(); this.submenuOpen = true; } }; @@ -84,7 +87,15 @@ var WaDropdownItem = class extends WebAwesomeElement { } disconnectedCallback() { super.disconnectedCallback(); - this.closeSubmenu(); + this.submenuGeneration++; + void this.closeSubmenu(); + const submenu = this.submenuElement; + if (submenu) { + submenu.classList.remove("show", "hide"); + submenu.hidden = true; + submenu.removeAttribute("data-visible"); + submenu.hidePopover?.(); + } this.removeEventListener?.("click", this.handleHostClick); this.removeEventListener?.("pointerenter", this.handlePointerEnter); this.shadowRoot?.removeEventListener?.("click", this.handleClick, { capture: true }); @@ -127,11 +138,8 @@ var WaDropdownItem = class extends WebAwesomeElement { } if (changedProperties.has("submenuOpen")) { this.customStates.set("submenu-open", this.submenuOpen); - if (this.submenuOpen) { - this.openSubmenu(); - } else { - this.closeSubmenu(); - } + const previousAnimation = this.submenuAnimation; + this.submenuAnimation = this.updateSubmenuVisibility(previousAnimation); } } /** Update the has-submenu custom state */ @@ -140,22 +148,51 @@ var WaDropdownItem = class extends WebAwesomeElement { } /** Opens the submenu. */ async openSubmenu() { - const submenu = this.submenuElement; - if (!this.hasSubmenu || !submenu || !this.isConnected) return; - this.notifyParentOfOpening(); - submenu.showPopover?.(); - submenu.hidden = false; - submenu.setAttribute("data-visible", ""); + if (!this.hasSubmenu || !this.submenuElement || !this.isConnected) return; this.submenuOpen = true; - this.setAttribute("aria-expanded", "true"); - await animateWithClass(submenu, "show"); - setTimeout(() => { + await this.updateComplete; + return this.submenuAnimation; + } + /** Reconciles property and public-method requests through one submenu lifecycle. */ + async updateSubmenuVisibility(previousAnimation) { + const submenu = this.submenuElement; + if (!this.submenuOpen) this.dispatchEvent(new Event("submenu-closing")); + if (!this.hasSubmenu || !submenu || !this.isConnected) { + await previousAnimation; + return; + } + const open = this.submenuOpen; + const generation = ++this.submenuGeneration; + const isCurrent = () => generation === this.submenuGeneration && this.isConnected && this.submenuOpen === open; + // Retire the previous class before joining its cleanup. A stale close must never + // hide a reopened native popover, and repeated public requests share this promise. + submenu.classList.remove("show", "hide"); + if (open) { + this.notifyParentOfOpening(); + if (!isCurrent()) return; + submenu.showPopover?.(); + submenu.hidden = false; + submenu.setAttribute("data-visible", ""); + } + // Opening observers initialize embedded controls before the expanded state changes. + this.setAttribute("aria-expanded", String(open)); + await previousAnimation; + await this.updateComplete; + if (!isCurrent()) return; + // Focus once when opening becomes usable, not again when the animation finishes: + // the operator may already have moved focus or returned to the parent by then. + if (open) { const items = this.getSubmenuItems(); - if (items.length > 0) { - items.forEach((item, index) => item.active = index === 0); - items[0].focus({ preventScroll: true }); - } - }, 0); + items.forEach((item, index) => item.active = index === 0); + items[0]?.focus({ preventScroll: true }); + } + await animateWithClass(submenu, open ? "show" : "hide"); + if (!isCurrent()) return; + if (!open) { + submenu.hidden = true; + submenu.removeAttribute("data-visible"); + submenu.hidePopover?.(); + } } /** Notifies the parent dropdown that this item is opening its submenu */ notifyParentOfOpening() { @@ -165,30 +202,15 @@ var WaDropdownItem = class extends WebAwesomeElement { detail: { item: this } }); this.dispatchEvent(event); - const parent = this.parentElement; - if (parent) { - const siblings = [...parent.children].filter( - (el) => el !== this && el.localName === "wa-dropdown-item" && el.getAttribute("slot") === this.getAttribute("slot") && el.submenuOpen - ); - siblings.forEach((sibling) => { - sibling.submenuOpen = false; - }); - } } /** Closes the submenu. */ async closeSubmenu() { - const submenu = this.submenuElement; - if (!this.hasSubmenu || !submenu) return; + // A never-connected item's first update cannot settle; closing it is a public no-op. + if (!this.hasSubmenu || !this.submenuElement) return; this.submenuOpen = false; - this.setAttribute("aria-expanded", "false"); - if (!submenu.hidden) { - await animateWithClass(submenu, "hide"); - if (submenu?.isConnected) { - submenu.hidden = true; - submenu.removeAttribute("data-visible"); - submenu.hidePopover?.(); - } - } + this.dispatchEvent(new Event("submenu-closing")); + await this.updateComplete; + return this.submenuAnimation; } /** Determines whether the item navigates when selected. Items with submenus never navigate. */ isLink() { @@ -218,8 +240,9 @@ var WaDropdownItem = class extends WebAwesomeElement { } /** Gets all dropdown items in the submenu. */ getSubmenuItems() { - return [...this.children].filter( - (el) => el.localName === "wa-dropdown-item" && el.getAttribute("slot") === "submenu" && !el.hasAttribute("disabled") + const slot = this.submenuElement?.querySelector('slot[name="submenu"]'); + return (slot?.assignedElements({ flatten: true }) ?? []).filter( + (el) => el.localName === "wa-dropdown-item" && !el.disabled ); } render() { @@ -278,7 +301,7 @@ var WaDropdownItem = class extends WebAwesomeElement { > - ` : ""} + ` : html``} `; } }; diff --git a/dist/chunks/chunk.KZJTFPDQ.js b/dist/chunks/chunk.KZJTFPDQ.js index 11de1ac9d650dc1e2db6922f8fb6cd217bc13a4d..7c076a5462bf77d2e3dc1f73335eaddbbb6054b9 100644 --- a/dist/chunks/chunk.KZJTFPDQ.js +++ b/dist/chunks/chunk.KZJTFPDQ.js @@ -58,17 +58,16 @@ var openDropdowns = /* @__PURE__ */ new Set(); var WaDropdown = class extends WebAwesomeElement { constructor() { super(...arguments); - this.submenuCleanups = /* @__PURE__ */ new Map(); this.localize = new LocalizeController(this); this.userTypedQuery = ""; - this.openSubmenuStack = []; + this.activeSubmenus = []; this.open = false; this.size = "m"; this.placement = "bottom-start"; this.distance = 0; this.skidding = 0; /** Handles key down events when the menu is open */ - this.handleDocumentKeyDown = async (event) => { + this.handleDocumentKeyDown = (event) => { const isRtl = this.localize.dir() === "rtl"; if (event.key === "Escape" && this.open && isTopDismissible(this)) { const trigger = this.getTrigger(); @@ -78,22 +77,11 @@ var WaDropdown = class extends WebAwesomeElement { trigger?.focus({ preventScroll: true }); return; } - const activeElement = [...activeElements()].find((el) => el.localName === "wa-dropdown-item"); - const isFocusedOnItem = activeElement?.localName === "wa-dropdown-item"; + const focusedItem = [...activeElements()].find((el) => el.localName === "wa-dropdown-item"); const currentSubmenuItem = this.getCurrentSubmenuItem(); - const isInSubmenu = !!currentSubmenuItem; - let items; - let activeItem; - let activeItemIndex; - if (isInSubmenu) { - items = this.getSubmenuItems(currentSubmenuItem); - activeItem = items.find((item) => item.active || item === activeElement); - activeItemIndex = activeItem ? items.indexOf(activeItem) : -1; - } else { - items = this.getItems(); - activeItem = items.find((item) => item.active || item === activeElement); - activeItemIndex = activeItem ? items.indexOf(activeItem) : -1; - } + const items = currentSubmenuItem ? this.getSubmenuItems(currentSubmenuItem) : this.getItems(); + const activeItem = items.find((item) => item === focusedItem) ?? items.find((item) => item.active); + const activeItemIndex = activeItem ? items.indexOf(activeItem) : -1; let itemToSelect; if (event.key === "ArrowUp") { event.preventDefault(); @@ -113,39 +101,23 @@ var WaDropdown = class extends WebAwesomeElement { itemToSelect = items[0]; } } - if (event.key === (isRtl ? "ArrowLeft" : "ArrowRight") && isFocusedOnItem && activeItem) { + if (event.key === (isRtl ? "ArrowLeft" : "ArrowRight") && focusedItem && activeItem) { if (activeItem.hasSubmenu) { event.preventDefault(); event.stopPropagation(); activeItem.submenuOpen = true; - this.addToSubmenuStack(activeItem); - setTimeout(() => { - const submenuItems = this.getSubmenuItems(activeItem); - if (submenuItems.length > 0) { - submenuItems.forEach((item, index) => item.active = index === 0); - submenuItems[0].focus({ preventScroll: true }); - } - }, 0); return; } } - if (event.key === (isRtl ? "ArrowRight" : "ArrowLeft") && isInSubmenu) { + if (event.key === (isRtl ? "ArrowRight" : "ArrowLeft") && currentSubmenuItem) { event.preventDefault(); event.stopPropagation(); - const removedItem = this.removeFromSubmenuStack(); - if (removedItem) { - removedItem.submenuOpen = false; - setTimeout(() => { - removedItem.focus({ preventScroll: true }); - removedItem.active = true; - const parentItems = removedItem.slot === "submenu" ? this.getSubmenuItems(removedItem.parentElement) : this.getItems(); - parentItems.forEach((item) => { - if (item !== removedItem) { - item.active = false; - } - }); - }, 0); - } + // Closing retires ownership synchronously; only this keypress restores focus. + void currentSubmenuItem.closeSubmenu(); + currentSubmenuItem.focus({ preventScroll: true }); + const parent = this.getCurrentSubmenuItem(); + const parentItems = parent ? this.getSubmenuItems(parent, true) : this.getItems(true); + parentItems.forEach((item) => item.active = item === currentSubmenuItem); return; } if (event.key === "Home" || event.key === "End") { @@ -154,7 +126,7 @@ var WaDropdown = class extends WebAwesomeElement { itemToSelect = event.key === "Home" ? items[0] : items[items.length - 1]; } if (event.key === "Tab") { - await this.hideMenu(); + this.open = false; } if (event.key.length === 1 && !(event.metaKey || event.ctrlKey || event.altKey) && !(event.key === " " && this.userTypedQuery === "")) { clearTimeout(this.userTypedTimeout); @@ -180,19 +152,11 @@ var WaDropdown = class extends WebAwesomeElement { itemToSelect.scrollIntoView({ block: "nearest" }); return; } - if ((event.key === "Enter" || event.key === " " && this.userTypedQuery === "") && isFocusedOnItem && activeItem) { + if ((event.key === "Enter" || event.key === " " && this.userTypedQuery === "") && focusedItem && activeItem) { event.preventDefault(); event.stopPropagation(); if (activeItem.hasSubmenu) { activeItem.submenuOpen = true; - this.addToSubmenuStack(activeItem); - setTimeout(() => { - const submenuItems = this.getSubmenuItems(activeItem); - if (submenuItems.length > 0) { - submenuItems.forEach((item, index) => item.active = index === 0); - submenuItems[0].focus({ preventScroll: true }); - } - }, 0); } else { this.makeSelection(activeItem, event); } @@ -213,7 +177,8 @@ var WaDropdown = class extends WebAwesomeElement { }; /** Handle global mouse movement for safe triangle logic */ this.handleGlobalMouseMove = (event) => { - const currentSubmenuItem = this.getCurrentSubmenuItem(); + const branch = this.activeSubmenus.at(-1); + const currentSubmenuItem = branch?.item; if (!currentSubmenuItem?.submenuOpen || !currentSubmenuItem.submenuElement) return; const submenuRect = currentSubmenuItem.submenuElement.getBoundingClientRect(); const isRtl = this.localize.dir() === "rtl"; @@ -231,7 +196,7 @@ var WaDropdown = class extends WebAwesomeElement { ); if (!isOverItem && !isOverSubmenu) { setTimeout(() => { - if (!submenuItemHovered && !submenuElementHovered) { + if (this.activeSubmenus.includes(branch) && !currentSubmenuItem.matches(":hover") && !currentSubmenuItem.submenuElement.matches(":hover")) { currentSubmenuItem.submenuOpen = false; } }, 100); @@ -241,15 +206,20 @@ var WaDropdown = class extends WebAwesomeElement { handleSizeChange() { warnDeprecatedSize(this.localName, this.size); } + connectedCallback() { + super.connectedCallback(); + if (this.hasUpdated && this.open) { + void this.updateMenuVisibility(true); + } + } disconnectedCallback() { super.disconnectedCallback(); + this.menuTransition?.abort(); + this.menu?.classList.remove("show", "hide"); + if (this.popup) this.popup.active = false; + openDropdowns.delete(this); clearInterval(this.userTypedTimeout); this.closeAllSubmenus(); - this.submenuCleanups.forEach((cleanup) => cleanup()); - this.submenuCleanups.clear(); - document.removeEventListener("mousemove", this.handleGlobalMouseMove); - document.removeEventListener("keydown", this.handleDocumentKeyDown); - document.removeEventListener("pointerdown", this.handleDocumentPointerDown); unregisterDismissible(this); } firstUpdated(changedProperties) { @@ -266,12 +236,7 @@ var WaDropdown = class extends WebAwesomeElement { return; } this.customStates.set("open", this.open); - if (this.open) { - await this.showMenu(); - } else { - this.closeAllSubmenus(); - await this.hideMenu(); - } + await this.updateMenuVisibility(this.open); } if (changedProperties.has("size")) { this.syncItemSizes(); @@ -300,104 +265,73 @@ var WaDropdown = class extends WebAwesomeElement { ); items.forEach((item) => item.size = this.size); } - /** Handles the submenu navigation stack */ - addToSubmenuStack(item) { - const index = this.openSubmenuStack.indexOf(item); - if (index !== -1) { - this.openSubmenuStack = this.openSubmenuStack.slice(0, index + 1); - } else { - this.openSubmenuStack.push(item); - } - } - /** Removes the last item from the submenu stack */ - removeFromSubmenuStack() { - return this.openSubmenuStack.pop(); - } - /** Gets the current active submenu item */ + /** Gets the current active submenu item. */ getCurrentSubmenuItem() { - return this.openSubmenuStack.length > 0 ? this.openSubmenuStack[this.openSubmenuStack.length - 1] : void 0; + return this.activeSubmenus.at(-1)?.item; } - /** Closes all submenus in the dropdown. */ - closeAllSubmenus() { - const items = this.getItems(true); - items.forEach((item) => { - item.submenuOpen = false; - }); - this.openSubmenuStack = []; - } - /** Closes sibling submenus at the same level as the specified item. */ - closeSiblingSubmenus(item) { - const parentDropdownItem = item.closest('wa-dropdown-item:not([slot="submenu"])'); - let siblingItems; - if (parentDropdownItem) { - siblingItems = this.getSubmenuItems(parentDropdownItem, true); - } else { - siblingItems = this.getItems(true); - } - siblingItems.forEach((siblingItem) => { - if (siblingItem !== item && siblingItem.submenuOpen) { - siblingItem.submenuOpen = false; - } - }); - if (!this.openSubmenuStack.includes(item)) { - this.openSubmenuStack.push(item); + /** Retires an ancestor branch before its hide animations can complete. */ + closeAllSubmenus(from = 0) { + // Remove ownership first: item close notifications may reenter this owner. + for (const { item, cleanup } of this.activeSubmenus.splice(from).reverse()) { + cleanup(); + void item.closeSubmenu(); } } /** Get the slotted trigger button, a or