Files
yellowjacket/frontend/src/utils/go-to-menu.ts
T
logan 8de412cf36
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 3m5s
CI / e2e (pull_request) Successful in 10m3s
feat(android): a name is not a link on a phone, the menu carries it
Every track, album and artist name in the app navigates through
`utils/explore-link.ts`, and every sentence of how it does that is a
desktop compromise: the navigation is held for one double-click
interval so double-clicking the row can still play it, and the target
is a few characters of text inside a row. On touch that is a delay on
an ambiguous target, and since #63 the row's own tap claims the click
anyway -- so the link was unreachable as well as fiddly.

So below the phone breakpoint a name renders as plain text and the
row's context menu carries the destination instead: `go-to-menu.ts`
draws "Go to Artist" / "Go to Album" under exactly the condition the
link is not, using `explore-link`'s own exported routing so an untagged
entity reaches the library page by the same lookup.

Three things this leans on. Suppressing a link with no menu behind it
is not a smaller affordance but a destination the phone cannot reach,
so `keepOnPhone` is the exception for the three surfaces with no row
menu. The items are drawn for a single selection only, which is the
Play item's rule one step on. And there is no "Go to Genre", because
no row renders a genre link to lose -- that would be new navigation
rather than a replacement.

Closes #67
2026-08-25 12:40:10 -04:00

110 lines
3.9 KiB
TypeScript

/**
* "Go to Artist" / "Go to Album", for the menus that carry a name the
* phone stopped drawing as a link (#67).
*
* `utils/explore-link.ts` renders a plain string below the phone
* breakpoint, because a few characters inside a row is not a touch
* target and the row's own tap already means "play". That takes a
* destination away, so the row's context menu gives it back — which is
* the whole of this issue: the navigation moves, it does not go.
*
* Three things about it are load-bearing.
*
* **It is drawn under exactly the condition the link is not.**
* `inlineLinksSuppressed()` answers both, so a desktop menu is
* untouched (the name beside it is still a link, and a menu that
* repeats what the row already offers is furniture) and a phone menu
* cannot be missing what the row lost.
*
* **It goes where the name went.** `openArtistPage` / `openAlbumPage`
* are `explore-link`'s own routing, exported rather than reimplemented,
* so an untagged artist reaches the library page here for the same
* reason and by the same lookup it does from a link.
*
* **The host says when it is over**, through `onSelect` — every menu in
* this app closes itself and most clear their selection, and both are
* the host's bookkeeping rather than something a shared item may do on
* its behalf. `onHover` is for the four hosts with a playlist submenu,
* which closes on any other item being pointed at.
*/
import { html, nothing } from 'lit';
import type { TemplateResult } from 'lit';
import { inlineLinksSuppressed, openArtistPage, openAlbumPage } from './explore-link';
/**
* The entities one row or card can send you to.
*
* Everything is optional because the hosts differ: a track row knows
* both, an album card knows only its artist, and an artist page's own
* tracklist knows only the album.
*/
export interface GoToTarget {
artistName?: string;
artistMBID?: string;
albumName?: string;
albumMBID?: string;
}
export interface GoToHandlers {
/** Called before navigating: close the menu, clear the selection. */
onSelect?: () => void;
/** Called on hover: close a playlist submenu, where the host has one. */
onHover?: () => void;
}
/**
* The menu items for a target, or nothing at all where the name beside
* them is still a link.
*/
export function goToMenuItems(
target: GoToTarget | undefined,
handlers: GoToHandlers = {},
): TemplateResult | typeof nothing {
if (!target || !inlineLinksSuppressed()) return nothing;
const artist = target.artistName?.trim();
const album = target.albumName?.trim();
if (!artist && !album) return nothing;
return html`
${artist
? html`<wa-dropdown-item
data-testid="go-to-artist"
@click=${(e: Event) => {
handlers.onSelect?.();
void openArtistPage(
e.currentTarget as EventTarget,
artist,
target.artistMBID ?? '',
);
}}
@mouseenter=${() => handlers.onHover?.()}
>
<wa-icon slot="icon" name="user-group"></wa-icon>
Go to Artist
</wa-dropdown-item>`
: nothing}
${album
? html`<wa-dropdown-item
data-testid="go-to-album"
@click=${(e: Event) => {
handlers.onSelect?.();
void openAlbumPage(
e.currentTarget as EventTarget,
album,
target.albumMBID ?? '',
artist,
);
}}
@mouseenter=${() => handlers.onHover?.()}
>
<wa-icon slot="icon" name="compact-disc"></wa-icon>
Go to Album
</wa-dropdown-item>`
: nothing}
`;
}