@synergy-design-system/components
Version:
This package provides the base of the Synergy Design System as native web components. It uses [lit](https://www.lit.dev) and parts of [shoelace](https://shoelace.style/). Synergy officially supports the latest two versions of all major browsers (as define
242 lines (240 loc) • 8.58 kB
JavaScript
// 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.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();
}
};
// 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();
};
// Prevent the parent menu-item from getting focus on mouse movement on the submenu
this.handlePopupMouseover = (event) => {
event.stopPropagation();
};
// 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("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("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("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("syn-reposition", this.handlePopupReposition);
this.isPopupConnected = 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;
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}"
strategy="fixed"
auto-size="vertical"
auto-size-padding="10"
>
<slot name="submenu"></slot>
</syn-popup>
`;
}
};
export {
SubmenuController
};
//# sourceMappingURL=chunk.V4HPWQE7.js.map