fix unneccesary navigation on grid card control click

This commit is contained in:
jeffvli
2025-11-22 04:33:12 -08:00
parent 9801337c61
commit 19a51d82be
@@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { MouseEvent } from 'react'; import { memo, MouseEvent, useMemo } from 'react';
import styles from './item-card-controls.module.css'; import styles from './item-card-controls.module.css';
@@ -50,24 +50,17 @@ const containerProps = {
}, },
}; };
export const ItemCardControls = ({ const createPlayHandler =
controls, (
enableExpansion, controls: ItemControls | undefined,
internalState, item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
item, internalState: ItemListStateActions | undefined,
itemType, itemType: LibraryItem,
type = 'default', playType: Play,
}: ItemCardControlsProps) => { ) =>
const isPlayerFetching = useIsPlayerFetching(); (e: MouseEvent<HTMLButtonElement>) => {
return (
<motion.div className={clsx(styles.container)} {...containerProps[type]}>
{controls?.onPlay && (
<>
<PlayButton
disabled={isPlayerFetching}
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault();
if (!item) { if (!item) {
return; return;
@@ -78,64 +71,20 @@ export const ItemCardControls = ({
internalState, internalState,
item, item,
itemType, itemType,
playType: Play.NOW, playType,
}); });
}} };
/>
<SecondaryPlayButton const createFavoriteHandler =
className={styles.left} (
icon="mediaPlayNext" controls: ItemControls | undefined,
onClick={(e) => { item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
e.stopPropagation(); internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
if (!item) { ) =>
return; (e: MouseEvent<HTMLButtonElement>) => {
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.NEXT,
});
}}
/>
<SecondaryPlayButton
className={styles.right}
icon="mediaPlayLast"
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.LAST,
});
}}
/>
</>
)}
{controls?.onFavorite && (
<SecondaryButton
className={styles.favorite}
icon="favorite"
iconProps={{
color: (item as { userFavorite: boolean }).userFavorite
? 'primary'
: 'default',
fill: (item as { userFavorite: boolean }).userFavorite
? 'primary'
: undefined,
}}
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault();
if (!item) { if (!item) {
return; return;
@@ -149,13 +98,16 @@ export const ItemCardControls = ({
item, item,
itemType, itemType,
}); });
}} };
/>
)} const createRatingChangeHandler =
{controls?.onRating && ( (
<Rating controls: ItemControls | undefined,
className={styles.rating} item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
onChange={(rating) => { internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
) =>
(rating: number) => {
if (!item) { if (!item) {
return; return;
} }
@@ -173,18 +125,31 @@ export const ItemCardControls = ({
itemType, itemType,
rating: newRating, rating: newRating,
}); });
}} };
onClick={(e) => {
const ratingClickHandler = (e: MouseEvent<HTMLElement>) => {
e.stopPropagation(); e.stopPropagation();
}} e.preventDefault();
size="xs" };
/>
)} const ratingMouseDownHandler = (e: React.MouseEvent<HTMLElement>) => {
{controls?.onMore && ( e.stopPropagation();
<SecondaryButton e.preventDefault();
className={styles.options} };
icon="ellipsisHorizontal"
onClick={(e) => { const moreDoubleClickHandler = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};
const createMoreHandler =
(
controls: ItemControls | undefined,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
) =>
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
controls?.onMore?.({ controls?.onMore?.({
@@ -193,81 +158,205 @@ export const ItemCardControls = ({
item, item,
itemType, itemType,
}); });
}} };
onDoubleClick={(e) => {
const createExpandHandler =
(
controls: ItemControls | undefined,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
) =>
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}}
/>
)}
{controls?.onExpand && enableExpansion && (
<SecondaryButton
className={styles.expand}
icon="arrowDownS"
onClick={(e) => {
e.stopPropagation();
controls?.onExpand?.({ controls?.onExpand?.({
event: e, event: e,
internalState, internalState,
item, item,
itemType, itemType,
}); });
}} };
export const ItemCardControls = ({
controls,
enableExpansion,
internalState,
item,
itemType,
type = 'default',
}: ItemCardControlsProps) => {
const isPlayerFetching = useIsPlayerFetching();
const playNowHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.NOW),
[controls, item, internalState, itemType],
);
const playNextHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.NEXT),
[controls, item, internalState, itemType],
);
const playLastHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.LAST),
[controls, item, internalState, itemType],
);
const favoriteHandler = useMemo(
() => createFavoriteHandler(controls, item, internalState, itemType),
[controls, item, internalState, itemType],
);
const ratingChangeHandler = useMemo(
() => createRatingChangeHandler(controls, item, internalState, itemType),
[controls, item, internalState, itemType],
);
const moreHandler = useMemo(
() => createMoreHandler(controls, item, internalState, itemType),
[controls, item, internalState, itemType],
);
const expandHandler = useMemo(
() => createExpandHandler(controls, item, internalState, itemType),
[controls, item, internalState, itemType],
);
const isFavorite = (item as { userFavorite?: boolean })?.userFavorite ?? false;
const favoriteIconProps = useMemo<Partial<IconProps>>(
() => ({
color: isFavorite ? ('primary' as const) : ('default' as const),
fill: isFavorite ? ('primary' as const) : undefined,
}),
[isFavorite],
);
return (
<motion.div className={clsx(styles.container)} {...containerProps[type]}>
{controls?.onPlay && (
<>
<PlayButton disabled={isPlayerFetching} onClick={playNowHandler} />
<SecondaryPlayButton
className={styles.left}
icon="mediaPlayNext"
onClick={playNextHandler}
/>
<SecondaryPlayButton
className={styles.right}
icon="mediaPlayLast"
onClick={playLastHandler}
/>
</>
)}
{controls?.onFavorite && (
<SecondaryButton
className={styles.favorite}
icon="favorite"
iconProps={favoriteIconProps}
onClick={favoriteHandler}
/>
)}
{controls?.onRating && (
<Rating
className={styles.rating}
onChange={ratingChangeHandler}
onClick={ratingClickHandler}
onMouseDown={ratingMouseDownHandler}
size="xs"
/>
)}
{controls?.onMore && (
<SecondaryButton
className={styles.options}
icon="ellipsisHorizontal"
onClick={moreHandler}
onDoubleClick={moreDoubleClickHandler}
/>
)}
{controls?.onExpand && enableExpansion && (
<SecondaryButton
className={styles.expand}
icon="arrowDownS"
onClick={expandHandler}
/> />
)} )}
</motion.div> </motion.div>
); );
}; };
const PlayButton = ({ const PlayButton = memo(
({
disabled, disabled,
loading, loading,
onClick, onClick,
}: { }: {
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}) => { }) => {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
if (disabled || loading) {
return;
}
onClick?.(e);
};
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};
return ( return (
<button <button
className={clsx(styles.playButton, styles.primary, { className={clsx(styles.playButton, styles.primary, {
[styles.disabled]: disabled, [styles.disabled]: disabled,
})} })}
disabled={disabled} disabled={disabled}
onClick={(e) => { onClick={handleClick}
e.stopPropagation(); onMouseDown={handleMouseDown}
if (disabled || loading) {
return;
}
onClick?.(e);
}}
> >
<Icon icon="mediaPlay" size="lg" /> <Icon icon="mediaPlay" size="lg" />
</button> </button>
); );
}; },
);
const SecondaryPlayButton = ({ const SecondaryPlayButton = memo(
({
className, className,
icon, icon,
onClick, onClick,
}: { }: {
className?: string; className?: string;
icon: keyof typeof AppIcon; icon: keyof typeof AppIcon;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}) => { }) => {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
onClick?.(e);
};
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};
return ( return (
<button <button
className={clsx(styles.playButton, styles.secondary, className)} className={clsx(styles.playButton, styles.secondary, className)}
onClick={(e) => { onClick={handleClick}
e.stopPropagation(); onMouseDown={handleMouseDown}
onClick?.(e);
}}
> >
<Icon icon={icon} size="lg" /> <Icon icon={icon} size="lg" />
</button> </button>
); );
}; },
);
interface SecondaryButtonProps { interface SecondaryButtonProps {
className?: string; className?: string;
@@ -275,31 +364,43 @@ interface SecondaryButtonProps {
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
} }
const SecondaryButton = ({ const SecondaryButton = memo(
({
className, className,
icon, icon,
iconProps, iconProps,
onClick, onClick,
onDoubleClick, onDoubleClick,
}: SecondaryButtonProps & { }: SecondaryButtonProps & {
iconProps?: Partial<IconProps>; iconProps?: Partial<IconProps>;
onDoubleClick?: (e: MouseEvent<HTMLButtonElement>) => void; onDoubleClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}) => { }) => {
return ( const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
<button
className={clsx(styles.secondaryButton, className)}
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onClick?.(e); onClick?.(e);
}} };
onDoubleClick={(e) => {
const handleDoubleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onDoubleClick?.(e); onDoubleClick?.(e);
}} };
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};
return (
<button
className={clsx(styles.secondaryButton, className)}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onMouseDown={handleMouseDown}
> >
<Icon icon={icon} size="lg" {...iconProps} /> <Icon icon={icon} size="lg" {...iconProps} />
</button> </button>
); );
}; },
);