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