300 lines
8.8 KiB
JavaScript
300 lines
8.8 KiB
JavaScript
// 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;
|
|
}
|
|
|
|
};
|