mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 07:27:04 +00:00
- Also fixes similar issues with navigating top level menubar items and submenus
This commit is contained in:
@@ -709,9 +709,24 @@ class PuterContextMenu extends PuterWebComponent {
|
||||
case 'ArrowDown':
|
||||
this._moveFocus(+1);
|
||||
return true;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowUp': {
|
||||
// Root dropdown: ArrowUp on the first focusable item bubbles
|
||||
// to the menubar, which closes the dropdown and re-focuses
|
||||
// the parent menubar button.
|
||||
if ( ! this._parentMenu ) {
|
||||
const f = this._focusableIndices();
|
||||
if ( f.length && this.#focusedIndex === f[0] ) {
|
||||
this.dispatchEvent(new CustomEvent('puter-menu-navigate', {
|
||||
detail: { direction: 'up' },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this._moveFocus(-1);
|
||||
return true;
|
||||
}
|
||||
case 'Home': {
|
||||
const f = this._focusableIndices();
|
||||
if ( f.length ) this._setFocusIndex(f[0]);
|
||||
@@ -822,14 +837,21 @@ class PuterContextMenu extends PuterWebComponent {
|
||||
if ( ! el ) return false;
|
||||
clearTimeout(this.#submenuTimeout);
|
||||
this._cancelSubmenuClose();
|
||||
this._showSubmenu(el, item.items);
|
||||
// Focus first item in submenu, and start it in keyboard-nav mode so a
|
||||
// stale cursor position doesn't fight the keyboard focus.
|
||||
// Don't re-open an already-open submenu for this parent — it would
|
||||
// wipe the user's focused item inside it. We only auto-focus the
|
||||
// first sub-item when the submenu is freshly opened by this call.
|
||||
const wasNewlyOpened = !this.#activeSubmenu || this.#activeSubmenu.parentEl !== el;
|
||||
if ( wasNewlyOpened ) {
|
||||
this._showSubmenu(el, item.items);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const sub = this.#activeSubmenu && this.#activeSubmenu.element;
|
||||
if ( sub ) {
|
||||
if ( ! sub ) return;
|
||||
if ( wasNewlyOpened ) {
|
||||
const f = sub._focusableIndices();
|
||||
if ( f.length ) sub._setFocusIndex(f[0]);
|
||||
}
|
||||
if ( typeof sub._setKeyboardNav === 'function' ) {
|
||||
sub._setKeyboardNav(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,6 +68,11 @@ class PuterMenubar extends PuterWebComponent {
|
||||
.menu-button.focused {
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
/* Suppress stale :hover during keyboard nav so only the focused
|
||||
button highlights. Cleared on the next mousemove. */
|
||||
.menubar.keyboard-nav .menu-button:hover:not(.focused):not(.active) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.menu-button:focus { outline: none; }
|
||||
@media (max-width: 480px) {
|
||||
.menubar {
|
||||
@@ -99,6 +104,9 @@ class PuterMenubar extends PuterWebComponent {
|
||||
.menu-button.focused {
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
.menubar.keyboard-nav .menu-button:hover:not(.focused):not(.active) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -122,6 +130,9 @@ class PuterMenubar extends PuterWebComponent {
|
||||
if ( this._docPointerDownHandler ) {
|
||||
document.removeEventListener('pointerdown', this._docPointerDownHandler, true);
|
||||
}
|
||||
if ( this._mouseMoveHandler ) {
|
||||
document.removeEventListener('mousemove', this._mouseMoveHandler);
|
||||
}
|
||||
|
||||
const buttons = this.$$('.menu-button');
|
||||
buttons.forEach((btn) => {
|
||||
@@ -182,6 +193,11 @@ class PuterMenubar extends PuterWebComponent {
|
||||
}
|
||||
};
|
||||
document.addEventListener('pointerdown', this._docPointerDownHandler, true);
|
||||
|
||||
// Once the user actually moves the mouse, exit keyboard-nav mode so
|
||||
// :hover styling on menubar buttons works normally again.
|
||||
this._mouseMoveHandler = () => this._setKeyboardNav(false);
|
||||
document.addEventListener('mousemove', this._mouseMoveHandler);
|
||||
}
|
||||
|
||||
_onGlobalKeyDown (e) {
|
||||
@@ -258,12 +274,14 @@ class PuterMenubar extends PuterWebComponent {
|
||||
this.#menubarActive = true;
|
||||
this.#focusedIndex = 0;
|
||||
this._renderButtonFocus();
|
||||
this._setKeyboardNav(true);
|
||||
}
|
||||
|
||||
_deactivateMenubar () {
|
||||
this.#menubarActive = false;
|
||||
this.#focusedIndex = null;
|
||||
this._renderButtonFocus();
|
||||
this._setKeyboardNav(false);
|
||||
}
|
||||
|
||||
_renderButtonFocus () {
|
||||
@@ -273,6 +291,11 @@ class PuterMenubar extends PuterWebComponent {
|
||||
});
|
||||
}
|
||||
|
||||
_setKeyboardNav (on) {
|
||||
const menubar = this.$('.menubar');
|
||||
if ( menubar ) menubar.classList.toggle('keyboard-nav', on);
|
||||
}
|
||||
|
||||
_moveButtonFocus (delta, { swapDropdown = true } = {}) {
|
||||
if ( ! this.#items.length ) return;
|
||||
const n = this.#items.length;
|
||||
@@ -280,6 +303,7 @@ class PuterMenubar extends PuterWebComponent {
|
||||
const next = (cur + delta + n) % n;
|
||||
this.#focusedIndex = next;
|
||||
this._renderButtonFocus();
|
||||
this._setKeyboardNav(true);
|
||||
|
||||
// If a dropdown is already open, swap to the new button's dropdown
|
||||
if ( swapDropdown && this.#activeDropdown ) {
|
||||
@@ -348,12 +372,23 @@ class PuterMenubar extends PuterWebComponent {
|
||||
this._deactivateMenubar();
|
||||
}
|
||||
});
|
||||
// Keyboard navigate request bubbling from the context menu
|
||||
// Keyboard navigate request bubbling from the context menu.
|
||||
// Arrow-left/right at the menubar level closes the current dropdown
|
||||
// and moves button focus only — the user must press ArrowDown (or
|
||||
// Enter/Space) to open the adjacent dropdown.
|
||||
// Arrow-up at the dropdown's first item closes it and returns
|
||||
// focus to the same menubar button (which can re-open with ArrowDown).
|
||||
dropdown.addEventListener('puter-menu-navigate', (e) => {
|
||||
if ( ! e.detail ) return;
|
||||
if ( e.detail.direction === 'up' ) {
|
||||
this._closeDropdown();
|
||||
this._renderButtonFocus();
|
||||
this._setKeyboardNav(true);
|
||||
return;
|
||||
}
|
||||
const delta = e.detail.direction === 'right' ? +1 : -1;
|
||||
this._closeDropdown();
|
||||
this._moveButtonFocus(delta, { swapDropdown: false });
|
||||
this._openFocusedButton(true);
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
@@ -387,6 +422,9 @@ class PuterMenubar extends PuterWebComponent {
|
||||
if ( this._docPointerDownHandler ) {
|
||||
document.removeEventListener('pointerdown', this._docPointerDownHandler, true);
|
||||
}
|
||||
if ( this._mouseMoveHandler ) {
|
||||
document.removeEventListener('mousemove', this._mouseMoveHandler);
|
||||
}
|
||||
}
|
||||
|
||||
_escapeHTML (str) {
|
||||
|
||||
Reference in New Issue
Block a user