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