feat(ui): add tooltips for long playlist and album names - 5068 (#5070)

* style(ui): add tooltips for long playlist and album names - 5068

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>

* fix dnd and improve performance

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>

* lint fixes

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>

* fix(ui): update tooltip styles for improved visibility and consistency

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(ui): add overflow tooltip to playlist name for better visibility

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(ui): simplify OverflowTooltip and improve render performance

- Inline styles from useMenuTooltipStyles into OverflowTooltip (single consumer)
- Use MUI named colors (grey[700]/grey[300] with alpha) instead of raw rgba
- Stabilize ref callback with useCallback to avoid unnecessary ref churn
- Memoize Tooltip classes and hoist TransitionProps to module level
- Fix useLayoutEffect dependency: observe DOM size, not title string

---------

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
This commit is contained in:
Thiago Sfredo
2026-03-15 15:55:55 -03:00
committed by GitHub
parent aa93911991
commit 36aea8a11f
5 changed files with 115 additions and 12 deletions
+9 -2
View File
@@ -13,7 +13,12 @@ import { linkToRecord, useListContext, Loading } from 'react-admin'
import { withContentRect } from 'react-measure' import { withContentRect } from 'react-measure'
import { useDrag } from 'react-dnd' import { useDrag } from 'react-dnd'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { AlbumContextMenu, PlayButton, ArtistLinkField } from '../common' import {
AlbumContextMenu,
PlayButton,
ArtistLinkField,
OverflowTooltip,
} from '../common'
import { DraggableTypes } from '../consts' import { DraggableTypes } from '../consts'
import clsx from 'clsx' import clsx from 'clsx'
import { AlbumDatesField } from './AlbumDatesField.jsx' import { AlbumDatesField } from './AlbumDatesField.jsx'
@@ -198,7 +203,9 @@ const AlbumGridTile = ({ showArtist, record, basePath, ...props }) => {
to={linkToRecord(basePath, record.id, 'show')} to={linkToRecord(basePath, record.id, 'show')}
> >
<span> <span>
<Typography className={classes.albumName}>{record.name}</Typography> <OverflowTooltip title={record.name}>
<Typography className={classes.albumName}>{record.name}</Typography>
</OverflowTooltip>
{record.tags && record.tags['albumversion'] && ( {record.tags && record.tags['albumversion'] && (
<Typography className={classes.albumVersion}> <Typography className={classes.albumVersion}>
{record.tags['albumversion']} {record.tags['albumversion']}
+90
View File
@@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Tooltip } from '@material-ui/core'
import { makeStyles, alpha } from '@material-ui/core/styles'
import grey from '@material-ui/core/colors/grey'
const useStyles = makeStyles(
(theme) => ({
tooltip: {
backgroundColor:
theme.palette.type === 'dark'
? alpha(grey[700], 0.92)
: alpha(grey[300], 0.92),
color:
theme.palette.type === 'dark'
? theme.palette.common.white
: theme.palette.common.black,
borderRadius: theme.shape.borderRadius,
...theme.typography.body2,
padding: theme.spacing(0.5, 1),
maxWidth: 300,
},
}),
{ name: 'NDOverflowTooltip' },
)
const transitionProps = { timeout: 0 }
export const OverflowTooltip = ({
children,
title,
placement = 'bottom-start',
}) => {
const classes = useStyles()
const textRef = React.useRef(null)
const [isOverflowing, setIsOverflowing] = React.useState(false)
const tooltipClasses = React.useMemo(
() => ({ tooltip: classes.tooltip }),
[classes.tooltip],
)
React.useLayoutEffect(() => {
const el = textRef.current
if (!el) return
const checkOverflow = () => {
setIsOverflowing(el.scrollWidth > el.clientWidth)
}
const resizeObserver = new ResizeObserver(checkOverflow)
resizeObserver.observe(el)
checkOverflow()
return () => resizeObserver.disconnect()
}, [])
const mergedRef = React.useCallback(
(el) => {
textRef.current = el
const { ref } = children
if (typeof ref === 'function') {
ref(el)
} else if (ref && typeof ref === 'object') {
ref.current = el
}
},
[children],
)
return (
<Tooltip
title={title}
disableHoverListener={!isOverflowing}
disableTouchListener
placement={placement}
TransitionProps={transitionProps}
classes={tooltipClasses}
>
{React.cloneElement(children, { ref: mergedRef })}
</Tooltip>
)
}
OverflowTooltip.propTypes = {
children: PropTypes.element.isRequired,
title: PropTypes.string.isRequired,
placement: PropTypes.string,
}
+1
View File
@@ -41,4 +41,5 @@ export * from './formatRange.js'
export * from './playlistUtils.js' export * from './playlistUtils.js'
export * from './PathField.jsx' export * from './PathField.jsx'
export * from './ParticipantsInfo' export * from './ParticipantsInfo'
export * from './OverflowTooltip'
export * from './useSearchRefocus' export * from './useSearchRefocus'
+6 -4
View File
@@ -12,7 +12,7 @@ import QueueMusicOutlinedIcon from '@material-ui/icons/QueueMusicOutlined'
import { BiCog } from 'react-icons/bi' import { BiCog } from 'react-icons/bi'
import { useDrop } from 'react-dnd' import { useDrop } from 'react-dnd'
import SubMenu from './SubMenu' import SubMenu from './SubMenu'
import { canChangeTracks } from '../common' import { canChangeTracks, OverflowTooltip } from '../common'
import { DraggableTypes } from '../consts' import { DraggableTypes } from '../consts'
import config from '../config' import config from '../config'
@@ -39,9 +39,11 @@ const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => {
<MenuItemLink <MenuItemLink
to={`/playlist/${pls.id}/show`} to={`/playlist/${pls.id}/show`}
primaryText={ primaryText={
<Typography variant="inherit" noWrap ref={dropRef}> <OverflowTooltip title={pls.name} placement="right">
{pls.name} <Typography variant="inherit" noWrap ref={dropRef}>
</Typography> {pls.name}
</Typography>
</OverflowTooltip>
} }
sidebarIsOpen={sidebarIsOpen} sidebarIsOpen={sidebarIsOpen}
dense={false} dense={false}
+9 -6
View File
@@ -19,6 +19,7 @@ import {
DurationField, DurationField,
SizeField, SizeField,
isWritable, isWritable,
OverflowTooltip,
} from '../common' } from '../common'
import config from '../config' import config from '../config'
import subsonic from '../subsonic' import subsonic from '../subsonic'
@@ -274,12 +275,14 @@ const PlaylistDetails = (props) => {
</div> </div>
<div className={classes.details}> <div className={classes.details}>
<CardContent className={classes.content}> <CardContent className={classes.content}>
<Typography <OverflowTooltip title={record.name || ''}>
variant={isDesktop ? 'h5' : 'h6'} <Typography
className={classes.title} variant={isDesktop ? 'h5' : 'h6'}
> className={classes.title}
{record.name || translate('ra.page.loading')} >
</Typography> {record.name || translate('ra.page.loading')}
</Typography>
</OverflowTooltip>
<Typography component="p" className={classes.stats}> <Typography component="p" className={classes.stats}>
{record.songCount ? ( {record.songCount ? (
<span> <span>