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:
@@ -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']}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user