From f1c066db6eaba4b2e76d9ffaf104579de365671d Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 19 Aug 2026 13:18:55 -0400 Subject: [PATCH] fix(page-header): collapse the actions that do not fit into a menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playlists slotted three buttons totalling 390px into a header that gets 700px at 900x600, so "New Smart Playlist" rendered 114 of its 162px with the queue closed, and 158 of 162 at the 800x600 enforced minimum. On a phone none of the three could be reached at all, which is what the Android report said. Plan 018's size matrix promises the opposite: no action is ever unreachable at any supported size. The header could not fix that for slotted markup, and that is a fact about the API rather than an effort estimate — a component cannot move another component's light-DOM children into a dropdown and keep their behaviour, and arbitrary markup offers nothing generic to render as a menu item. So a host passes `PageAction[]` and the header chooses the rendering; the slot survives for markup a data list cannot express, at the stated cost that a slotted action does not collapse. All three hosts that slot actions migrated, which also normalises the plain- + + + + `; + } + + private renderActionButton(a: PageAction) { + const drop = a.drop; + + return html` + + `; + } + + // ================================================================= + // The overflow menu + // ================================================================= + + private onActionSelect(a: PageAction): void { + if (a.disabled === true) return; + + this.closeMenu(); + a.onSelect(); + } + + private onMoreClick = (): void => { + if (this.menuOpen) { + this.closeMenu(); + + return; + } + + this.menuOpen = true; + + void this.updateComplete.then(() => { + if (!this.menuOpen) return; + + this.popup?.reposition(); + this.menuKeyboard.open(this.menuPanel ?? null, this.moreButton); + this.attachOutsideClose(); + }); + }; + + private closeMenu(): void { + if (!this.menuOpen) return; + + this.detachOutsideClose(); + this.menuKeyboard.close(); + this.menuOpen = false; + } + + /** + * A click anywhere else closes it. `composedPath` rather than + * `contains`, because the trigger and the panel are both inside + * this shadow root and a click retargets at the host. + */ + private onOutsideDown = (e: Event): void => { + if (e.composedPath().includes(this.menuPanel as EventTarget)) return; + if (e.composedPath().includes(this.moreButton as EventTarget)) return; + + this.closeMenu(); + }; + + private attachOutsideClose(): void { + if (this.outsideCloseAttached) return; + + this.outsideCloseAttached = true; + document.addEventListener('mousedown', this.onOutsideDown, true); + } + + private detachOutsideClose(): void { + if (!this.outsideCloseAttached) return; + + this.outsideCloseAttached = false; + document.removeEventListener('mousedown', this.onOutsideDown, true); + } + private renderCount() { if (this.count === null) return nothing; @@ -258,7 +785,10 @@ export class PageHeader extends LitElement { if (this.sortOptions.length === 1) { return html`
- Sort: ${this.sortOptions[0]?.label} + Sort: ${this.sortOptions[0]?.label} ${this.renderDirectionButton(ascending)}
`; @@ -267,7 +797,7 @@ export class PageHeader extends LitElement { return html`