// Window menu bar export default (Toolkit,_package)=>class MenuBar extends Toolkit.Component { // Instance fields #_ariaLabel; // Accessible label ///////////////////////// Initialization Methods ////////////////////////// static { _package.MenuBar = { activate : (c,i,f,x)=>c.#_activate(i,f,x), children : c=>this.#_children(c), onLocalize: c=>c.#onLocalize() }; } constructor(app, overrides) { super(app, _package.override(overrides, { ariaOrientation: "horizontal", className : "menu-bar", role : "menubar", style : { display : "flex", flexWrap: "wrap" } })); // Configure instance fields this.#_ariaLabel = null; // Configure event listeners this.addEventListener("focusout", e=>this.#_onBlur (e)); this.addEventListener("keydown" , e=>this.#_onKeyDown(e)); } /////////////////////////////// Properties //////////////////////////////// // Accessible label get ariaLabel() { return this.#_ariaLabel; } set ariaLabel(value) { if (value != null && !(value instanceof RegExp)) value = String(value); this.#_ariaLabel = value; this.#onLocalize(); } ///////////////////////////// Event Handlers ////////////////////////////// // Focus out #_onBlur(e) { if (this.element.contains(e.relatedTarget)) return; } // Key pressed #_onKeyDown(e) { let item = Toolkit.component(e.originalTarget); // Processing by key switch (e.key) { case " ": case "Enter": case "Pointer": this.#_activate(item, e.key == "Enter", e.key != " "); break; case "ArrowDown": // Expand the sub-menu and focus its first item if (item.parent instanceof Toolkit.MenuBar) { item.expanded = true; this.#_focusBookend(item, false); } // Focus the next available sibling else this.#_focusCycle(item, false); break; case "ArrowUp": // Focus the previous available sibling if (!(item.parent instanceof Toolkit.MenuBar)) this.#_focusCycle(item, true); break; case "ArrowRight": // Focus the next available sibling if (item.parent instanceof Toolkit.MenuBar) this.#_focusCycle(item, false); // Expand the sub-menu and focus its first item else if (item.children.length != 0) { item.expanded = true; this.#_focusBookend(item, false); } // Focus the next top-level sibling's first sub-item else { while (!(item.parent instanceof Toolkit.MenuBar)) item = item.parent; let expanded = item.expanded; let next = this.#_focusCycle(item, false); if (!( expanded && next != null && next != item && next.children.length != 0 )) break; next.expanded = true; this.#_focusBookend(next, false); } break; case "ArrowLeft": // Focus the previous available sibling if (item.parent instanceof Toolkit.MenuBar) this.#_focusCycle(item, true); // Focus the previous top-level sibling's first sub-item else if (item.parent.parent instanceof Toolkit.MenuBar) { while (!(item.parent instanceof Toolkit.MenuBar)) item = item.parent; let expanded = item; let next = this.#_focusCycle(item, true); if (!(expanded && next != null && next != item)) break; next.expanded = true; this.#_focusBookend(next, false); } // Collapse the sub-menu else item.parent.element.focus(); break; case "End": // Focus the last sibling menu item this.#_focusBookend(item.parent, true); break; case "Escape": // Collapse the sub-menu if (item.expanded) { item.expanded = false; } // Collapse the current menu and focus on the menu item else if (!(item.parent instanceof Toolkit.MenuBar)) { item.parent.expanded = false; item.parent.element.focus(); } // Restore focus to the previous element else this.#_restoreFocus(); break; case "Home": // Focus the first sibling menu item this.#_focusBookend(item.parent, false); break; default: { // Focus the next item that starts with the typed key let key = e.key.toLowerCase(); if (key.length != 1) return; // Allow the event to bubble this.#_focusCycle(item, false, key); } } // Event has been handled Toolkit.consume(e); } // Configure display text #onLocalize() { this.element.ariaLabel = this.app.translate(this.#_ariaLabel, this) ?? ""; } ///////////////////////////// Public Methods ////////////////////////////// // Add a menu item add(comp) { if (!(comp instanceof Toolkit.MenuItem)) throw new TypeError("Component must be a Toolkit.MenuItem."); super.add(comp); comp.element.tabIndex = _package.MenuBar.children(this).length == 1 ? 0 : -1; } ///////////////////////////// Private Methods ///////////////////////////// // Activate a menu item #_activate(item, focus, close) { // Error checking if (item.disabled) return; //switch (item.constructor) { // case Toolkit.CheckBoxMenuItem : return; // case Toolkit.RadioButtonMenuItem: return; //} // Item does not have a sub-menu if (_package.MenuBar.children(item).length == 0) { _package.MenuItem.activate(item, true); if (close || item.type == "button") this.#_restoreFocus(); return; } // Collapse any other open sub-menu let prev = item.parent.children.find(c=>c.expanded); if (prev != null && prev != item) prev.expanded = false; // Expand the sub-menu item.expanded = true; if (focus) this.#_focusBookend(item, false); } // Select eligible menu items static #_children(menu) { return menu == null ? [] : menu.children.filter(c=>c.visible); } // Collapse other sub-menus and expand a given sub-menu #_expand(item) { let other = item.parent.children.find(c=>c.expanded && c != item); if (other != null) other.expanded = false; if (item.children.length != 0) other.expanded = true; } // Move focus to the first or last menu item #_focusBookend(menu, end) { let children = _package.MenuBar.children(menu); if (children.length == 0) return null; let item = children[end ? children.length - 1 : 0]; item.element.focus(); return item; } // Move focus to the next sibling of a menu item #_focusCycle(item, reverse, key = null) { let children = _package.MenuBar.children(item.parent).filter(c=> (key == null || _package.MenuItem.startsWith(c, key))); // No sibling menu items are eligible if (children.length == 0) return null; // Working variables let index = children.indexOf(item); let step = children.length + (reverse ? -1 : 1); // Find the next eligible sibling in the list let sibling = children[(index + step) % children.length]; sibling.element.focus(); return sibling; } // Retrieve the root-level menu bar containing a menu item #_menuBar(item) { while (!(item instanceof Toolkit.MenuBar)) item = item.parent; return item; } // Restore focus to the previous component #_restoreFocus() { let item = _package.MenuBar.children(this).find(c=>c.expanded) if (item != null) item.expanded = false; } };