fix(playlist): column picker no longer clipped on short lists (#853)

* fix(playlist): columns dropdown no longer clipped on short lists (#839)

The column picker rendered inside `.tracklist` (overflow-x: auto, which
makes overflow-y compute to auto). On a 1-song playlist the downward popover
overflowed the short box → clipped behind suggestions, an extra scrollbar,
and the row vanishing when scrolling that inner bar (the virtualizer tracks
the main viewport, not the tracklist). Move the picker outside `.tracklist`
by reusing the shared TracklistColumnPicker (parametrized with allColumns);
fixes the same latent bug in the favorites tracklist and dedupes three
inline copies into one.

* docs(changelog): playlist/favorites column picker fix (#853)
This commit is contained in:
Frank Stellmacher
2026-05-22 19:20:34 +02:00
committed by GitHub
parent e8e41752a7
commit cc8e6cc811
5 changed files with 41 additions and 70 deletions
+8
View File
@@ -183,6 +183,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Playlists & Favorites — column picker on short lists
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#853](https://github.com/Psychotoxical/psysonic/pull/853)**
* On a one-song playlist (or short favorites list) the column menu was clipped behind the list, added a stray scrollbar, and could hide the row when scrolled. The picker now sits outside the scroll area, so it opens fully on lists of any length.
## [1.46.0] - 2026-05-18
> **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages.
+1
View File
@@ -119,6 +119,7 @@ export default function AlbumTrackList({
return (
<>
<TracklistColumnPicker
allColumns={COLUMNS}
pickerRef={pickerRef}
pickerOpen={pickerOpen}
setPickerOpen={setPickerOpen}
@@ -1,9 +1,11 @@
import React from 'react';
import { Check, ChevronDown, RotateCcw } from 'lucide-react';
import type { TFunction } from 'i18next';
import { COLUMNS } from '../../utils/componentHelpers/albumTrackListHelpers';
import type { ColDef } from '../../utils/useTracklistColumns';
interface Props {
/** Every column (required ones are filtered out of the menu). */
allColumns: readonly ColDef[];
pickerRef: React.RefObject<HTMLDivElement | null>;
pickerOpen: boolean;
setPickerOpen: (updater: (v: boolean) => boolean) => void;
@@ -20,6 +22,7 @@ interface Props {
* button.
*/
export function TracklistColumnPicker({
allColumns,
pickerRef,
pickerOpen,
setPickerOpen,
@@ -41,7 +44,7 @@ export function TracklistColumnPicker({
{pickerOpen && (
<div className="tracklist-col-picker-menu">
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
{COLUMNS.filter(c => !c.required).map(c => {
{allColumns.filter(c => !c.required).map(c => {
const label = c.i18nKey ? t(`albumDetail.${c.i18nKey as string}`) : c.key;
const isOn = colVisible.has(c.key);
return (
@@ -1,9 +1,9 @@
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import FavoriteSongRow, { type FavoriteSongRowCallbacks } from './FavoriteSongRow';
import { TracklistColumnPicker } from '../albumTrackList/TracklistColumnPicker';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Check, ChevronDown, RotateCcw } from 'lucide-react';
import type { ColDef } from '../../utils/useTracklistColumns';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { usePlayerStore } from '../../store/playerStore';
@@ -168,39 +168,21 @@ export default function FavoritesSongsTracklist({
const virtualItems = rowVirtualizer.getVirtualItems();
return (
<>
<TracklistColumnPicker
allColumns={allColumns}
pickerRef={pickerRef}
pickerOpen={pickerOpen}
setPickerOpen={setPickerOpen}
colVisible={colVisible}
toggleColumn={toggleColumn}
resetColumns={resetColumns}
t={t}
/>
<div className="tracklist" data-preview-loc="favorites" style={{ padding: 0 }} ref={tracklistRef} onClick={e => {
if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll();
}}>
{/* Column visibility picker */}
<div className="tracklist-col-picker-wrapper" ref={pickerRef}>
<div className="tracklist-col-picker">
<button className="tracklist-col-picker-btn" onClick={e => { e.stopPropagation(); setPickerOpen(v => !v); }} data-tooltip={t('albumDetail.columns')}>
<ChevronDown size={14} />
</button>
{pickerOpen && (
<div className="tracklist-col-picker-menu">
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
{allColumns.filter(c => !c.required).map(c => {
const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key;
const isOn = colVisible.has(c.key);
return (
<button key={c.key} className={`tracklist-col-picker-item${isOn ? ' active' : ''}`} onClick={() => toggleColumn(c.key)}>
<span className="tracklist-col-picker-check">{isOn && <Check size={13} />}</span>
{label}
</button>
);
})}
<div className="tracklist-col-picker-divider" />
<button className="tracklist-col-picker-reset" onClick={resetColumns}>
<RotateCcw size={13} />
{t('albumDetail.resetColumns')}
</button>
</div>
)}
</div>
</div>
<div style={{ position: 'relative' }}>
<div className="tracklist-header tracklist-va" style={gridStyle}>
{visibleCols.map((colDef, colIndex) => {
@@ -321,5 +303,6 @@ export default function FavoritesSongsTracklist({
</div>
)}
</div>
</>
);
}
+14 -38
View File
@@ -1,12 +1,13 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import PlaylistRow, { type PlaylistRowCallbacks } from './PlaylistRow';
import { TracklistColumnPicker } from '../albumTrackList/TracklistColumnPicker';
import { useTranslation } from 'react-i18next';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
import { useElementClientHeightById } from '../../hooks/useResizeClientHeight';
import { useNavigate } from 'react-router-dom';
import {
Check, ChevronDown, ListPlus, RotateCcw, Search, Trash2, X,
ListPlus, Search, Trash2, X,
} from 'lucide-react';
import type { ColDef } from '../../utils/useTracklistColumns';
import type { SubsonicSong } from '../../api/subsonicTypes';
@@ -235,6 +236,17 @@ export default function PlaylistTracklist({
}
return (
<>
<TracklistColumnPicker
allColumns={allColumns}
pickerRef={pickerRef}
pickerOpen={pickerOpen}
setPickerOpen={setPickerOpen}
colVisible={colVisible}
toggleColumn={toggleColumn}
resetColumns={resetColumns}
t={t}
/>
<div className="tracklist" data-preview-loc="playlists" ref={tracklistRef}>
{/* Bulk action bar */}
@@ -277,43 +289,6 @@ export default function PlaylistTracklist({
</div>
)}
{/* Column visibility picker */}
<div className="tracklist-col-picker-wrapper" ref={pickerRef}>
<div className="tracklist-col-picker">
<button
className="tracklist-col-picker-btn"
onClick={e => { e.stopPropagation(); setPickerOpen(v => !v); }}
data-tooltip={t('albumDetail.columns')}
>
<ChevronDown size={14} />
</button>
{pickerOpen && (
<div className="tracklist-col-picker-menu">
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
{allColumns.filter(c => !c.required).map(c => {
const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key;
const isOn = colVisible.has(c.key);
return (
<button
key={c.key}
className={`tracklist-col-picker-item${isOn ? ' active' : ''}`}
onClick={() => toggleColumn(c.key)}
>
<span className="tracklist-col-picker-check">{isOn && <Check size={13} />}</span>
{label}
</button>
);
})}
<div className="tracklist-col-picker-divider" />
<button className="tracklist-col-picker-reset" onClick={resetColumns}>
<RotateCcw size={13} />
{t('albumDetail.resetColumns')}
</button>
</div>
)}
</div>
</div>
{/* Header */}
<div style={{ position: 'relative' }}>
<div className="tracklist-header tracklist-va" style={gridStyle}>
@@ -481,5 +456,6 @@ export default function PlaylistTracklist({
</div>
</>
);
}