shrooms-vb-web/toolkit/MenuBar.js

300 lines
8.8 KiB
JavaScript
Raw Permalink Normal View History

2025-02-18 22:39:36 +00:00
// 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;
}
};