feat(shell): put every menu in the app through the one surface

Fourteen call sites, one tag name each and nothing else -- which is what
menu-surface's shape buys: the host's panel is slotted into whichever
presentation is up, so no item model, no keyboard model and no styling
moved.  The 48px rows come from contextMenuStyles, the one stylesheet
every one of these hosts already includes, because the panel is the
host's own light DOM and only the host's stylesheet can reach it.

Two of the fourteen were found by the source sweep rather than by the
conversion: queue-panel's add-to-playlist popup, which is a real menu.
now-playing's cover preview is allowlisted instead -- it is a hover
affordance in the bottom bar, so a touch device never opens it and
nothing clips it.

The playlist submenu had to come too, and that is the one place this
change made something worse before it made it better.  It is a
placement="right-start" flyout anchored to its row, and making the menu
full-width moved that anchor to x=0 -- so the flip put the picker at
x -182 to 0, entirely off-screen, and "Add to Playlist" led nowhere at
all.  Before the change the row started at x~245 and the same flip
landed on screen.  It is a sheet now and stacks over the first, which
is also why menu-shown does not re-assert focus while it is open.

The three hosts that do not use ContextMenuController -- page-header's
overflow menu, playlist-view's hand-rolled menu, queue-panel's picker
-- bind menu-dismiss themselves, or Escape would close the sheet and
leave their own open flag set.

page-header is included deliberately: the clipping does not bite there,
since it opens downward from the top of a full-height view, but on a
phone every action of an overflowing page lives in that menu at
wa-dropdown-item defaults.  One surface, so there is no second answer
to what a menu looks like.
This commit is contained in:
2026-08-21 02:23:48 -04:00
parent 9aaa8beb99
commit 9e7e7ce5a1
12 changed files with 145 additions and 162 deletions
@@ -26,14 +26,15 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { FavoritesController } from '@store/controllers/favorites-controller'; import { FavoritesController } from '@store/controllers/favorites-controller';
import { ViewLifecycleMixin } from '@utils/view-lifecycle'; import { ViewLifecycleMixin } from '@utils/view-lifecycle';
import { RovingGridController } from '@utils/roving-grid'; import { RovingGridController } from '@utils/roving-grid';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@components/playlist-picker/playlist-picker.js'; import '@components/playlist-picker/playlist-picker.js';
import { dict, list } from '@utils/binding'; import { dict, list } from '@utils/binding';
@@ -131,17 +132,17 @@ export class ArtistsView
private contextMenuArtistId: number | null = null; private contextMenuArtistId: number | null = null;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): getPlaylistSubmenuPopup():
| WaPopup | MenuTarget
| undefined { | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -1336,11 +1337,8 @@ export class ArtistsView
private renderContextMenu() { private renderContextMenu() {
return html` return html`
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.contextMenuOpen} .contextMenuOpen}
> >
@@ -1434,13 +1432,12 @@ export class ArtistsView
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.playlistSubmenuOpen} .playlistSubmenuOpen}
> >
@@ -1468,7 +1465,7 @@ export class ArtistsView
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
@@ -23,7 +23,8 @@ import { gridColumnsFor, gridSpacingFor } from '@utils/grid-spacing';
import { queueStore } from '@store/queue-store'; import { queueStore } from '@store/queue-store';
import type { QueueSource } from '@store/queue-store'; import type { QueueSource } from '@store/queue-store';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@components/playlist-picker/playlist-picker.js'; import '@components/playlist-picker/playlist-picker.js';
@@ -51,7 +52,7 @@ import {
ContextMenuController, ContextMenuController,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { FavoritesController } from '@store/controllers/favorites-controller'; import { FavoritesController } from '@store/controllers/favorites-controller';
import { creditLink, exploreLinkStyles } from '../../utils/explore-link'; import { creditLink, exploreLinkStyles } from '../../utils/explore-link';
import { creditStore } from '@store/credit-store'; import { creditStore } from '@store/credit-store';
@@ -415,17 +416,17 @@ export class CoverGrid
splitIndex = 0; splitIndex = 0;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
// ContextMenuHost interface. // ContextMenuHost interface.
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -2087,11 +2088,8 @@ export class CoverGrid
const { ctxMenu } = this; const { ctxMenu } = this;
return html` return html`
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${ctxMenu.contextMenuOpen} .active=${ctxMenu.contextMenuOpen}
> >
${ctxMenu.contextMenuOpen ${ctxMenu.contextMenuOpen
@@ -2204,13 +2202,12 @@ export class CoverGrid
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${ctxMenu.playlistSubmenuOpen} .active=${ctxMenu.playlistSubmenuOpen}
> >
${ctxMenu.playlistSubmenuOpen ${ctxMenu.playlistSubmenuOpen
@@ -2232,7 +2229,7 @@ export class CoverGrid
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<track-details></track-details> <track-details></track-details>
`; `;
@@ -49,9 +49,11 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import { dictByName } from '@utils/binding'; import { dictByName } from '@utils/binding';
import type { TrackDetails } from '@components/track-details/track-details.js'; import type { TrackDetails } from '@components/track-details/track-details.js';
@@ -327,7 +329,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
@state() private ctxMenuTrack: MBTrack | null = null; @state() private ctxMenuTrack: MBTrack | null = null;
@query('#track-context-menu') @query('#track-context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup?: WaPopup; private playlistSubmenuPopup?: WaPopup;
@@ -337,11 +339,11 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
// -- ContextMenuHost interface -- // -- ContextMenuHost interface --
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -3781,11 +3783,8 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
const track = this.ctxMenuTrack; const track = this.ctxMenuTrack;
return html` return html`
<wa-popup <menu-surface
id="track-context-menu" id="track-context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu.contextMenuOpen} .active=${this.ctxMenu.contextMenuOpen}
> >
${this.ctxMenu.contextMenuOpen && track ${this.ctxMenu.contextMenuOpen && track
@@ -3846,13 +3845,12 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu.playlistSubmenuOpen} .active=${this.ctxMenu.playlistSubmenuOpen}
> >
${this.ctxMenu.playlistSubmenuOpen ${this.ctxMenu.playlistSubmenuOpen
@@ -3869,7 +3867,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
} }
@@ -61,9 +61,11 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import { dict, dictByName } from '@utils/binding'; import { dict, dictByName } from '@utils/binding';
import type { TrackDetails } from '@components/track-details/track-details.js'; import type { TrackDetails } from '@components/track-details/track-details.js';
@@ -215,7 +217,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
@state() private ctxMenuTarget: ContextMenuTarget | null = null; @state() private ctxMenuTarget: ContextMenuTarget | null = null;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup?: WaPopup; private playlistSubmenuPopup?: WaPopup;
@@ -233,11 +235,11 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
// -- ContextMenuHost interface -- // -- ContextMenuHost interface --
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -2621,11 +2623,8 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
const target = this.ctxMenuTarget; const target = this.ctxMenuTarget;
return html` return html`
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu.contextMenuOpen} .active=${this.ctxMenu.contextMenuOpen}
> >
${this.ctxMenu.contextMenuOpen && target ${this.ctxMenu.contextMenuOpen && target
@@ -2643,13 +2642,12 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu.playlistSubmenuOpen} .active=${this.ctxMenu.playlistSubmenuOpen}
> >
${this.ctxMenu.playlistSubmenuOpen ${this.ctxMenu.playlistSubmenuOpen
@@ -2666,7 +2664,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
@@ -37,9 +37,11 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import { dict, dictByName } from '@utils/binding'; import { dict, dictByName } from '@utils/binding';
import { ICON_QUEUE } from '@utils/icon-language'; import { ICON_QUEUE } from '@utils/icon-language';
@@ -206,13 +208,13 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
@state() private ctxMenuTarget: ExploreMenuTarget | null = null; @state() private ctxMenuTarget: ExploreMenuTarget | null = null;
@litQuery('#explore-context-menu') @litQuery('#explore-context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
// -- ContextMenuHost interface -- // -- ContextMenuHost interface --
// No playlist submenu — same reason as the album/artist detail // No playlist submenu — same reason as the album/artist detail
// pages: every action here resolves its one file lazily. // pages: every action here resolves its one file lazily.
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
@@ -1341,11 +1343,8 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
const owned = Boolean(target?.localId); const owned = Boolean(target?.localId);
return html` return html`
<wa-popup <menu-surface
id="explore-context-menu" id="explore-context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu.contextMenuOpen} .active=${this.ctxMenu.contextMenuOpen}
> >
${this.ctxMenu.contextMenuOpen && target ${this.ctxMenu.contextMenuOpen && target
@@ -1374,7 +1373,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
@@ -24,14 +24,15 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { FavoritesController } from '@store/controllers/favorites-controller'; import { FavoritesController } from '@store/controllers/favorites-controller';
import { ViewLifecycleMixin } from '@utils/view-lifecycle'; import { ViewLifecycleMixin } from '@utils/view-lifecycle';
import { RovingGridController } from '@utils/roving-grid'; import { RovingGridController } from '@utils/roving-grid';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@components/playlist-picker/playlist-picker.js'; import '@components/playlist-picker/playlist-picker.js';
import { dictByName } from '@utils/binding'; import { dictByName } from '@utils/binding';
@@ -137,19 +138,19 @@ export class GenresView
private contextMenuGenreName: string | null = null; private contextMenuGenreName: string | null = null;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
// ----- ContextMenuHost interface ----- // ----- ContextMenuHost interface -----
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): getPlaylistSubmenuPopup():
| WaPopup | MenuTarget
| undefined { | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -1176,11 +1177,8 @@ export class GenresView
private renderContextMenu() { private renderContextMenu() {
return html` return html`
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.contextMenuOpen} .contextMenuOpen}
> >
@@ -1284,13 +1282,12 @@ export class GenresView
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.playlistSubmenuOpen} .playlistSubmenuOpen}
> >
@@ -1318,7 +1315,7 @@ export class GenresView
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
@@ -1,9 +1,9 @@
import { LitElement, html, css, nothing } from 'lit'; import { LitElement, html, css, nothing } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js'; import { customElement, property, query, state } from 'lit/decorators.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import { designTokens } from '../../styles/tokens.css'; import { designTokens } from '../../styles/tokens.css';
import { import {
@@ -183,8 +183,8 @@ export class PageHeader extends LitElement {
@query('#page-header-overflow') @query('#page-header-overflow')
private menuPanel?: HTMLElement; private menuPanel?: HTMLElement;
@query('wa-popup') @query('menu-surface')
private popup?: WaPopup; private popup?: MenuSurface;
private menuKeyboard = new MenuKeyboard(() => this.closeMenu()); private menuKeyboard = new MenuKeyboard(() => this.closeMenu());
@@ -418,7 +418,7 @@ export class PageHeader extends LitElement {
outline-offset: -1px; outline-offset: -1px;
} }
wa-popup { menu-surface {
z-index: 200; z-index: 200;
} }
@@ -670,10 +670,17 @@ export class PageHeader extends LitElement {
return html` return html`
<div class="actions"> <div class="actions">
${this.actions.map((a) => this.renderActionButton(a))} ${this.actions.map((a) => this.renderActionButton(a))}
<wa-popup <!-- A sheet below 600px, like every other menu in the
app (#60). The clipping that issue is about does
not bite here — this one opens downward from the
top of a full-height view, so it has somewhere to
go even without top-layer promotion — but the touch
targets do: on a phone *every* action of a page
that overflows lives in here, at wa-dropdown-item
defaults. One surface, so there is no second
answer to what a menu looks like. -->
<menu-surface
placement="bottom-end" placement="bottom-end"
flip
shift
.active=${this.menuOpen} .active=${this.menuOpen}
> >
<button <button
@@ -711,7 +718,7 @@ export class PageHeader extends LitElement {
`, `,
)} )}
</div> </div>
</wa-popup> </menu-surface>
</div> </div>
`; `;
} }
@@ -7,7 +7,8 @@ import {
} from 'lit/decorators.js'; } from 'lit/decorators.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@lit-labs/virtualizer'; import '@lit-labs/virtualizer';
import type { LitVirtualizer } from '@lit-labs/virtualizer'; import type { LitVirtualizer } from '@lit-labs/virtualizer';
@@ -35,7 +36,7 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { focusRovingRow, nextRovingIndex } from '@utils/roving-rows'; import { focusRovingRow, nextRovingIndex } from '@utils/roving-rows';
import { FavoritesController } from '@store/controllers/favorites-controller'; import { FavoritesController } from '@store/controllers/favorites-controller';
import { notificationStore } from '@store/notification-store'; import { notificationStore } from '@store/notification-store';
@@ -145,10 +146,10 @@ export class PlaylistDetails
private dragImageEl: HTMLElement | null = null; private dragImageEl: HTMLElement | null = null;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
@query('track-details') @query('track-details')
private trackDetailsDialog!: TrackDetails; private trackDetailsDialog!: TrackDetails;
@@ -163,11 +164,11 @@ export class PlaylistDetails
// ContextMenuHost interface // ContextMenuHost interface
// ================================================================= // =================================================================
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -1617,11 +1618,8 @@ export class PlaylistDetails
private renderContextMenu() { private renderContextMenu() {
return html` return html`
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.contextMenuOpen} .contextMenuOpen}
> >
@@ -1783,13 +1781,12 @@ export class PlaylistDetails
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.playlistSubmenuOpen} .playlistSubmenuOpen}
> >
@@ -1816,7 +1813,7 @@ export class PlaylistDetails
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
} }
@@ -2,7 +2,9 @@ import { LitElement, html, css, nothing } from 'lit';
import { customElement, state, query } from 'lit/decorators.js'; import { customElement, state, query } from 'lit/decorators.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import { import {
@@ -140,7 +142,7 @@ export class PlaylistView extends ViewLifecycleMixin(LitElement) {
private pendingDropPaths: string[] = []; private pendingDropPaths: string[] = [];
@query('#playlist-context-menu') @query('#playlist-context-menu')
private playlistContextMenuPopup!: WaPopup; private playlistContextMenuPopup!: MenuSurface;
@query('duplicate-tracks-dialog') @query('duplicate-tracks-dialog')
private duplicateDialog!: DuplicateTracksDialog; private duplicateDialog!: DuplicateTracksDialog;
@@ -1059,7 +1061,7 @@ export class PlaylistView extends ViewLifecycleMixin(LitElement) {
); );
} }
private closePlaylistContextMenu() { private closePlaylistContextMenu = () => {
if (!this.playlistContextMenuOpen) return; if (!this.playlistContextMenuOpen) return;
this.menuKeyboard.close(); this.menuKeyboard.close();
@@ -1072,7 +1074,7 @@ export class PlaylistView extends ViewLifecycleMixin(LitElement) {
if (popup) { if (popup) {
popup.active = false; popup.active = false;
} }
} };
private async onPlaylistContextAction( private async onPlaylistContextAction(
action: string, action: string,
@@ -1500,13 +1502,11 @@ export class PlaylistView extends ViewLifecycleMixin(LitElement) {
</div>` </div>`
: this.renderPlaylistList()} : this.renderPlaylistList()}
<wa-popup <menu-surface
id="playlist-context-menu" id="playlist-context-menu"
placement="bottom-start"
flip
shift
.active=${this .active=${this
.playlistContextMenuOpen} .playlistContextMenuOpen}
@menu-dismiss=${this.closePlaylistContextMenu}
> >
${this.playlistContextMenuOpen ${this.playlistContextMenuOpen
? html` ? html`
@@ -1564,7 +1564,7 @@ export class PlaylistView extends ViewLifecycleMixin(LitElement) {
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<duplicate-tracks-dialog <duplicate-tracks-dialog
@playlist-action-complete=${() => @playlist-action-complete=${() =>
@@ -9,7 +9,8 @@ import {
} from 'lit/decorators.js'; } from 'lit/decorators.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import { QueueController } from '@store/controllers/queue-controller'; import { QueueController } from '@store/controllers/queue-controller';
import { creditStore } from '@store/credit-store'; import { creditStore } from '@store/credit-store';
@@ -34,7 +35,7 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { focusRovingRow, nextRovingIndex } from '@utils/roving-rows'; import { focusRovingRow, nextRovingIndex } from '@utils/roving-rows';
import { FavoritesController } from '@store/controllers/favorites-controller'; import { FavoritesController } from '@store/controllers/favorites-controller';
import { import {
@@ -140,13 +141,13 @@ export class QueuePanel
private delegationAttached = false; private delegationAttached = false;
@query('#add-to-playlist-popup') @query('#add-to-playlist-popup')
private addToPlaylistPopup!: WaPopup; private addToPlaylistPopup!: MenuSurface;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
/** Unsubscribes the credit-arrival repaint. */ /** Unsubscribes the credit-arrival repaint. */
private creditsUnsub?: () => void; private creditsUnsub?: () => void;
@@ -305,11 +306,11 @@ export class QueuePanel
// ContextMenuHost interface // ContextMenuHost interface
// ================================================================= // =================================================================
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -1092,7 +1093,7 @@ export class QueuePanel
} }
} }
private closePlaylistPicker() { private closePlaylistPicker = () => {
if (!this.playlistPickerOpen) return; if (!this.playlistPickerOpen) return;
this.playlistPickerOpen = false; this.playlistPickerOpen = false;
@@ -1102,7 +1103,7 @@ export class QueuePanel
if (popup) { if (popup) {
popup.active = false; popup.active = false;
} }
} };
private onPlaylistActionComplete = () => { private onPlaylistActionComplete = () => {
this.closePlaylistPicker(); this.closePlaylistPicker();
@@ -2042,9 +2043,11 @@ export class QueuePanel
</div> </div>
</div> </div>
<wa-popup <menu-surface
id="add-to-playlist-popup" id="add-to-playlist-popup"
label="Add to playlist"
placement="bottom-end" placement="bottom-end"
@menu-dismiss=${this.closePlaylistPicker}
.active=${this.playlistPickerOpen} .active=${this.playlistPickerOpen}
> >
${this.playlistPickerOpen ${this.playlistPickerOpen
@@ -2060,7 +2063,7 @@ export class QueuePanel
></playlist-picker> ></playlist-picker>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<div <div
class="list-area" class="list-area"
@@ -2103,11 +2106,8 @@ export class QueuePanel
</div> </div>
</div> </div>
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu.contextMenuOpen} .active=${this.ctxMenu.contextMenuOpen}
> >
${this.ctxMenu.contextMenuOpen ${this.ctxMenu.contextMenuOpen
@@ -2195,13 +2195,12 @@ export class QueuePanel
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu.playlistSubmenuOpen} .active=${this.ctxMenu.playlistSubmenuOpen}
> >
${this.ctxMenu.playlistSubmenuOpen && ${this.ctxMenu.playlistSubmenuOpen &&
@@ -2224,7 +2223,7 @@ export class QueuePanel
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<track-details></track-details> <track-details></track-details>
`; `;
@@ -26,7 +26,7 @@ import {
contextMenuStyles, contextMenuStyles,
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { focusRovingRow, nextRovingIndex } from '@utils/roving-rows'; import { focusRovingRow, nextRovingIndex } from '@utils/roving-rows';
import { FavoritesController } from '@store/controllers/favorites-controller'; import { FavoritesController } from '@store/controllers/favorites-controller';
import { import {
@@ -40,7 +40,8 @@ import {
} from '@utils/drag-image'; } from '@utils/drag-image';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@lit-labs/virtualizer'; import '@lit-labs/virtualizer';
import type { LitVirtualizer } from '@lit-labs/virtualizer'; import type { LitVirtualizer } from '@lit-labs/virtualizer';
@@ -176,10 +177,10 @@ export class SmartPlaylistDetails
private dragImageEl: HTMLElement | null = null; private dragImageEl: HTMLElement | null = null;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
@query('track-details') @query('track-details')
private trackDetailsDialog!: TrackDetails; private trackDetailsDialog!: TrackDetails;
@@ -188,11 +189,11 @@ export class SmartPlaylistDetails
// ContextMenuHost interface // ContextMenuHost interface
// ================================================================= // =================================================================
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -1465,11 +1466,8 @@ export class SmartPlaylistDetails
private renderContextMenu() { private renderContextMenu() {
return html` return html`
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu.contextMenuOpen} .active=${this.ctxMenu.contextMenuOpen}
> >
${this.ctxMenu.contextMenuOpen ${this.ctxMenu.contextMenuOpen
@@ -1583,13 +1581,12 @@ export class SmartPlaylistDetails
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu .active=${this.ctxMenu
.playlistSubmenuOpen} .playlistSubmenuOpen}
> >
@@ -1616,7 +1613,7 @@ export class SmartPlaylistDetails
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
`; `;
} }
} }
@@ -18,7 +18,7 @@ import {
isContextMenuKey, isContextMenuKey,
} from '@utils/context-menu-controller.js'; } from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import type { ContextMenuHost, MenuTarget } from '@utils/context-menu-controller.js';
import { PlayerController } from '@store/controllers/player-controller'; import { PlayerController } from '@store/controllers/player-controller';
import { SearchController } from '@store/controllers/search-controller'; import { SearchController } from '@store/controllers/search-controller';
import '@components/page-header/page-header'; import '@components/page-header/page-header';
@@ -63,7 +63,8 @@ import type {
} from '@lit-labs/virtualizer'; } from '@lit-labs/virtualizer';
import { flow } from '@lit-labs/virtualizer/layouts/flow.js'; import { flow } from '@lit-labs/virtualizer/layouts/flow.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import type { MenuSurface } from '../menu-surface/menu-surface';
import '../menu-surface/menu-surface';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import { describeError } from '@utils/describe-error'; import { describeError } from '@utils/describe-error';
@@ -231,18 +232,18 @@ export class TrackList
private tracks: library.Track[] = []; private tracks: library.Track[] = [];
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: WaPopup; private contextMenuPopup!: MenuSurface;
@query('#playlist-submenu') @query('#playlist-submenu')
private playlistSubmenuPopup!: WaPopup; private playlistSubmenuPopup!: MenuSurface;
// -- ContextMenuHost interface -- // -- ContextMenuHost interface --
getContextMenuPopup(): WaPopup | undefined { getContextMenuPopup(): MenuTarget | undefined {
return this.contextMenuPopup; return this.contextMenuPopup;
} }
getPlaylistSubmenuPopup(): WaPopup | undefined { getPlaylistSubmenuPopup(): MenuTarget | undefined {
return this.playlistSubmenuPopup; return this.playlistSubmenuPopup;
} }
@@ -2323,11 +2324,8 @@ export class TrackList
</div> </div>
`} `}
<wa-popup <menu-surface
id="context-menu" id="context-menu"
placement="bottom-start"
flip
shift
.active=${this.ctxMenu.contextMenuOpen} .active=${this.ctxMenu.contextMenuOpen}
> >
${this.ctxMenu.contextMenuOpen ${this.ctxMenu.contextMenuOpen
@@ -2413,13 +2411,12 @@ export class TrackList
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<wa-popup <menu-surface
id="playlist-submenu" id="playlist-submenu"
label="Add to playlist"
placement="right-start" placement="right-start"
flip
shift
.active=${this.ctxMenu.playlistSubmenuOpen} .active=${this.ctxMenu.playlistSubmenuOpen}
> >
${this.ctxMenu.playlistSubmenuOpen && this.selection.hasSelection ${this.ctxMenu.playlistSubmenuOpen && this.selection.hasSelection
@@ -2437,7 +2434,7 @@ export class TrackList
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </menu-surface>
<track-details></track-details> <track-details></track-details>
`; `;