diff --git a/dist/chunks/chunk.4HJPQ4O4.js b/dist/chunks/chunk.4HJPQ4O4.js
index 0cd72f56fc98357609a4b2b7576f3579e36bc997..49f741894a607d779a79ca3644e2cfcfcff09197 100644
--- a/dist/chunks/chunk.4HJPQ4O4.js
+++ b/dist/chunks/chunk.4HJPQ4O4.js
@@ -25,9 +25,6 @@ import {
import {
uniqueId
} from "./chunk.KNJT7KBU.js";
-import {
- waitForEvent
-} from "./chunk.F25QOBDY.js";
import {
animateWithClass
} from "./chunk.L6CIKOFQ.js";
@@ -80,83 +77,45 @@ var WaTooltip = class extends WebAwesomeElement {
this.for = null;
this.anchor = null;
this.eventController = new AbortController();
- this.handleBlur = () => {
- this.dismissedByPress = false;
- if (this.hasTrigger("focus")) {
- this.hide();
- }
- };
- this.handleClick = () => {
- if (this.hasTrigger("click")) {
- if (this.open) {
- this.hide();
- } else {
- this.show();
+ this.openTransition = { open: false };
+ this.handleTrigger = (event) => {
+ const type = event.type;
+ const hover = type === "mouseover" || type === "mouseout";
+ let open = type === "focus" || type === "mouseover";
+ if (type === "click" || type === "mousedown") {
+ if (!this.hasTrigger("click")) {
+ if (!this.hasTrigger("manual")) this.lightDismiss();
+ return;
}
- return;
- }
- if (this.hasTrigger("manual")) {
- return;
- }
- this.lightDismiss();
- };
- this.handleFocus = () => {
- if (this.dismissedByPress) {
- return;
- }
- if (this.hasTrigger("focus")) {
- this.show();
+ if (type !== "click") return;
+ open = !this.open;
+ } else {
+ if (!open) {
+ const target = event.relatedTarget;
+ if (hover && target && ((this.anchor && containsComposedNode(this.anchor, target)) || containsComposedNode(this, target))) return;
+ this.dismissedByPress = false;
+ }
+ if (this.dismissedByPress || !this.hasTrigger(hover ? "hover" : "focus")) return;
}
- };
- this.handleMouseDown = () => {
- if (this.hasTrigger("click") || this.hasTrigger("manual")) {
- return;
+ const request = () => open ? this.show() : this.hide();
+ if (hover) {
+ clearTimeout(this.hoverTimeout);
+ this.hoverTimeout = window.setTimeout(request, open ? this.showDelay : this.hideDelay);
+ } else {
+ request();
}
- this.lightDismiss();
};
- this.handleDocumentKeyDown = (event) => {
- if (this.hasTrigger("manual")) {
- return;
- }
- if (event.key === "Escape" && this.open && isTopDismissible(this)) {
+ this.handleDocumentDismiss = (event) => {
+ if (this.hasTrigger("manual")) return;
+ if (event.type === "keydown") {
+ if (event.key !== "Escape" || !this.open || !isTopDismissible(this)) return;
event.preventDefault();
event.stopPropagation();
- this.hide();
- }
- };
- this.handleDocumentClick = (event) => {
- if (this.hasTrigger("manual")) {
- return;
- }
- if (this.anchor && event.composedPath().includes(this.anchor)) {
+ } else if (this.anchor && event.composedPath().includes(this.anchor)) {
return;
}
this.hide();
};
- this.handleMouseOver = () => {
- if (this.dismissedByPress) {
- return;
- }
- if (this.hasTrigger("hover")) {
- clearTimeout(this.hoverTimeout);
- this.hoverTimeout = window.setTimeout(() => this.show(), this.showDelay);
- }
- };
- this.handleMouseOut = (event) => {
- const relatedTarget = event.relatedTarget;
- const movedIntoAnchor = Boolean(relatedTarget && this.anchor && containsComposedNode(this.anchor, relatedTarget));
- const movedIntoTooltip = Boolean(relatedTarget && containsComposedNode(this, relatedTarget));
- if (movedIntoAnchor || movedIntoTooltip) {
- return;
- }
- this.dismissedByPress = false;
- if (this.hasTrigger("hover")) {
- clearTimeout(this.hoverTimeout);
- this.hoverTimeout = window.setTimeout(() => {
- this.hide();
- }, this.hideDelay);
- }
- };
}
connectedCallback() {
super.connectedCallback();
@@ -165,43 +124,33 @@ var WaTooltip = class extends WebAwesomeElement {
if (this.eventController.signal.aborted) {
this.eventController = new AbortController();
}
- this.addEventListener("mouseout", this.handleMouseOut);
+ this.addEventListener("mouseout", this.handleTrigger);
this.dismissedByPress = false;
- if (this.open) {
- this.open = false;
- this.updateComplete.then(() => {
- this.open = true;
- });
- }
+ // The owner reads current intent after rendering, including on reconnect.
+ void this.requestOpen(this.open);
if (!this.id) {
this.id = uniqueId("wa-tooltip-");
}
- if (this.for && this.anchor) {
+ if (this.for) {
this.anchor = null;
this.handleForChange();
- } else if (this.for) {
- this.handleForChange();
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
- document.removeEventListener("keydown", this.handleDocumentKeyDown);
- document.removeEventListener("click", this.handleDocumentClick);
unregisterDismissible(this);
- this.eventController.abort();
+ // Avoid retaining detached anchors in the default abort reason's stack.
+ this.eventController.abort(null);
+ clearTimeout(this.hoverTimeout);
+ this.openTransition = { open: false, promise: this.openTransition.promise };
+ this.popup?.popup?.classList.remove("show-with-scale", "hide-with-scale");
+ if (this.body) this.body.hidden = true;
+ if (this.popup) this.popup.active = false;
if (this.anchor) {
this.removeFromAriaLabelledBy(this.anchor, this.id);
}
}
- firstUpdated(changedProperties) {
- this.body.hidden = !this.open;
- if (this.open) {
- this.popup.active = true;
- this.popup.reposition();
- }
- super.firstUpdated(changedProperties);
- }
/** Hides the tooltip, or cancels a pending show, and keeps it hidden until re-armed. */
lightDismiss() {
clearTimeout(this.hoverTimeout);
@@ -233,41 +182,47 @@ var WaTooltip = class extends WebAwesomeElement {
}
}
async handleOpenChange() {
- if (this.open) {
- if (this.disabled) {
- return;
- }
- const waShowEvent = new WaShowEvent();
- this.dispatchEvent(waShowEvent);
- if (waShowEvent.defaultPrevented) {
- this.open = false;
- return;
- }
- if (!this.hasTrigger("manual")) {
- document.addEventListener("keydown", this.handleDocumentKeyDown, { signal: this.eventController.signal });
- document.addEventListener("click", this.handleDocumentClick, { signal: this.eventController.signal });
- registerDismissible(this);
- }
- this.body.hidden = false;
- this.popup.active = true;
- await animateWithClass(this.popup.popup, "show-with-scale");
- this.popup.reposition();
- this.dispatchEvent(new WaAfterShowEvent());
- } else {
- const waHideEvent = new WaHideEvent();
- this.dispatchEvent(waHideEvent);
- if (waHideEvent.defaultPrevented) {
- this.open = true;
- return;
+ if (this.disabled) this.open = false;
+ const previous = this.openTransition;
+ const open = this.open;
+ if (!this.isConnected || previous.open === open) return previous.promise;
+ const allowed = this.dispatchEvent(open ? new WaShowEvent() : new WaHideEvent());
+ if (this.openTransition === previous) {
+ if (!allowed && !this.disabled) {
+ this.open = previous.open;
+ } else if ((this.open && !this.disabled) === open) {
+ const transition = this.openTransition = { open };
+ // Disconnect invalidates the transition, so identity also fences connections.
+ const isCurrent = () => this.openTransition === transition && (this.open && !this.disabled) === open;
+ const dismissible = open && !this.hasTrigger("manual");
+ for (const type of ["keydown", "click"]) {
+ document[dismissible ? "addEventListener" : "removeEventListener"](type, this.handleDocumentDismiss, { signal: this.eventController.signal });
+ }
+ if (dismissible) registerDismissible(this);
+ else unregisterDismissible(this);
+ if (open) {
+ this.body.hidden = false;
+ this.popup.active = true;
+ }
+ // Join previous animation cleanup before reusing its classes.
+ this.popup.popup.classList.remove("show-with-scale", "hide-with-scale");
+ transition.promise = (async () => {
+ await previous.promise;
+ if (!isCurrent()) return;
+ await animateWithClass(this.popup.popup, open ? "show-with-scale" : "hide-with-scale");
+ if (!isCurrent()) return;
+ if (open) {
+ this.popup.reposition();
+ } else {
+ this.popup.active = false;
+ this.body.hidden = true;
+ }
+ // Reposition emits a synchronous public event that can revoke ownership.
+ if (isCurrent()) this.dispatchEvent(open ? new WaAfterShowEvent() : new WaAfterHideEvent());
+ })();
}
- document.removeEventListener("keydown", this.handleDocumentKeyDown);
- document.removeEventListener("click", this.handleDocumentClick);
- unregisterDismissible(this);
- await animateWithClass(this.popup.popup, "hide-with-scale");
- this.popup.active = false;
- this.body.hidden = true;
- this.dispatchEvent(new WaAfterHideEvent());
}
+ return this.openTransition.promise;
}
handleForChange() {
const rootNode = this.getRootNode?.();
@@ -281,23 +236,19 @@ var WaTooltip = class extends WebAwesomeElement {
}
this.dismissedByPress = false;
const { signal } = this.eventController;
- if (newAnchor) {
- this.addToAriaLabelledBy(newAnchor, this.id);
- newAnchor.addEventListener("blur", this.handleBlur, { capture: true, signal });
- newAnchor.addEventListener("focus", this.handleFocus, { capture: true, signal });
- newAnchor.addEventListener("click", this.handleClick, { signal });
- newAnchor.addEventListener("mousedown", this.handleMouseDown, { signal });
- newAnchor.addEventListener("mouseover", this.handleMouseOver, { signal });
- newAnchor.addEventListener("mouseout", this.handleMouseOut, { signal });
- }
- if (oldAnchor) {
- this.removeFromAriaLabelledBy(oldAnchor, this.id);
- oldAnchor.removeEventListener("blur", this.handleBlur, { capture: true });
- oldAnchor.removeEventListener("focus", this.handleFocus, { capture: true });
- oldAnchor.removeEventListener("click", this.handleClick);
- oldAnchor.removeEventListener("mousedown", this.handleMouseDown);
- oldAnchor.removeEventListener("mouseover", this.handleMouseOver);
- oldAnchor.removeEventListener("mouseout", this.handleMouseOut);
+ const events = [
+ ["blur", this.handleTrigger, true],
+ ["focus", this.handleTrigger, true],
+ ["click", this.handleTrigger],
+ ["mousedown", this.handleTrigger],
+ ["mouseover", this.handleTrigger],
+ ["mouseout", this.handleTrigger]
+ ];
+ if (newAnchor) this.addToAriaLabelledBy(newAnchor, this.id);
+ if (oldAnchor) this.removeFromAriaLabelledBy(oldAnchor, this.id);
+ for (const [type, listener, capture] of events) {
+ newAnchor?.addEventListener(type, listener, { capture, signal });
+ oldAnchor?.removeEventListener(type, listener, capture);
}
this.anchor = newAnchor;
}
@@ -307,26 +258,19 @@ var WaTooltip = class extends WebAwesomeElement {
this.popup.reposition();
}
}
- handleDisabledChange() {
- if (this.disabled && this.open) {
- this.hide();
- }
+ async requestOpen(open) {
+ if (!this.isConnected) return;
+ this.open = open && !this.disabled;
+ await this.updateComplete;
+ return this.handleOpenChange();
}
/** Shows the tooltip. */
- async show() {
- if (this.open) {
- return void 0;
- }
- this.open = true;
- return waitForEvent(this, "wa-after-show");
+ show() {
+ return this.requestOpen(true);
}
/** Hides the tooltip */
- async hide() {
- if (!this.open) {
- return void 0;
- }
- this.open = false;
- return waitForEvent(this, "wa-after-hide");
+ hide() {
+ return this.requestOpen(false);
}
render() {
return html`
@@ -349,7 +293,7 @@ var WaTooltip = class extends WebAwesomeElement {
hover-bridge
.anchor=${this.anchor}
>
-
+
@@ -401,7 +345,7 @@ __decorateClass([
state()
], WaTooltip.prototype, "anchor", 2);
__decorateClass([
- watch("open", { waitUntilFirstUpdate: true })
+ watch(["open", "disabled"], { waitUntilFirstUpdate: true })
], WaTooltip.prototype, "handleOpenChange", 1);
__decorateClass([
watch("for")
@@ -409,9 +353,6 @@ __decorateClass([
__decorateClass([
watch(["distance", "placement", "skidding"])
], WaTooltip.prototype, "handleOptionsChange", 1);
-__decorateClass([
- watch("disabled")
-], WaTooltip.prototype, "handleDisabledChange", 1);
WaTooltip = __decorateClass([
customElement("wa-tooltip")
], WaTooltip);
diff --git a/dist/chunks/chunk.BV5W4T7H.js b/dist/chunks/chunk.BV5W4T7H.js
index ae8bd9b5f14b5b0fbdab4faca38319fbc7e48b2e..9a52ad235e67c90728ca8829d627f9298d8b637d 100644
--- a/dist/chunks/chunk.BV5W4T7H.js
+++ b/dist/chunks/chunk.BV5W4T7H.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.KB3IOPVF.js b/dist/chunks/chunk.KB3IOPVF.js
index 3b1abee761087a9d9784b7a91f6555e1be323145..a870bafe03138f84a1dcdc8a687cda3e5940ad1e 100644
--- a/dist/chunks/chunk.KB3IOPVF.js
+++ b/dist/chunks/chunk.KB3IOPVF.js
@@ -169,10 +169,17 @@ var WaPopup = class extends WebAwesomeElement {
if (!this.anchorEl || !this.active || !this.isConnected) {
return;
}
+ this.cleanup?.();
+ this.cleanup = void 0;
this.popup?.showPopover?.();
- this.cleanup = autoUpdate(this.anchorEl, this.popup, () => {
+ const cleanup = autoUpdate(this.anchorEl, this.popup, () => {
this.reposition();
});
+ if (!this.active || !this.isConnected) {
+ cleanup();
+ return;
+ }
+ this.cleanup = cleanup;
}
async stop() {
return new Promise((resolve) => {
diff --git a/dist/chunks/chunk.L6CIKOFQ.js b/dist/chunks/chunk.L6CIKOFQ.js
index 20d309f89017ef7d1857f3b1c88216a88354fefd..ea0b8ee34255009d9d9e86605dbdc05969ea3840 100644
--- a/dist/chunks/chunk.L6CIKOFQ.js
+++ b/dist/chunks/chunk.L6CIKOFQ.js
@@ -5,32 +5,19 @@ async function animate(el, keyframes, options) {
return el.animate(keyframes, options).finished.catch(() => {
});
}
-function animateWithClass(el, className) {
- return new Promise((resolve) => {
- const controller = new AbortController();
- const { signal } = controller;
- if (el.classList.contains(className)) {
- return;
- }
- el.classList.add(className);
- let resolved = false;
- let onEnd = () => {
- if (resolved) {
- return;
- }
- resolved = true;
- el.classList.remove(className);
- resolve();
- controller.abort();
- };
- el.addEventListener("animationend", onEnd, { once: true, signal });
- el.addEventListener("animationcancel", onEnd, { once: true, signal });
- requestAnimationFrame(() => {
- if (!resolved && el.getAnimations().length === 0) {
- onEnd();
- }
- });
- });
+async function animateWithClass(el, className) {
+ // Observe pending class removal before reusing the same animation name;
+ // otherwise an immediate show/hide can reuse the completed CSSAnimation.
+ el.getAnimations();
+ el.classList.add(className);
+ // Popup activation is reactive: let its display styles render before sampling.
+ // Native completion also settles animations canceled before animationstart, unlike DOM events.
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
+ const animations = el.getAnimations().filter(
+ (animation) => animation instanceof CSSAnimation && animation.effect?.getComputedTiming().endTime !== Infinity
+ );
+ await Promise.allSettled(animations.map((animation) => animation.finished));
+ el.classList.remove(className);
}
function parseDuration(duration) {
duration = duration.toString().toLowerCase();
diff --git a/dist/chunks/chunk.LKUZUNJV.js b/dist/chunks/chunk.LKUZUNJV.js
index e85a015903374362f83cdb454cb7f449346a9448..79901048ffc7619e842bfa31e09cd65ce602c07e 100644
--- a/dist/chunks/chunk.LKUZUNJV.js
+++ b/dist/chunks/chunk.LKUZUNJV.js
@@ -43,9 +43,6 @@ import {
import {
size_styles_default
} from "./chunk.G5ZZIGWB.js";
-import {
- waitForEvent
-} from "./chunk.F25QOBDY.js";
import {
animateWithClass
} from "./chunk.L6CIKOFQ.js";
@@ -74,6 +71,7 @@ var WaSelect = class extends WebAwesomeFormAssociatedElement {
this.selectionOrder = /* @__PURE__ */ new Map();
this.typeToSelectString = "";
this.slotChangePending = false;
+ this.openTransition = { open: false };
this.displayLabel = "";
this.selectedOptions = [];
this.name = "";
@@ -294,7 +292,12 @@ var WaSelect = class extends WebAwesomeFormAssociatedElement {
}
disconnectedCallback() {
super.disconnectedCallback();
- this.removeOpenListeners();
+ this.openTransition.controller?.abort();
+ this.openTransition = { open: false };
+ this.popup?.popup?.classList.remove("show", "hide");
+ if (this.listbox) this.listbox.hidden = true;
+ if (this.popup) this.popup.active = false;
+ unregisterDismissible(this);
this.cachedOptions = null;
}
updateDefaultValue() {
@@ -308,24 +311,6 @@ var WaSelect = class extends WebAwesomeFormAssociatedElement {
this._defaultValue = this.getAttribute("value") || null;
}
}
- addOpenListeners() {
- document.addEventListener("focusin", this.handleDocumentFocusIn);
- document.addEventListener("keydown", this.handleDocumentKeyDown);
- document.addEventListener("mousedown", this.handleDocumentMouseDown);
- registerDismissible(this);
- if (this.getRootNode() !== document) {
- this.getRootNode().addEventListener("focusin", this.handleDocumentFocusIn);
- }
- }
- removeOpenListeners() {
- document.removeEventListener("focusin", this.handleDocumentFocusIn);
- document.removeEventListener("keydown", this.handleDocumentKeyDown);
- document.removeEventListener("mousedown", this.handleDocumentMouseDown);
- unregisterDismissible(this);
- if (this.getRootNode() !== document) {
- this.getRootNode().removeEventListener("focusin", this.handleDocumentFocusIn);
- }
- }
handleFocus() {
this.displayInput.setSelectionRange(0, 0);
}
@@ -590,6 +575,8 @@ var WaSelect = class extends WebAwesomeFormAssociatedElement {
handleDisabledChange() {
if (this.disabled && this.open) {
this.open = false;
+ // The open watcher has already run for this update.
+ this.handleOpenChange();
}
}
handleValueChange() {
@@ -600,56 +587,72 @@ var WaSelect = class extends WebAwesomeFormAssociatedElement {
this.updateValidity();
}
async handleOpenChange() {
- if (this.open && !this.disabled) {
- this.setCurrentOption(this.selectedOptions[0] || this.getFirstOption());
- const waShowEvent = new WaShowEvent();
- this.dispatchEvent(waShowEvent);
- if (waShowEvent.defaultPrevented) {
- this.open = false;
- return;
- }
- this.addOpenListeners();
+ const open = this.open && !this.disabled;
+ if (!this.isConnected || this.openTransition.open === open) return this.openAnimation;
+ if (open) this.setCurrentOption(this.selectedOptions[0] || this.getFirstOption());
+ const event = open ? new WaShowEvent() : new WaHideEvent();
+ this.dispatchEvent(event);
+ if (event.defaultPrevented && !this.disabled) {
+ this.open = this.openTransition.open;
+ return this.openAnimation;
+ }
+ if (!this.isConnected || (this.open && !this.disabled) !== open) return this.openAnimation;
+ this.openTransition.controller?.abort();
+ this.openTransition = { open, controller: new AbortController() };
+ const { signal } = this.openTransition.controller;
+ const isCurrent = () => !signal.aborted && this.isConnected && (this.open && !this.disabled) === open;
+ if (open) {
+ document.addEventListener("focusin", this.handleDocumentFocusIn, { signal });
+ document.addEventListener("keydown", this.handleDocumentKeyDown, { signal });
+ document.addEventListener("mousedown", this.handleDocumentMouseDown, { signal });
+ registerDismissible(this);
+ const root = this.getRootNode();
+ if (root !== document) root.addEventListener("focusin", this.handleDocumentFocusIn, { signal });
this.listbox.hidden = false;
this.popup.active = true;
requestAnimationFrame(() => {
- this.setCurrentOption(this.currentOption);
+ if (isCurrent()) this.setCurrentOption(this.currentOption);
});
- await animateWithClass(this.popup.popup, "show");
- if (this.currentOption) {
+ } else {
+ unregisterDismissible(this);
+ }
+ // Retire the old animation before joining its cleanup; only the accepted
+ // transition may publish visibility, focus, or completion after an await.
+ this.popup.popup.classList.remove("show", "hide");
+ const previousAnimation = this.openAnimation;
+ this.openAnimation = (async () => {
+ await previousAnimation;
+ if (!isCurrent()) return;
+ await animateWithClass(this.popup.popup, open ? "show" : "hide");
+ if (!isCurrent()) return;
+ if (open && this.currentOption) {
scrollIntoView(this.currentOption, this.listbox, "vertical", "auto");
}
- this.dispatchEvent(new WaAfterShowEvent());
- } else {
- const waHideEvent = new WaHideEvent();
- this.dispatchEvent(waHideEvent);
- if (waHideEvent.defaultPrevented) {
- this.open = false;
- return;
+ if (!open) {
+ this.listbox.hidden = true;
+ this.popup.active = false;
}
- this.removeOpenListeners();
- await animateWithClass(this.popup.popup, "hide");
- this.listbox.hidden = true;
- this.popup.active = false;
- this.dispatchEvent(new WaAfterHideEvent());
- }
+ this.dispatchEvent(open ? new WaAfterShowEvent() : new WaAfterHideEvent());
+ })();
+ return this.openAnimation;
}
/** Shows the listbox. */
async show() {
- if (this.open || this.disabled) {
+ if (this.disabled) {
this.open = false;
- return void 0;
+ return;
}
+ if (!this.isConnected) return;
this.open = true;
- return waitForEvent(this, "wa-after-show");
+ await this.updateComplete;
+ return this.handleOpenChange();
}
/** Hides the listbox. */
async hide() {
- if (!this.open || this.disabled) {
- this.open = false;
- return void 0;
- }
this.open = false;
- return waitForEvent(this, "wa-after-hide");
+ if (!this.isConnected) return;
+ await this.updateComplete;
+ return this.openAnimation;
}
/** Sets focus on the control. */
focus(options) {
diff --git a/dist/chunks/chunk.XSKA3LE6.js b/dist/chunks/chunk.XSKA3LE6.js
index 000b05d33179ddd930fd72221becf7a7e3042a2f..971c99516ef06aabb30afca195c4aa45f941bfed 100644
--- a/dist/chunks/chunk.XSKA3LE6.js
+++ b/dist/chunks/chunk.XSKA3LE6.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,12 @@ 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();
+ // A focused item outside this level (e.g. the owner of a hover-opened submenu) keeps its keys.
+ const activeItem = items.find((item) => item === focusedItem) ?? (focusedItem ? void 0 : items.find((item) => item.active));
+ const activeItemIndex = activeItem ? items.indexOf(activeItem) : -1;
let itemToSelect;
if (event.key === "ArrowUp") {
event.preventDefault();
@@ -113,39 +102,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 +127,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 +153,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 +178,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 +197,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 +207,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 +237,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 +266,96 @@ 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