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,6 +50,134 @@ const containerProps = {
}, },
}; };
const createPlayHandler =
(
controls: ItemControls | undefined,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
playType: Play,
) =>
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType,
});
};
const createFavoriteHandler =
(
controls: ItemControls | undefined,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
) =>
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
if (!item) {
return;
}
const newFavorite = !(item as { userFavorite: boolean }).userFavorite;
controls?.onFavorite?.({
event: e,
favorite: newFavorite,
internalState,
item,
itemType,
});
};
const createRatingChangeHandler =
(
controls: ItemControls | undefined,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
) =>
(rating: number) => {
if (!item) {
return;
}
let newRating = rating;
if (rating === (item as { userRating: number }).userRating) {
newRating = 0;
}
controls?.onRating?.({
event: null,
internalState,
item,
itemType,
rating: newRating,
});
};
const ratingClickHandler = (e: MouseEvent<HTMLElement>) => {
e.stopPropagation();
e.preventDefault();
};
const ratingMouseDownHandler = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
e.preventDefault();
};
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.preventDefault();
controls?.onMore?.({
event: e,
internalState,
item,
itemType,
});
};
const createExpandHandler =
(
controls: ItemControls | undefined,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
internalState: ItemListStateActions | undefined,
itemType: LibraryItem,
) =>
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
controls?.onExpand?.({
event: e,
internalState,
item,
itemType,
});
};
export const ItemCardControls = ({ export const ItemCardControls = ({
controls, controls,
enableExpansion, enableExpansion,
@@ -60,65 +188,65 @@ export const ItemCardControls = ({
}: ItemCardControlsProps) => { }: ItemCardControlsProps) => {
const isPlayerFetching = useIsPlayerFetching(); 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 ( return (
<motion.div className={clsx(styles.container)} {...containerProps[type]}> <motion.div className={clsx(styles.container)} {...containerProps[type]}>
{controls?.onPlay && ( {controls?.onPlay && (
<> <>
<PlayButton <PlayButton disabled={isPlayerFetching} onClick={playNowHandler} />
disabled={isPlayerFetching}
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.NOW,
});
}}
/>
<SecondaryPlayButton <SecondaryPlayButton
className={styles.left} className={styles.left}
icon="mediaPlayNext" icon="mediaPlayNext"
onClick={(e) => { onClick={playNextHandler}
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.NEXT,
});
}}
/> />
<SecondaryPlayButton <SecondaryPlayButton
className={styles.right} className={styles.right}
icon="mediaPlayLast" icon="mediaPlayLast"
onClick={(e) => { onClick={playLastHandler}
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.LAST,
});
}}
/> />
</> </>
)} )}
@@ -126,57 +254,16 @@ export const ItemCardControls = ({
<SecondaryButton <SecondaryButton
className={styles.favorite} className={styles.favorite}
icon="favorite" icon="favorite"
iconProps={{ iconProps={favoriteIconProps}
color: (item as { userFavorite: boolean }).userFavorite onClick={favoriteHandler}
? 'primary'
: 'default',
fill: (item as { userFavorite: boolean }).userFavorite
? 'primary'
: undefined,
}}
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
const newFavorite = !(item as { userFavorite: boolean }).userFavorite;
controls?.onFavorite?.({
event: e,
favorite: newFavorite,
internalState,
item,
itemType,
});
}}
/> />
)} )}
{controls?.onRating && ( {controls?.onRating && (
<Rating <Rating
className={styles.rating} className={styles.rating}
onChange={(rating) => { onChange={ratingChangeHandler}
if (!item) { onClick={ratingClickHandler}
return; onMouseDown={ratingMouseDownHandler}
}
let newRating = rating;
if (rating === (item as { userRating: number }).userRating) {
newRating = 0;
}
controls?.onRating?.({
event: null,
internalState,
item,
itemType,
rating: newRating,
});
}}
onClick={(e) => {
e.stopPropagation();
}}
size="xs" size="xs"
/> />
)} )}
@@ -184,90 +271,92 @@ export const ItemCardControls = ({
<SecondaryButton <SecondaryButton
className={styles.options} className={styles.options}
icon="ellipsisHorizontal" icon="ellipsisHorizontal"
onClick={(e) => { onClick={moreHandler}
e.stopPropagation(); onDoubleClick={moreDoubleClickHandler}
e.preventDefault();
controls?.onMore?.({
event: e,
internalState,
item,
itemType,
});
}}
onDoubleClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
/> />
)} )}
{controls?.onExpand && enableExpansion && ( {controls?.onExpand && enableExpansion && (
<SecondaryButton <SecondaryButton
className={styles.expand} className={styles.expand}
icon="arrowDownS" icon="arrowDownS"
onClick={(e) => { onClick={expandHandler}
e.stopPropagation();
controls?.onExpand?.({
event: e,
internalState,
item,
itemType,
});
}}
/> />
)} )}
</motion.div> </motion.div>
); );
}; };
const PlayButton = ({ const PlayButton = memo(
disabled, ({
loading, disabled,
onClick, loading,
}: { onClick,
disabled?: boolean; }: {
loading?: boolean; disabled?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; loading?: boolean;
}) => { onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
return ( }) => {
<button const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
className={clsx(styles.playButton, styles.primary, { e.stopPropagation();
[styles.disabled]: disabled, e.preventDefault();
})} if (disabled || loading) {
disabled={disabled} return;
onClick={(e) => { }
e.stopPropagation(); onClick?.(e);
if (disabled || loading) { };
return;
}
onClick?.(e);
}}
>
<Icon icon="mediaPlay" size="lg" />
</button>
);
};
const SecondaryPlayButton = ({ const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
className, e.stopPropagation();
icon, e.preventDefault();
onClick, };
}: {
className?: string; return (
icon: keyof typeof AppIcon; <button
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; className={clsx(styles.playButton, styles.primary, {
}) => { [styles.disabled]: disabled,
return ( })}
<button disabled={disabled}
className={clsx(styles.playButton, styles.secondary, className)} onClick={handleClick}
onClick={(e) => { onMouseDown={handleMouseDown}
e.stopPropagation(); >
onClick?.(e); <Icon icon="mediaPlay" size="lg" />
}} </button>
> );
<Icon icon={icon} size="lg" /> },
</button> );
);
}; const SecondaryPlayButton = memo(
({
className,
icon,
onClick,
}: {
className?: string;
icon: keyof typeof AppIcon;
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 (
<button
className={clsx(styles.playButton, styles.secondary, className)}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
<Icon icon={icon} size="lg" />
</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, ({
icon, className,
iconProps, icon,
onClick, iconProps,
onDoubleClick, onClick,
}: SecondaryButtonProps & { onDoubleClick,
iconProps?: Partial<IconProps>; }: SecondaryButtonProps & {
onDoubleClick?: (e: MouseEvent<HTMLButtonElement>) => void; iconProps?: Partial<IconProps>;
}) => { onDoubleClick?: (e: MouseEvent<HTMLButtonElement>) => void;
return ( }) => {
<button const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
className={clsx(styles.secondaryButton, className)} e.stopPropagation();
onClick={(e) => { e.preventDefault();
e.stopPropagation(); onClick?.(e);
e.preventDefault(); };
onClick?.(e);
}} const handleDoubleClick = (e: MouseEvent<HTMLButtonElement>) => {
onDoubleClick={(e) => { e.stopPropagation();
e.stopPropagation(); e.preventDefault();
e.preventDefault(); onDoubleClick?.(e);
onDoubleClick?.(e); };
}}
> const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
<Icon icon={icon} size="lg" {...iconProps} /> e.stopPropagation();
</button> e.preventDefault();
); };
};
return (
<button
className={clsx(styles.secondaryButton, className)}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onMouseDown={handleMouseDown}
>
<Icon icon={icon} size="lg" {...iconProps} />
</button>
);
},
);