Skip to content

refactor(webapp): one menu for cards and tracks, the media query fallback everywhere, steadier tests - #350

Merged
InstaZDLL merged 3 commits into
mainfrom
refactor/one-menu
Oct 26, 2026
Merged

InstaZDLL merged 3 commits into
mainfrom
refactor/one-menu

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #347, #348 and #349, settling three things they left behind.

One menu instead of two

#347 moved the track menu's panel into PopupMenu, and #348 wrote ActionMenu, which had its own copy of the same panel: the ARIA menu keys, the outside-close listeners and the placement. ActionMenu is now just the "⋯" button. The menu itself is PopupMenu, which gains:

  • an optional icon, so the card menu draws text-only entries;
  • danger entries, drawn in the danger colour;
  • an id for aria-controls, and an extra className;
  • above: a menu opened near the bottom of the window hangs upward from its button instead of being pushed up over it. This is what ActionMenu already did.

onClose(refocus). It now tells the caller whether focus goes back to the opener: yes for Escape, Tab or a chosen entry, no for a press, a wheel or a gesture elsewhere. The track menu and the artist edit menu ignore the argument and behave exactly as before.

Same Tab behaviour everywhere. The Tab handling #348 fixed after review (focus back to the button, then the Tab runs from there) now applies to every menu.

The card menu's entries still close it before they run, so the "Edit" dialog finds focus on the button to hand back. The card menu also picks up the track menu's finer outside-close rules: a long-press drift doesn't close it, and the wheel over a scrollable menu doesn't either.

Placement is a pure function. It now lives in placeMenu (webapp/src/lib/menu-place.ts), which has its own tests.

onMediaChange everywhere

#349 added a fallback for a MediaQueryList that only has addListener (Safari before 14), where calling addEventListener throws inside an effect. Two other hooks still called addEventListener directly. Both now go through the fallback:

Steadier tests

  • "subscribes to a podcast…" read the route's writes straight after a click, before the route had seen them. The three write checks are now polled.
  • "lists what needs attention, splits a credit…": the split confirmation stays armed for four seconds, and on a loaded machine the accessibility scan alone can take longer. If it lapsed, the test arms it again before clicking.
  • "tries a failed station again from the bar" is not changed. It failed once with a click timeout during a full run on merged main. Since then it passed 16/16 alone, 48/48 run 8 workers wide together with the split test, and in two more full runs with traces kept. Without a trace there was no evidence to fix it on, and CI retries twice.

Tests

  • New vitest menu-place.test.ts: the point, align: "end", clamping at every edge, hanging above the button, and staying below when it fits. Disabling the above-rule makes "hangs a menu above its button" fail.
  • Biome passes, with the same 10 warnings as main. Typecheck, vitest (323) and build pass.
  • Full Playwright run: 477 passed, 31 skipped, 0 failed. The existing radio menu tests, including the Tab test, the track menu tests and the artist edit menu tests, all pass unchanged.

Summary by CodeRabbit

  • Améliorations
    • Les menus contextuels s’ouvrent désormais à une position adaptée à l’écran, notamment au-dessus du bouton si l’espace manque en dessous.
    • La navigation au clavier et le retour du focus après une action sont mieux pris en charge.
  • Tests
    • Les tests attendent désormais que les enregistrements d’abonnement, de téléchargement et d’actualisation soient visibles avant de les vérifier.
    • Le test de décision de crédit relance la confirmation si elle a expiré avant le clic.

ActionMenu (#348) carried its own copy of the panel, its keys and its outside-close listeners beside PopupMenu (#347). It is now a button over PopupMenu, which gains an optional icon, a danger entry, an id, a class, and a placement above its button when there is no room below. The placement is a pure function with its own tests. onClose now says whether focus goes back to the opener: yes for a key or an entry, no for a press elsewhere.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
The reduced-motion hook and the podcasts two-panel hook called addEventListener on a MediaQueryList directly, which throws on a Safari older than 14. Both now go through the fallback #349 added for the theme.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
…missed

The podcast test read the route's writes straight after a click, before the route had seen them, so they are now polled. The split test's accessibility scan can outlast the four seconds the confirmation stands on a loaded machine, so it is armed again if it lapsed.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added scope: web Embedded web player (React) scope: components Web UI components type: refactor Code refactoring labels Oct 26, 2026
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 77f9445 and e6096cb.

Files selected for processing: 8
  • webapp/e2e/smart-playlists-and-podcasts.spec.ts
  • webapp/e2e/studio-nocturne.spec.ts
  • webapp/src/components/action-menu.tsx
  • webapp/src/components/canvas.tsx
  • webapp/src/components/popup-menu.tsx
  • webapp/src/lib/menu-place.test.ts
  • webapp/src/lib/menu-place.ts
  • webapp/src/pages/podcasts-page.tsx

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Walkthrough

Le changement modifie le placement et la fermeture des menus contextuels, remplace deux abonnements aux requêtes média par onMediaChange et ajuste des attentes dans deux tests E2E.

Changes: Menus contextuels

Layer / File(s) Summary
Calcul et tests du placement
webapp/src/lib/menu-place.ts, webapp/src/lib/menu-place.test.ts
placeMenu calcule les coordonnées selon l’alignement, l’espace disponible et les limites de la fenêtre. Les tests couvrent ces cas de placement.
Contrat et comportement de PopupMenu
webapp/src/components/popup-menu.tsx
PopupMenu utilise placeMenu, accepte des options de présentation et transmet un booléen lors de la fermeture pour indiquer si le focus revient à l’élément d’ouverture.
Intégration de ActionMenu
webapp/src/components/action-menu.tsx
ActionMenu remplace son panneau local par PopupMenu. Lorsqu’une entrée est choisie, le menu rend le focus au bouton avant d’exécuter l’action.

Abonnements aux requêtes média

Layer / File(s) Summary
Utilisation de onMediaChange
webapp/src/components/canvas.tsx, webapp/src/pages/podcasts-page.tsx
Les deux composants remplacent la gestion directe de l’événement change par onMediaChange.

Test E2E des podcasts

Layer / File(s) Summary
Attente des écritures du podcast
webapp/e2e/smart-playlists-and-podcasts.spec.ts
Le test attend que podcastWrites contienne les écritures attendues après l’abonnement, le téléchargement et l’actualisation.

Test E2E de Studio Nocturne

Layer / File(s) Summary
Réarmement de la confirmation Split
webapp/e2e/studio-nocturne.spec.ts
Si le bouton « Split » n’est plus visible avant la confirmation finale, le test clique de nouveau dessus.

Priority: Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant Bouton
  participant ActionMenu
  participant PopupMenu
  participant placeMenu
  Bouton->>ActionMenu: clic
  ActionMenu->>PopupMenu: transmet les coordonnées et les entrées
  PopupMenu->>placeMenu: demande les coordonnées finales
  placeMenu-->>PopupMenu: retourne left et top
  PopupMenu-->>ActionMenu: sélection d’une entrée
  ActionMenu-->>Bouton: rend le focus puis exécute l’action
Loading

Suggested labels: scope: web, scope: components

Merge Risk: Minimal · up to e6096

No actionable issue remains from this review; the PR is mergeable after normal checks.

Security architecture risk: Minimal · up to e6096

The shared-menu refactor preserves existing action callbacks and dialog handoffs without introducing new privileges or weakening an identified control. The remaining production changes reuse an existing browser-listener compatibility helper.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed shared contract affects card, track, and artist-image menu interactions within the browser. The traced integration adds no new action callback source or privilege to those consumers.

Trust Boundaries and Controls

  • inferred — The changed public interface is a UI component contract, not a new server endpoint. Browser events still select existing caller-defined actions; placement and focus properties do not grant authority or replace authorization enforcement.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed Le titre décrit clairement les trois changements principaux : consolidation des menus, utilisation de onMediaChange et stabilisation des tests. Il reste concis et pertinent.
Description check Passed La description est détaillée et couvre le résumé, les changements, les tests et les points d’attention. Elle n’utilise pas les sections exactes du modèle et ne contient pas les cases à cocher ni la co…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

La description est détaillée et couvre le résumé, les changements, les tests et les points d’attention. Elle n’utilise pas les sections exactes du modèle et ne contient pas les cases à cocher ni la confirmation DCO, mais le contenu requis reste majoritairement présent.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 Biome (2.5.15)
webapp/e2e/smart-playlists-and-podcasts.spec.ts

Biome could not lint this file: nested root configuration. Check the repository's Biome configuration and plugins.

webapp/e2e/studio-nocturne.spec.ts

Biome could not lint this file: nested root configuration. Check the repository's Biome configuration and plugins.

webapp/src/components/action-menu.tsx

Biome could not lint this file: nested root configuration. Check the repository's Biome configuration and plugins.

  • 5 others

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size: m 50-200 lines type: refactor Code refactoring and removed type: refactor Code refactoring labels Oct 26, 2026
@InstaZDLL InstaZDLL self-assigned this Oct 26, 2026
@InstaZDLL
InstaZDLL merged commit 7d00fb4 into main Oct 26, 2026
14 of 16 checks passed
@InstaZDLL
InstaZDLL deleted the refactor/one-menu branch October 26, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: components Web UI components scope: web Embedded web player (React) size: m 50-200 lines type: refactor Code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant