@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
128 lines • 4.64 kB
JavaScript
import { EventHub, ObservableValue } from '@furystack/utils';
/**
* Manages context menu state including open/close, items, focus, positioning, and keyboard navigation
*/
export class ContextMenuManager extends EventHub {
isOpened = new ObservableValue(false);
items = new ObservableValue([]);
focusedIndex = new ObservableValue(-1);
position = new ObservableValue({ x: 0, y: 0 });
/**
* Returns the indices of items that are navigable (non-separator, non-disabled)
*/
getNavigableIndices() {
return this.items
.getValue()
.map((item, index) => ({ item, index }))
.filter(({ item }) => item.type === 'item' && !item.disabled)
.map(({ index }) => index);
}
/**
* Opens the context menu, optionally setting items and position
* @param options - Items and/or position to set
*/
open(options = {}) {
if (options.items) {
this.items.setValue(options.items);
}
if (options.position) {
this.position.setValue(options.position);
}
const navigableIndices = this.getNavigableIndices();
this.focusedIndex.setValue(navigableIndices.length > 0 ? navigableIndices[0] : -1);
this.isOpened.setValue(true);
}
/**
* Closes the context menu and resets focus
*/
close() {
this.isOpened.setValue(false);
this.focusedIndex.setValue(-1);
}
/**
* Selects a menu item by index, emits the selection event, and closes the menu
* @param index - The index of the item to select (defaults to focused item)
*/
selectItem(index) {
const idx = index ?? this.focusedIndex.getValue();
const item = this.items.getValue()[idx];
if (item?.type === 'item' && !item.disabled && item.data !== undefined) {
this.emit('onSelectItem', item.data);
this.close();
}
}
/**
* Handles keyboard events for menu navigation
* @param ev - The keyboard event
*/
handleKeyDown(ev) {
if (!this.isOpened.getValue())
return;
const navigableIndices = this.getNavigableIndices();
switch (ev.key) {
case 'ArrowDown': {
ev.preventDefault();
if (navigableIndices.length === 0)
break;
const currentNavPosition = navigableIndices.indexOf(this.focusedIndex.getValue());
if (currentNavPosition < 0 || currentNavPosition >= navigableIndices.length - 1) {
this.focusedIndex.setValue(navigableIndices[0]);
}
else {
this.focusedIndex.setValue(navigableIndices[currentNavPosition + 1]);
}
break;
}
case 'ArrowUp': {
ev.preventDefault();
if (navigableIndices.length === 0)
break;
const currentNavPosition = navigableIndices.indexOf(this.focusedIndex.getValue());
if (currentNavPosition <= 0) {
this.focusedIndex.setValue(navigableIndices[navigableIndices.length - 1]);
}
else {
this.focusedIndex.setValue(navigableIndices[currentNavPosition - 1]);
}
break;
}
case 'Home': {
ev.preventDefault();
if (navigableIndices.length > 0) {
this.focusedIndex.setValue(navigableIndices[0]);
}
break;
}
case 'End': {
ev.preventDefault();
if (navigableIndices.length > 0) {
this.focusedIndex.setValue(navigableIndices[navigableIndices.length - 1]);
}
break;
}
case 'Enter': {
ev.preventDefault();
const focusedIdx = this.focusedIndex.getValue();
if (focusedIdx >= 0) {
this.selectItem(focusedIdx);
}
break;
}
case 'Escape': {
ev.preventDefault();
this.close();
break;
}
default:
break;
}
}
[Symbol.dispose]() {
this.isOpened[Symbol.dispose]();
this.items[Symbol.dispose]();
this.focusedIndex[Symbol.dispose]();
this.position[Symbol.dispose]();
super[Symbol.dispose]();
}
}
//# sourceMappingURL=context-menu-manager.js.map