implement double click handler on default controls

This commit is contained in:
jeffvli
2025-11-13 20:54:18 -08:00
parent c5e11cca58
commit a75f64d204
6 changed files with 291 additions and 61 deletions
@@ -176,6 +176,7 @@ export const ItemCardControls = ({
icon="ellipsisHorizontal"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
controls?.onMore?.({
event: e,
internalState,
@@ -183,6 +184,10 @@ export const ItemCardControls = ({
itemType,
});
}}
onDoubleClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
/>
)}
{controls?.onExpand && (
@@ -260,14 +265,25 @@ interface SecondaryButtonProps {
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}
const SecondaryButton = ({ className, icon, onClick }: SecondaryButtonProps) => {
const SecondaryButton = ({
className,
icon,
onClick,
onDoubleClick,
}: SecondaryButtonProps & { onDoubleClick?: (e: MouseEvent<HTMLButtonElement>) => void }) => {
return (
<button
className={clsx(styles.secondaryButton, className)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onClick?.(e);
}}
onDoubleClick={(e) => {
e.stopPropagation();
e.preventDefault();
onDoubleClick?.(e);
}}
>
<Icon icon={icon} size="lg" />
</button>
+124 -36
View File
@@ -15,6 +15,7 @@ import { Image } from '/@/shared/components/image/image';
import { Separator } from '/@/shared/components/separator/separator';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text';
import { useDoubleClick } from '/@/shared/hooks/use-double-click';
import {
Album,
AlbumArtist,
@@ -138,22 +139,50 @@ const CompactItemCard = ({
}
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleClick = useDoubleClick({
onSingleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
// Don't trigger selection if clicking on interactive elements
const target = e.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
controls.onClick?.({
event: e,
internalState,
item: data as any,
itemType,
});
},
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
controls.onDoubleClick?.({
event: e,
internalState,
item: data as any,
itemType,
});
},
});
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
// Don't trigger selection if clicking on interactive elements
const target = e.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
controls.onClick?.({
e.preventDefault();
controls.onMore?.({
event: e,
internalState,
item: data as any,
@@ -170,6 +199,7 @@ const CompactItemCard = ({
<div
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
onClick={handleClick}
onContextMenu={handleContextMenu}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -242,22 +272,50 @@ const DefaultItemCard = ({
}
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleClick = useDoubleClick({
onSingleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
// Don't trigger selection if clicking on interactive elements
const target = e.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
controls.onClick?.({
event: e,
internalState,
item: data as any,
itemType,
});
},
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
controls.onDoubleClick?.({
event: e,
internalState,
item: data as any,
itemType,
});
},
});
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
// Don't trigger selection if clicking on interactive elements
const target = e.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
controls.onClick?.({
e.preventDefault();
controls.onMore?.({
event: e,
internalState,
item: data as any,
@@ -274,6 +332,7 @@ const DefaultItemCard = ({
<div
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
onClick={handleClick}
onContextMenu={handleContextMenu}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -390,22 +449,50 @@ const PosterItemCard = ({
}
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleClick = useDoubleClick({
onSingleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
// Don't trigger selection if clicking on interactive elements
const target = e.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
controls.onClick?.({
event: e,
internalState,
item: data as any,
itemType,
});
},
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
controls.onDoubleClick?.({
event: e,
internalState,
item: data as any,
itemType,
});
},
});
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) {
return;
}
// Don't trigger selection if clicking on interactive elements
const target = e.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
controls.onClick?.({
e.preventDefault();
controls.onMore?.({
event: e,
internalState,
item: data as any,
@@ -424,6 +511,7 @@ const PosterItemCard = ({
<div
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
onClick={handleClick}
onContextMenu={handleContextMenu}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -2,11 +2,30 @@ import {
ItemTableListInnerColumn,
TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { ItemListItem } from '/@/renderer/components/item-list/types';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
export const ActionsColumn = (props: ItemTableListInnerColumn) => {
const row: any = (props.data as (any | undefined)[])[props.rowIndex];
const handleActionClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
if (row !== undefined) {
props.controls.onMore?.({
event,
internalState: props.internalState,
item: row as ItemListItem,
itemType: props.itemType,
});
}
};
const handleActionDoubleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
};
if (row !== undefined) {
return (
<TableColumnContainer {...props}>
@@ -17,6 +36,8 @@ export const ActionsColumn = (props: ItemTableListInnerColumn) => {
color: 'muted',
size: 'md',
}}
onDoubleClick={handleActionDoubleClick}
onClick={handleActionClick}
size="xs"
variant="subtle"
/>
@@ -22,6 +22,8 @@ export const FavoriteColumn = (props: ItemTableListInnerColumn) => {
size: 'md',
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
props.controls.onFavorite?.({
event,
favorite: !row,
@@ -30,6 +32,10 @@ export const FavoriteColumn = (props: ItemTableListInnerColumn) => {
itemType: props.itemType,
});
}}
onDoubleClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
size="xs"
variant="subtle"
/>
@@ -36,6 +36,7 @@ import { Flex } from '/@/shared/components/flex/flex';
import { Icon } from '/@/shared/components/icon/icon';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text';
import { useDoubleClick } from '/@/shared/hooks/use-double-click';
import { LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
import { TableColumn } from '/@/shared/types/types';
@@ -426,19 +427,43 @@ export const TableColumnTextContainer = (
}
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
// Don't trigger row selection if clicking on interactive elements
const target = event.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
const handleClick = useDoubleClick({
onDoubleClick: (event: React.MouseEvent<HTMLDivElement>) => {
if (isDataRow && item) {
props.controls.onDoubleClick?.({
event,
internalState: props.internalState,
item: item as ItemListItem,
itemType: props.itemType,
});
}
},
onSingleClick: (event: React.MouseEvent<HTMLDivElement>) => {
// Don't trigger row selection if clicking on interactive elements
const target = event.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
if (isInteractiveElement) {
return;
}
if (isDataRow && item && props.enableSelection) {
props.controls.onClick?.({
if (isDataRow && item && props.enableSelection) {
props.controls.onClick?.({
event,
internalState: props.internalState,
item: item as ItemListItem,
itemType: props.itemType,
});
}
},
});
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
if (isDataRow && item) {
event.preventDefault();
props.controls.onMore?.({
event,
internalState: props.internalState,
item: item as ItemListItem,
@@ -478,7 +503,8 @@ export const TableColumnTextContainer = (
[styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn,
})}
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
onClick={handleCellClick}
onClick={handleClick}
onContextMenu={handleContextMenu}
ref={mergedRef}
style={props.style}
>
@@ -604,19 +630,43 @@ export const TableColumnContainer = (
}
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
// Don't trigger row selection if clicking on interactive elements
const target = event.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
const handleClick = useDoubleClick({
onDoubleClick: (event: React.MouseEvent<HTMLDivElement>) => {
if (isDataRow && item) {
props.controls.onDoubleClick?.({
event,
internalState: props.internalState,
item: item as ItemListItem,
itemType: props.itemType,
});
}
},
onSingleClick: (event: React.MouseEvent<HTMLDivElement>) => {
// Don't trigger row selection if clicking on interactive elements
const target = event.target as HTMLElement;
const isInteractiveElement = target.closest(
'button, a, input, select, textarea, [role="button"]',
);
if (isInteractiveElement) {
return;
}
if (isInteractiveElement) {
return;
}
if (isDataRow && item && props.enableSelection) {
props.controls.onClick?.({
if (isDataRow && item && props.enableSelection) {
props.controls.onClick?.({
event,
internalState: props.internalState,
item: item as ItemListItem,
itemType: props.itemType,
});
}
},
});
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
if (isDataRow && item) {
event.preventDefault();
props.controls.onMore?.({
event,
internalState: props.internalState,
item: item as ItemListItem,
@@ -656,7 +706,8 @@ export const TableColumnContainer = (
[styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn,
})}
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
onClick={handleCellClick}
onClick={handleClick}
onContextMenu={handleContextMenu}
ref={mergedRef}
style={{ ...props.containerStyle, ...props.style }}
>
+48
View File
@@ -0,0 +1,48 @@
import { useCallback, useEffect, useRef } from 'react';
export const useDoubleClick = ({
latency = 160,
onDoubleClick = () => null,
onSingleClick = () => null,
}: {
latency?: number;
onDoubleClick?: (e: any) => void;
onSingleClick?: (e: any) => void;
}) => {
const clickCountRef = useRef(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleClick = useCallback(
(e: any) => {
clickCountRef.current += 1;
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set a new timeout to determine if it's a single or double click
timeoutRef.current = setTimeout(() => {
if (clickCountRef.current === 1) {
onSingleClick(e);
} else if (clickCountRef.current === 2) {
onDoubleClick(e);
}
clickCountRef.current = 0;
}, latency);
},
[latency, onDoubleClick, onSingleClick],
);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return handleClick;
};