From e79abd43e36ac305f599e2eecd100306a83b9919 Mon Sep 17 00:00:00 2001 From: Miika Kuisma Date: Tue, 12 May 2026 12:00:15 +0300 Subject: [PATCH] PuterJS menubar web component improvements --- .../src/ui/components/PuterContextMenu.js | 8 +++- .../src/ui/components/PuterMenubar.js | 39 ++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/puter-js/src/ui/components/PuterContextMenu.js b/src/puter-js/src/ui/components/PuterContextMenu.js index 1f4442506..989cdcb7a 100644 --- a/src/puter-js/src/ui/components/PuterContextMenu.js +++ b/src/puter-js/src/ui/components/PuterContextMenu.js @@ -93,14 +93,17 @@ class PuterContextMenu extends PuterWebComponent { .menu-item:hover:not(.disabled):not(.divider) .check, .menu-item:hover:not(.disabled):not(.divider) .submenu-arrow, .menu-item:hover:not(.disabled):not(.divider) .label, + .menu-item:hover:not(.disabled):not(.divider) .shortcut, .menu-item.focused:not(.disabled):not(.divider) .icon, .menu-item.focused:not(.disabled):not(.divider) .check, .menu-item.focused:not(.disabled):not(.divider) .submenu-arrow, .menu-item.focused:not(.disabled):not(.divider) .label, + .menu-item.focused:not(.disabled):not(.divider) .shortcut, .menu-item.has-open-submenu .icon, .menu-item.has-open-submenu .check, .menu-item.has-open-submenu .submenu-arrow, - .menu-item.has-open-submenu .label { + .menu-item.has-open-submenu .label, + .menu-item.has-open-submenu .shortcut { color: white; } .menu-item:hover:not(.disabled):not(.divider) .icon svg, @@ -128,6 +131,9 @@ class PuterContextMenu extends PuterWebComponent { .context-menu.safe-traverse .menu-item:hover:not(.has-open-submenu):not(.focused):not(.disabled):not(.divider) .label { color: #333; } + .context-menu.safe-traverse .menu-item:hover:not(.has-open-submenu):not(.focused):not(.disabled):not(.divider) .shortcut { + color: #999; + } .context-menu.safe-traverse .menu-item:hover:not(.has-open-submenu):not(.focused):not(.disabled):not(.divider) .icon svg { filter: none; } diff --git a/src/puter-js/src/ui/components/PuterMenubar.js b/src/puter-js/src/ui/components/PuterMenubar.js index bb6843acd..2b9b8324f 100644 --- a/src/puter-js/src/ui/components/PuterMenubar.js +++ b/src/puter-js/src/ui/components/PuterMenubar.js @@ -105,6 +105,9 @@ class PuterMenubar extends PuterWebComponent { if ( this._keyUpHandler ) { document.removeEventListener('keyup', this._keyUpHandler, true); } + if ( this._docPointerDownHandler ) { + document.removeEventListener('pointerdown', this._docPointerDownHandler, true); + } const buttons = this.$$('.menu-button'); buttons.forEach((btn) => { @@ -132,19 +135,6 @@ class PuterMenubar extends PuterWebComponent { this._openDropdown(btn, item); }); - // pointerdown on this button while it owns the open dropdown: - // mark it so the outside-pointerdown close (about to fire) and - // the trailing click don't reopen. - btn.addEventListener('pointerdown', () => { - if ( this.#activeButtonEl === btn ) { - this._suppressClickFor = btn; - clearTimeout(this._suppressClickTimer); - this._suppressClickTimer = setTimeout(() => { - this._suppressClickFor = null; - }, 400); - } - }); - // Hover-switch when a dropdown is already open btn.addEventListener('mouseenter', () => { if ( this.#activeDropdown && this.#activeButtonEl !== btn ) { @@ -158,6 +148,26 @@ class PuterMenubar extends PuterWebComponent { this._keyUpHandler = (e) => this._onGlobalKeyUp(e); document.addEventListener('keydown', this._keyHandler, true); document.addEventListener('keyup', this._keyUpHandler, true); + + // Capture-phase pointerdown on document. The open context menu also + // listens in capture for outside-pointerdown to close itself; that + // listener registers later (when the dropdown opens) so ours runs + // first. If the press lands on the currently-active button, mark it + // so the trailing click — which would otherwise reopen the just-closed + // dropdown — is treated as a toggle-close. Uses composedPath because + // the button lives inside this shadow root. + this._docPointerDownHandler = (e) => { + if ( ! this.#activeButtonEl ) return; + const path = typeof e.composedPath === 'function' ? e.composedPath() : []; + if ( path.includes(this.#activeButtonEl) ) { + this._suppressClickFor = this.#activeButtonEl; + clearTimeout(this._suppressClickTimer); + this._suppressClickTimer = setTimeout(() => { + this._suppressClickFor = null; + }, 400); + } + }; + document.addEventListener('pointerdown', this._docPointerDownHandler, true); } _onGlobalKeyDown (e) { @@ -357,6 +367,9 @@ class PuterMenubar extends PuterWebComponent { if ( this._keyUpHandler ) { document.removeEventListener('keyup', this._keyUpHandler, true); } + if ( this._docPointerDownHandler ) { + document.removeEventListener('pointerdown', this._docPointerDownHandler, true); + } } _escapeHTML (str) {