UNPKG

@synergy-design-system/components

Version:
318 lines (316 loc) 11.2 kB
// src/components/menu-item/submenu-controller.ts import { createRef, ref } from "lit/directives/ref.js"; import { html } from "lit"; var SubmenuController = class { constructor(host, hasSlotController) { this.popupRef = createRef(); this.enableSubmenuTimer = -1; this.hasGlobalDismissListeners = false; this.isConnected = false; this.isPopupConnected = false; this.skidding = 0; this.submenuOpenDelay = 100; // Set the safe triangle cursor position this.handleMouseMove = (event) => { this.host.style.setProperty("--safe-triangle-cursor-x", `${event.clientX}px`); this.host.style.setProperty("--safe-triangle-cursor-y", `${event.clientY}px`); }; this.handleMouseOver = () => { if (this.hasSlotController.test("submenu")) { this.enableSubmenu(); } }; this.handleHostMouseLeave = (event) => { if (this.isWithinSubmenuInteractionTree(event.relatedTarget)) { return; } this.disableSubmenu(); }; // Focus on the first menu-item of a submenu. this.handleKeyDown = (event) => { switch (event.key) { case "Escape": case "Tab": this.disableSubmenu(); break; case "ArrowLeft": if (event.target !== this.host) { event.preventDefault(); event.stopPropagation(); this.host.focus(); this.disableSubmenu(); } break; case "ArrowRight": case "Enter": case " ": this.handleSubmenuEntry(event); break; default: break; } }; this.handleClick = (event) => { var _a; if (event.target === this.host) { event.preventDefault(); event.stopPropagation(); } else if (event.target instanceof Element && (event.target.tagName === "syn-menu-item" || ((_a = event.target.role) == null ? void 0 : _a.startsWith("menuitem")))) { this.disableSubmenu(); } }; // Close this submenu on focus outside of the parent or any descendants. this.handleFocusOut = (event) => { if (event.relatedTarget && event.relatedTarget instanceof Element && this.host.contains(event.relatedTarget)) { return; } this.disableSubmenu(); }; this.handleWindowBlur = () => { this.disableSubmenu(); }; this.handlePageHide = () => { this.disableSubmenu(); }; this.handleVisibilityChange = () => { if (document.visibilityState === "hidden") { this.disableSubmenu(); } }; this.handleDocumentKeyDown = (event) => { if (event.key === "Escape" && this.isExpanded()) { this.disableSubmenu(); event.stopPropagation(); } }; // Prevent the parent menu-item from getting focus on mouse movement on the submenu this.handlePopupMouseover = (event) => { event.stopPropagation(); }; this.handlePopupMouseLeave = (event) => { if (this.isWithinSubmenuInteractionTree(event.relatedTarget)) { return; } this.disableSubmenu(); }; // Set the safe triangle values for the submenu when the position changes this.handlePopupReposition = () => { const submenuSlot = this.host.renderRoot.querySelector("slot[name='submenu']"); const menu = submenuSlot == null ? void 0 : submenuSlot.assignedElements({ flatten: true }).filter((el) => el.localName === "syn-menu")[0]; const isRtl = getComputedStyle(this.host).direction === "rtl"; if (!menu) { return; } const { left, top, width, height } = menu.getBoundingClientRect(); this.host.style.setProperty("--safe-triangle-submenu-start-x", `${isRtl ? left + width : left}px`); this.host.style.setProperty("--safe-triangle-submenu-start-y", `${top}px`); this.host.style.setProperty("--safe-triangle-submenu-end-x", `${isRtl ? left + width : left}px`); this.host.style.setProperty("--safe-triangle-submenu-end-y", `${top + height}px`); }; (this.host = host).addController(this); this.hasSlotController = hasSlotController; } hostConnected() { if (this.hasSlotController.test("submenu") && !this.host.disabled) { this.addListeners(); } } hostDisconnected() { this.removeListeners(); } hostUpdated() { if (this.hasSlotController.test("submenu") && !this.host.disabled) { this.addListeners(); this.updateSkidding(); } else { this.removeListeners(); } } addListeners() { if (!this.isConnected) { this.host.addEventListener("mousemove", this.handleMouseMove); this.host.addEventListener("mouseover", this.handleMouseOver); this.host.addEventListener("mouseleave", this.handleHostMouseLeave); this.host.addEventListener("keydown", this.handleKeyDown); this.host.addEventListener("click", this.handleClick); this.host.addEventListener("focusout", this.handleFocusOut); this.isConnected = true; } if (!this.isPopupConnected) { if (this.popupRef.value) { this.popupRef.value.addEventListener("mouseover", this.handlePopupMouseover); this.popupRef.value.addEventListener("mouseleave", this.handlePopupMouseLeave); this.popupRef.value.addEventListener("syn-reposition", this.handlePopupReposition); this.isPopupConnected = true; } } } removeListeners() { if (this.isConnected) { this.host.removeEventListener("mousemove", this.handleMouseMove); this.host.removeEventListener("mouseover", this.handleMouseOver); this.host.removeEventListener("mouseleave", this.handleHostMouseLeave); this.host.removeEventListener("keydown", this.handleKeyDown); this.host.removeEventListener("click", this.handleClick); this.host.removeEventListener("focusout", this.handleFocusOut); this.isConnected = false; } if (this.isPopupConnected) { if (this.popupRef.value) { this.popupRef.value.removeEventListener("mouseover", this.handlePopupMouseover); this.popupRef.value.removeEventListener("mouseleave", this.handlePopupMouseLeave); this.popupRef.value.removeEventListener("syn-reposition", this.handlePopupReposition); this.isPopupConnected = false; } } this.removeGlobalDismissListeners(); } addGlobalDismissListeners() { if (this.hasGlobalDismissListeners) { return; } document.addEventListener("keydown", this.handleDocumentKeyDown); window.addEventListener("blur", this.handleWindowBlur); window.addEventListener("pagehide", this.handlePageHide); document.addEventListener("visibilitychange", this.handleVisibilityChange); this.hasGlobalDismissListeners = true; } removeGlobalDismissListeners() { if (!this.hasGlobalDismissListeners) { return; } document.removeEventListener("keydown", this.handleDocumentKeyDown); window.removeEventListener("blur", this.handleWindowBlur); window.removeEventListener("pagehide", this.handlePageHide); document.removeEventListener("visibilitychange", this.handleVisibilityChange); this.hasGlobalDismissListeners = false; } isWithinSubmenuInteractionTree(target) { var _a; if (!(target instanceof Node)) { return false; } if (this.host.contains(target)) { return true; } if ((_a = this.popupRef.value) == null ? void 0 : _a.contains(target)) { return true; } const rootNode = target.getRootNode(); if (rootNode instanceof ShadowRoot) { return this.isWithinSubmenuInteractionTree(rootNode.host); } return false; } handleSubmenuEntry(event) { const submenuSlot = this.host.renderRoot.querySelector("slot[name='submenu']"); if (!submenuSlot) { console.error("Cannot activate a submenu if no corresponding menuitem can be found.", this); return; } let menuItems = null; for (const elt of submenuSlot.assignedElements()) { menuItems = elt.querySelectorAll("syn-menu-item, [role^='menuitem']"); if (menuItems.length !== 0) { break; } } if (!menuItems || menuItems.length === 0) { return; } menuItems[0].setAttribute("tabindex", "0"); for (let i = 1; i !== menuItems.length; ++i) { menuItems[i].setAttribute("tabindex", "-1"); } if (this.popupRef.value) { event.preventDefault(); event.stopPropagation(); if (this.popupRef.value.active) { if (menuItems[0] instanceof HTMLElement) { menuItems[0].focus(); } } else { this.enableSubmenu(false); this.host.updateComplete.then(() => { if (menuItems[0] instanceof HTMLElement) { menuItems[0].focus(); } }); this.host.requestUpdate(); } } } setSubmenuState(state) { if (this.popupRef.value) { if (this.popupRef.value.active !== state) { this.popupRef.value.active = state; if (state) { this.addGlobalDismissListeners(); } else { this.removeGlobalDismissListeners(); } this.host.requestUpdate(); } } } // Shows the submenu. Supports disabling the opening delay, e.g. for keyboard events that want to set the focus to the // newly opened menu. enableSubmenu(delay = true) { if (delay) { window.clearTimeout(this.enableSubmenuTimer); this.enableSubmenuTimer = window.setTimeout(() => { this.setSubmenuState(true); }, this.submenuOpenDelay); } else { this.setSubmenuState(true); } } disableSubmenu() { window.clearTimeout(this.enableSubmenuTimer); this.setSubmenuState(false); } // Calculate the space the top of a menu takes-up, for aligning the popup menu-item with the activating element. updateSkidding() { var _a; if (!((_a = this.host.parentElement) == null ? void 0 : _a.computedStyleMap)) { return; } const styleMap = this.host.parentElement.computedStyleMap(); const attrs = ["padding-top", "border-top-width", "margin-top"]; const skidding = attrs.reduce((accumulator, attr) => { var _a2; const styleValue = (_a2 = styleMap.get(attr)) != null ? _a2 : new CSSUnitValue(0, "px"); const unitValue = styleValue instanceof CSSUnitValue ? styleValue : new CSSUnitValue(0, "px"); const pxValue = unitValue.to("px"); return accumulator - pxValue.value; }, 0); this.skidding = skidding; } isExpanded() { return this.popupRef.value ? this.popupRef.value.active : false; } renderSubmenu() { const isRtl = getComputedStyle(this.host).direction === "rtl"; if (!this.isConnected) { return html` <slot name="submenu" hidden></slot> `; } return html` <syn-popup ${ref(this.popupRef)} placement=${isRtl ? "left-start" : "right-start"} anchor="anchor" flip flip-fallback-strategy="best-fit" skidding="${this.skidding}" auto-size="vertical" auto-size-padding="10" > <slot name="submenu"></slot> </syn-popup> `; } }; export { SubmenuController }; //# sourceMappingURL=chunk.WUGCLTH6.js.map