fix(13-02): defer virtualizer event delegation until element exists
- Event delegation handlers were attached in firstUpdated(), but the virtualizer is conditionally rendered (hidden when tracks empty/loading) - On first render, tracks are [] so virtualizer doesn't exist, and firstUpdated() never fires again — delegation was never attached - Move delegation to a guarded helper called from both firstUpdated() and updated(), so it attaches as soon as the virtualizer appears - Fixes click, multi-select, context menu, double-click, and drag in both track-list and queue-panel components
This commit is contained in:
@@ -78,6 +78,9 @@ export class QueuePanel
|
||||
|
||||
private dragImageEl: HTMLElement | null = null;
|
||||
|
||||
/** Whether delegated event handlers have been attached to the virtualizer. */
|
||||
private delegationAttached = false;
|
||||
|
||||
@query('#add-to-playlist-popup')
|
||||
private addToPlaylistPopup!: WaPopup;
|
||||
|
||||
@@ -485,6 +488,23 @@ export class QueuePanel
|
||||
`];
|
||||
|
||||
override firstUpdated() {
|
||||
this.attachVirtualizerHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach scroll-error monkey-patch, mousedown listener,
|
||||
* and delegated event handlers to the virtualizer.
|
||||
* The virtualizer may not exist on first render (queue
|
||||
* empty), so this is called from both firstUpdated()
|
||||
* and updated() — guarded by a flag.
|
||||
*/
|
||||
private attachVirtualizerHooks() {
|
||||
if (this.delegationAttached) return;
|
||||
|
||||
const virtEl = this.virtualizer;
|
||||
|
||||
if (!virtEl) return;
|
||||
|
||||
// Monkey-patch lit-virtualizer's scroll error correction to suppress it
|
||||
// during native scrollbar drag. Without this, the virtualizer calls
|
||||
// scrollTo() to "correct" sub-pixel estimation errors, which fights the
|
||||
@@ -497,7 +517,7 @@ export class QueuePanel
|
||||
// calls scrollTo() internally. This monkey-patch is still needed
|
||||
// to suppress that internal correction during scrollbar drag.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const virt = (this.virtualizer as any)?.[virtualizerRef];
|
||||
const virt = (virtEl as any)?.[virtualizerRef];
|
||||
if (virt) {
|
||||
const origCorrect = virt._correctScrollError.bind(virt);
|
||||
virt._correctScrollError = () => {
|
||||
@@ -511,21 +531,19 @@ export class QueuePanel
|
||||
origCorrect();
|
||||
};
|
||||
}
|
||||
this.virtualizer?.addEventListener(
|
||||
virtEl.addEventListener(
|
||||
'mousedown',
|
||||
this.onVirtualizerMouseDown,
|
||||
);
|
||||
|
||||
// Event delegation: attach stable handlers to the virtualizer
|
||||
// so renderTrackItem creates zero per-item closures.
|
||||
const virtEl = this.virtualizer;
|
||||
if (virtEl) {
|
||||
virtEl.addEventListener('click', this.onDelegatedClick);
|
||||
virtEl.addEventListener('dblclick', this.onDelegatedDblClick);
|
||||
virtEl.addEventListener('contextmenu', this.onDelegatedContextMenu);
|
||||
virtEl.addEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virtEl.addEventListener('dragend', this.onTrackDragEnd);
|
||||
}
|
||||
virtEl.addEventListener('click', this.onDelegatedClick);
|
||||
virtEl.addEventListener('dblclick', this.onDelegatedDblClick);
|
||||
virtEl.addEventListener('contextmenu', this.onDelegatedContextMenu);
|
||||
virtEl.addEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virtEl.addEventListener('dragend', this.onTrackDragEnd);
|
||||
this.delegationAttached = true;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -608,9 +626,14 @@ export class QueuePanel
|
||||
virtEl.removeEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virtEl.removeEventListener('dragend', this.onTrackDragEnd);
|
||||
}
|
||||
this.delegationAttached = false;
|
||||
}
|
||||
|
||||
override updated() {
|
||||
// The virtualizer may not exist on first render
|
||||
// (queue empty). Retry hooks here when it appears.
|
||||
this.attachVirtualizerHooks();
|
||||
|
||||
const currentIndex = this.queue.currentIndex;
|
||||
|
||||
// Force virtualizer to re-render visible items when the
|
||||
|
||||
@@ -204,6 +204,9 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
@query('#sort-dropdown')
|
||||
private sortDropdownPopup!: WaPopup;
|
||||
|
||||
/** Whether delegated event handlers have been attached to the virtualizer. */
|
||||
private delegationAttached = false;
|
||||
|
||||
private resizingColumn: number | null = null;
|
||||
private resizeStartX = 0;
|
||||
private resizeStartWidths: number[] = [];
|
||||
@@ -1084,6 +1087,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
virt.removeEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virt.removeEventListener('dragend', this.onTrackDragEnd);
|
||||
}
|
||||
this.delegationAttached = false;
|
||||
|
||||
this.virtualizer?.removeEventListener(
|
||||
'visibilityChanged',
|
||||
@@ -1121,20 +1125,35 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
|
||||
override firstUpdated() {
|
||||
this.initColumnWidths();
|
||||
this.attachDelegation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach delegated event handlers to the virtualizer.
|
||||
* The virtualizer may not exist on first render (tracks
|
||||
* still loading), so this is called from both
|
||||
* firstUpdated() and updated() — guarded by a flag.
|
||||
*/
|
||||
private attachDelegation() {
|
||||
if (this.delegationAttached) return;
|
||||
|
||||
// Event delegation: attach stable handlers to the virtualizer
|
||||
// so renderTrackRow creates zero per-item closures.
|
||||
const virt = this.virtualizer;
|
||||
if (virt) {
|
||||
virt.addEventListener('click', this.onDelegatedClick);
|
||||
virt.addEventListener('dblclick', this.onDelegatedDblClick);
|
||||
virt.addEventListener('contextmenu', this.onDelegatedContextMenu);
|
||||
virt.addEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virt.addEventListener('dragend', this.onTrackDragEnd);
|
||||
}
|
||||
|
||||
if (!virt) return;
|
||||
|
||||
virt.addEventListener('click', this.onDelegatedClick);
|
||||
virt.addEventListener('dblclick', this.onDelegatedDblClick);
|
||||
virt.addEventListener('contextmenu', this.onDelegatedContextMenu);
|
||||
virt.addEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virt.addEventListener('dragend', this.onTrackDragEnd);
|
||||
this.delegationAttached = true;
|
||||
}
|
||||
|
||||
override updated(changed: Map<string, unknown>) {
|
||||
// The virtualizer may not exist on first render
|
||||
// (tracks still loading). Retry delegation here.
|
||||
this.attachDelegation();
|
||||
|
||||
// Recompute widths when the column config changes.
|
||||
const colKey = this.trackListCtrl.columnIds.join(
|
||||
',',
|
||||
|
||||
Reference in New Issue
Block a user