From f63c7d81420132cbeb13939499b2b16ffc620ba8 Mon Sep 17 00:00:00 2001 From: Miika Kuisma Date: Wed, 13 May 2026 09:27:44 +0300 Subject: [PATCH] - Fixes https://github.com/HeyPuter/puter/issues/3100 - Also fixes similar issues with navigating top level menubar items and submenus --- .../src/ui/components/PuterContextMenu.js | 32 +++++++++++--- .../src/ui/components/PuterMenubar.js | 42 ++++++++++++++++++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/puter-js/src/ui/components/PuterContextMenu.js b/src/puter-js/src/ui/components/PuterContextMenu.js index faee57e6c..6ebd29ad9 100644 --- a/src/puter-js/src/ui/components/PuterContextMenu.js +++ b/src/puter-js/src/ui/components/PuterContextMenu.js @@ -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); } }); diff --git a/src/puter-js/src/ui/components/PuterMenubar.js b/src/puter-js/src/ui/components/PuterMenubar.js index 7f07ffe9f..6997f10cc 100644 --- a/src/puter-js/src/ui/components/PuterMenubar.js +++ b/src/puter-js/src/ui/components/PuterMenubar.js @@ -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) {