Skip to content

fix(library): stop popover clicks bubbling + clamp/flip portal placement#78

Merged
InstaZDLL merged 2 commits into
mainfrom
fix/popover-bubble-and-viewport-clamp
May 20, 2026
Merged

fix(library): stop popover clicks bubbling + clamp/flip portal placement#78
InstaZDLL merged 2 commits into
mainfrom
fix/popover-bubble-and-viewport-clamp

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 20, 2026

Summary

Follow-up to #77 — two issues caught by Qodo's review.

  1. React events bubble through portals. The popover container only stopped onDoubleClick, so picking a playlist row also fired the album / artist tile onClick underneath and navigated away mid-pick. The DOM was re-parented but React's synthetic event system still propagates through the React tree.
  2. Portal placement had no viewport bounds. rect.bottom + 4 / rect.right - width could push the popover off the bottom of the viewport (last visible row) or off the left edge (first-column trigger where rect.right < 224).

Fix

  • Stop onClick and onMouseDown (in addition to existing onDoubleClick) at the popover root. Outside-click detection still works — those handlers use document listeners with target.closest("[data-add-to-playlist-popover]") which short-circuits before stopPropagation runs.
  • Measure the popover via a second ResizeObserver; prefer placement below the trigger, flip above when below would clip and there's room, then clamp both axes with an 8 px viewport margin.

Test plan

  • Click + on an album card, then click a playlist row → track added, page does not navigate to the album.
  • Same on an artist tile.
  • Open + on a card in the first column → popover stays inside the viewport on the left edge.
  • Open + on a card near the bottom of the viewport → popover flips above the trigger when there's no room below.
  • Resize the window while the popover is open → position / flip updates live.

Summary by CodeRabbit

  • Bug Fixes
    • Amélioration du positionnement du popover "Ajouter à la playlist" pour éviter les débordements. Le popover se repositionne désormais automatiquement au-dessus du bouton si nécessaire et s'adapte correctement lors des changements de contenu.

Review Change Stack

Two follow-up issues caught in PR review:

1. React synthetic events still bubble through the React tree even when
   the popover is rendered through a portal. The container only stopped
   `onDoubleClick`, so clicking a playlist row also fired the album /
   artist tile `onClick` underneath and navigated away mid-pick. Stop
   `onClick` + `onMouseDown` at the popover root.

2. Portal placement used raw `rect.bottom + 4` / `rect.right - width`
   with no bounds checks. First-column trigger pushed the popover off
   the left edge; trigger near the viewport bottom clipped half the
   menu. Measure the popover via a second ResizeObserver, prefer below
   then flip above when below would clip, and clamp on both axes with
   an 8 px viewport margin.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1c89237-0934-446b-98ce-af6d1fb4a1c2

📥 Commits

Reviewing files that changed from the base of the PR and between 929f182 and 58befa8.

📒 Files selected for processing (1)
  • src/components/views/LibraryView.tsx

📝 Walkthrough

Walkthrough

Le composant AddToPlaylistPopover dans LibraryView.tsx améliore sa logique de positionnement pour éviter le clipping de contenu. Il mesure désormais la hauteur réelle du popover via ResizeObserver et ajuste automatiquement son placement en le basculant au-dessus du déclencheur si nécessaire.

Changes

Positionnement adaptatif du popover

Couche / Fichier(s) Résumé
État et logique de placement adaptatif du popover
src/components/views/LibraryView.tsx
État popoverHeight stocke la hauteur mesurée. useLayoutEffect avec ResizeObserver met à jour la hauteur en temps réel. La logique de placement bascule le popover au-dessus du déclencheur si le placement en bas provoquerait un clipping vertical, avec serrage horizontal et vertical. Le style du portail passe à position: fixed avec top/left basés sur placement, restant invisible tant que la position n'est pas déterminée.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#74: Corrige également le comportement du popover « Add to playlist » en ajustant le zIndex de la TrackTable, complétant l'amélioration du positionnement avec une gestion de l'empilement.

Suggested labels

size: s

Poem

Popover sage, mesure ta taille,
Grimpe ou descends, c'est du travail !
ResizeObserver veille avec soin,
Pas de clipping loin du coin 📍✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning La description couvre le résumé, les tests concrets et le plan de test, mais manque les sections 'How I tested' et 'Checklist' du template obligatoire. Compléter la description avec les sections 'How I tested' et 'Checklist' du template de dépôt pour respecter les prérequis standards.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit Conventional Commits avec scope kebab-case et décrit précisément les deux correctifs : arrêt de la propagation des clics du popover et positionnement dans les limites du viewport.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/popover-bubble-and-viewport-clamp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 20, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Remediation recommended

1. Forced reflow on scroll ✓ Resolved 🐞 Bug ➹ Performance
Description
The popover height measuring layout-effect depends on rect, so every scroll/resize rect update
reruns the effect and calls el.offsetHeight, creating repeated synchronous layout reads while the
popover is open. This can introduce scroll jank because rect updates are driven by a high-frequency
scroll listener.
Code

src/components/views/LibraryView.tsx[R1389-1398]

Evidence
rect is updated on every scroll/resize via update() and scroll listeners; because the popover
measurement effect depends on rect, each rect change reruns the effect and executes measure()
which reads el.offsetHeight (a synchronous layout read).

src/components/views/LibraryView.tsx[1372-1384]
src/components/views/LibraryView.tsx[1389-1398]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useLayoutEffect` that measures `popoverHeight` depends on `rect`, so it reruns on every scroll-driven `rect` update and reads `el.offsetHeight` repeatedly. `offsetHeight` is a synchronous layout read and can cause reflow/jank during scroll.

## Issue Context
`rect` is updated via a scroll listener (`window.addEventListener("scroll", update, true)`) and `update()` calls `setRect(anchorEl.getBoundingClientRect())`, so it can change many times per second while scrolling.

## Fix Focus Areas
- src/components/views/LibraryView.tsx[1372-1398]

## Suggested fix
- Change the popover measurement effect dependency array from `[anchorEl, rect]` to just `[anchorEl]` (or `[anchorEl, popoverRef]` pattern via a callback ref) so it sets up once per open.
- Keep the `ResizeObserver` on the popover element to update height when the popover content actually changes.
- Keep the initial `measure()` call inside the effect so `popoverHeight` is available immediately on mount, but avoid rerunning it on every `rect` change.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) type: fix Bug fix size: m 50-200 lines labels May 20, 2026
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 20, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 28 complexity · 0 duplication

Metric Results
Complexity 28
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@InstaZDLL InstaZDLL self-assigned this May 20, 2026
Qodo follow-up: the measurement `useLayoutEffect` depended on `rect`,
which is updated on every scroll tick. Each tick tore down + remounted
the ResizeObserver and forced a synchronous `offsetHeight` read, all
to recompute a value that hadn't actually changed. Depend on
`anchorEl` only — the ResizeObserver still catches real height
changes (locale wrap, content reflow).
@InstaZDLL InstaZDLL merged commit fecd401 into main May 20, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the fix/popover-bubble-and-viewport-clamp branch May 20, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: frontend React/Vite frontend (src/) size: m 50-200 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant