fix(v3): dismiss Section Practice popover when another player popover opens (#638)

* fix(v3): dismiss Section Practice popover when another player popover opens

The Section Practice popover (Songs > Song > Practice pill) stayed open
when the user then clicked a v3 player-rail icon (Plugins, Audio, …),
leaving two popovers stacked on top of each other. Reported on 0.3.0
(macOS) and still reproducing in the 2026-06-28 build.

Root cause: the popover's outside-click dismiss was bound in the
bubbling phase, but the v3 rail's icon buttons call e.stopPropagation()
in their click handler (player-chrome.js wireRail), which kills bubbling
before the click reaches document. So the dismiss listener never fired
for a rail-icon click and the popover was orphaned open.

Fix: bind the outside-click dismiss in the capture phase, which runs
before the target's handler so stopPropagation() can't swallow it. This
mirrors the audio mixer popover (audio-mixer.js), which already
dismisses outside-clicks via capture-phase listeners for exactly this
reason. Esc handling stays in the bubble phase (no rail handler stops
keydown propagation, and capturing it would reorder it ahead of the
player's Escape-to-exit handling).

Shared app.js code, so v2 is covered too; v2 has no stopPropagation rail,
so its outside-click dismiss behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

* chore(#638): add CHANGELOG entry + capture-phase regression test

Review follow-ups for the Section Practice popover dismiss fix:
- CHANGELOG [Unreleased] → Fixed entry (repo workflow requires one).
- tests/js/section_practice_dismiss.test.js pins the fix: the outside-click
  dismiss binds in the CAPTURE phase (so a rail icon's stopPropagation can't
  swallow it), exactly one capture binding (Escape keydown stays bubble-phase),
  and the #section-practice-control containment guard (no self-close). A revert
  to bubble-phase fails the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
ChrisBeWithYou
2026-06-29 15:00:28 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 byrongamatos
parent 8fbbc761fc
commit 199550e5fb
3 changed files with 57 additions and 1 deletions
+13 -1
View File
@@ -8714,12 +8714,24 @@ function _installSectionPracticeDismiss() {
// inside #section-practice-control so it never self-closes. Listeners added
// mid-dispatch don't fire for the opening click, so there's no immediate
// close race.
//
// The click listener uses the CAPTURE phase: the v3 player rail's icon
// buttons call e.stopPropagation() in their click handler (player-chrome.js
// wireRail), which kills bubbling before it reaches document. A bubble-phase
// outside-click dismiss would therefore never fire when the user clicks a
// rail icon (Plugins, Audio, …) to open another popover, leaving this
// popover stranded open on top of it. Capture runs before the target's
// handler, so the stopPropagation can't swallow it. This mirrors the audio
// mixer popover (audio-mixer.js), which dismisses outside-clicks the same
// way. (Esc stays bubble-phase — no rail handler stops keydown propagation,
// so it already reaches us, and capturing it would reorder it ahead of the
// player's Escape-to-exit handling.)
document.addEventListener('click', (e) => {
if (!_sectionPracticePopoverOpen()) return;
const ctrl = document.getElementById('section-practice-control');
if (ctrl && ctrl.contains(e.target)) return;
_closeSectionPracticePopover();
});
}, true);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && _sectionPracticePopoverOpen()) _closeSectionPracticePopover();
});