I am building a context menu with support for escape and navigation keys. However, I've noticed that if I open the context menu for the first time, the arrow navigation keys work perfectly; however, if I open it by the second time, the arrow navigation keys work in reverse order (a bug in my logic?). If I open it more times, it bugs totally and may not work anymore.
Playground
Here are related code parts:
(navigation on outermost context menu)
for (let i = 0; i < div.children.length; i++)
{
// Child (item or submenu)
const child = div.children[i] as HTMLElement;
// If focused
if (document.activeElement === child)
{
// navigate up
if (Input.input.justPressed("navigateUp"))
{
focusPrevSibling(child);
}
// navigate down
else if (Input.input.justPressed("navigateDown"))
{
focusNextSibling(child);
}
// open submenu
else if (Input.input.justPressed(localeDir == "ltr" ? "navigateRight" : "navigateLeft") && child.classList.contains(submenuItemClassName))
{
(child as HTMLButtonElement).click();
}
return;
}
}
(focusability utils)
/**
* Focus the previous focusable sibling of an element.
*/
export function focusPrevSibling(element: HTMLElement): void
{
const parent = element.parentElement;
const children = Array.from(parent.children) as HTMLElement[];
const i = children.indexOf(element);
const list = children.slice(0, i).reverse().concat(children.slice(i + 1).reverse());
const firstFocusable = list.find(e => canFocus(e));
if (firstFocusable)
{
(firstFocusable as HTMLElement).focus();
}
}
/**
* Focus the next focusable sibling of an element.
*/
export function focusNextSibling(element: HTMLElement): void
{
const parent = element.parentElement;
const children = Array.from(parent.children) as HTMLElement[];
const i = children.indexOf(element);
const list = children.slice(i + 1).concat(children.slice(0, i + 1));
const firstFocusable = list.find(e => canFocus(e));
if (firstFocusable)
{
(firstFocusable as HTMLElement).focus();
}
}
The problem here is basically that these functions like focusNextSibling
are messed up next open up times. The firstFocusable
that comes out is wrong in past opens.
It does seem to have to do with the Input.input
event listeners that get accumulated, because since when I press for example down
it outputs multiple console.log
s gradually more each time I re-open the context menu; however, I have now done removeEventListener
before addEventListener
and it doesn't seem to work still (it keeps accumulating).
I am building a context menu with support for escape and navigation keys. However, I've noticed that if I open the context menu for the first time, the arrow navigation keys work perfectly; however, if I open it by the second time, the arrow navigation keys work in reverse order (a bug in my logic?). If I open it more times, it bugs totally and may not work anymore.
Playground
Here are related code parts:
(navigation on outermost context menu)
for (let i = 0; i < div.children.length; i++)
{
// Child (item or submenu)
const child = div.children[i] as HTMLElement;
// If focused
if (document.activeElement === child)
{
// navigate up
if (Input.input.justPressed("navigateUp"))
{
focusPrevSibling(child);
}
// navigate down
else if (Input.input.justPressed("navigateDown"))
{
focusNextSibling(child);
}
// open submenu
else if (Input.input.justPressed(localeDir == "ltr" ? "navigateRight" : "navigateLeft") && child.classList.contains(submenuItemClassName))
{
(child as HTMLButtonElement).click();
}
return;
}
}
(focusability utils)
/**
* Focus the previous focusable sibling of an element.
*/
export function focusPrevSibling(element: HTMLElement): void
{
const parent = element.parentElement;
const children = Array.from(parent.children) as HTMLElement[];
const i = children.indexOf(element);
const list = children.slice(0, i).reverse().concat(children.slice(i + 1).reverse());
const firstFocusable = list.find(e => canFocus(e));
if (firstFocusable)
{
(firstFocusable as HTMLElement).focus();
}
}
/**
* Focus the next focusable sibling of an element.
*/
export function focusNextSibling(element: HTMLElement): void
{
const parent = element.parentElement;
const children = Array.from(parent.children) as HTMLElement[];
const i = children.indexOf(element);
const list = children.slice(i + 1).concat(children.slice(0, i + 1));
const firstFocusable = list.find(e => canFocus(e));
if (firstFocusable)
{
(firstFocusable as HTMLElement).focus();
}
}
The problem here is basically that these functions like focusNextSibling
are messed up next open up times. The firstFocusable
that comes out is wrong in past opens.
It does seem to have to do with the Input.input
event listeners that get accumulated, because since when I press for example down
it outputs multiple console.log
s gradually more each time I re-open the context menu; however, I have now done removeEventListener
before addEventListener
and it doesn't seem to work still (it keeps accumulating).
The only solution I have found is to have a global Input.input.addEventListener()
call (rather than adding an event listener per context menu) and through it call a global callback that is updated by the context menus.
// Weak map mapping to Input listeners of submenus reliably.
// The keys are the submenu lists themselves, not the submenu representing items.
const submenuInputPressedListeners = new WeakMap<HTMLDivElement, Function>();
// Globalized input action listener
Input.input.addEventListener("inputPressed", function(): void
{
currentInputPressedListener?.();
});
(Updated with either currentInputPressedListener = input_onInputPressed;
or currentInputPressedListener = null;
, instead of Input.input.addEventListener
or removeEventListener
)
Now am I doing this to test navigation input on submenus from outer context menus:
// Check input on submenu
submenuInputPressedListeners.get(submenus[submenus.length - 1])();