@claromentis/design-system
Version:
Claromentis Design System Component Library
248 lines (247 loc) • 7.3 kB
JavaScript
import { h } from '@stencil/core';
/**
* @slot - Allows menu items to be added to the dropdown menu.
*/
export class Cardmenu {
constructor() {
this.toggle = false;
this.isMobile = false;
this.closeOnSelect = false;
}
/**
* Listen for mousedown events and handle them
*/
handleClick(event) {
// Close the menu if the user clicks elsewhere on the page
if (!this.toggle)
return;
if (event.composedPath().indexOf(this.element) === -1)
this.toggle = false;
// Close Menu on Item Select
const targetElement = event.target;
if (this.closeOnSelect && targetElement.parentElement.closest('cla-menu')) {
this.toggle = false;
}
}
/**
* Listen for window resize events and handle
*/
handleWindowResize() {
this.checkIfMobile();
}
getMenuItems() {
const allSlotted = Array.from(this.element.querySelectorAll('*'));
return allSlotted.filter(el => {
// Exclude non-interactive elements (hr, div, span, p, etc.)
const isFocusable = (el instanceof HTMLAnchorElement ||
el instanceof HTMLButtonElement ||
el instanceof HTMLInputElement ||
el instanceof HTMLSelectElement ||
el instanceof HTMLTextAreaElement ||
el.tabIndex >= 0 // catches custom elements with explicit tabindex
);
if (!isFocusable)
return false;
// Exclude disabled elements
if (el.disabled)
return false;
// Exclude hidden elements
if (el.offsetParent === null)
return false;
// Exclude the toggle button itself
if (el.classList.contains('dropdown-toggle-button'))
return false;
return true;
});
}
getFocusedIndex(items) {
return items.findIndex(item => item === document.activeElement);
}
getToggleButton() {
return this.element.querySelector('.dropdown-toggle-button');
}
/**
* Listen for keydown events and handle
*
*/
handleKeyDown(event) {
var _a, _b, _c, _d;
// Ignore events that didn't originate inside this component instance
if (!this.element.contains(event.target))
return;
const items = this.getMenuItems();
const currentIndex = this.getFocusedIndex(items);
switch (event.key) {
case 'Enter': {
const target = event.target;
// open/close menu when the toggle button is focussed
if (target.shadowRoot.activeElement.classList.contains('dropdown-toggle-button')) {
this.toggleMenu();
break;
}
//when closeOnSelect is true close menu when 'Enter' is pressed
if (this.closeOnSelect && this.toggle) {
this.toggle = false;
(_a = this.getToggleButton()) === null || _a === void 0 ? void 0 : _a.focus(); // return focus
}
break;
}
case 'Escape': {
//close menu if tab away from last item
if (this.toggle) {
this.toggleMenu();
(_b = this.getToggleButton()) === null || _b === void 0 ? void 0 : _b.focus(); // ← return focus to trigger
}
break;
}
case 'Tab': {
if (!this.toggle)
break;
const isFirst = currentIndex === 0;
const isLast = currentIndex === items.length - 1;
// Tab forward from last item, or shift+Tab from first item → close
if ((!event.shiftKey && isLast) || (event.shiftKey && isFirst)) {
this.toggleMenu();
// Don't preventDefault — let Tab move focus naturally
}
break;
}
case 'ArrowDown': {
if (!this.toggle)
break;
event.preventDefault(); // stop page scroll
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
(_c = items[nextIndex]) === null || _c === void 0 ? void 0 : _c.focus();
break;
}
case 'ArrowUp': {
if (!this.toggle)
break;
event.preventDefault(); // stop page scroll
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
(_d = items[prevIndex]) === null || _d === void 0 ? void 0 : _d.focus();
break;
}
}
}
componentWillLoad() {
//make non-link menu items tabbable
let childrenArray = Array.from(this.element.children);
childrenArray.forEach(function (menuItem) {
menuItem.setAttribute('tabindex', '0');
});
}
componentDidLoad() {
this.checkIfMobile();
}
/**
* Checks if screen width is a mobile width
*
* @return Boolean
*/
checkIfMobile() {
return window.screen.width < 768 ? this.isMobile = true : this.isMobile = false;
}
/**
* Toggles the opening and closing of the menu.
*
* @return Boolean
*/
toggleMenu() {
return this.toggle = !this.toggle;
}
/**
* Get the map of CSS classes for the button.
*
* @return CssClassMap
*/
getMenuClassMap() {
return {
'dropdown-menu': true,
'dropdown-menu-right': this.isMobile ? true : false
};
}
render() {
return (h("div", { class: this.toggle ? "dropdown-wrapper in" : "dropdown-wrapper" }, h("button", { class: "dropdown-toggle-button", onMouseDown: () => this.toggleMenu(), onKeyDown: (e) => this.handleKeyDown(e) }, h("ion-icon", { name: "ellipsis-vertical-circle-outline", "aria-hidden": "true" }), h("span", { class: "sr-only" }, "toggle dropdown")), h("div", { class: this.getMenuClassMap() }, h("slot", null))));
}
static get is() { return "cla-menu"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"$": ["cla-menu.scss"]
};
}
static get styleUrls() {
return {
"$": ["cla-menu.css"]
};
}
static get properties() {
return {
"closeOnSelect": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "If 'true' the menu will close when an item is selected"
},
"attribute": "close-on-select",
"reflect": false,
"defaultValue": "false"
}
};
}
static get states() {
return {
"toggle": {},
"isMobile": {}
};
}
static get events() {
return [{
"method": "toggleEvent",
"name": "toggleEvent",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the menu is toggled."
},
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
}
}];
}
static get elementRef() { return "element"; }
static get listeners() {
return [{
"name": "mousedown",
"method": "handleClick",
"target": "window",
"capture": false,
"passive": true
}, {
"name": "resize",
"method": "handleWindowResize",
"target": "window",
"capture": false,
"passive": true
}, {
"name": "keydown",
"method": "handleKeyDown",
"target": "document",
"capture": false,
"passive": false
}];
}
}