add select all hotkey to lists

This commit is contained in:
jeffvli
2025-11-27 14:17:36 -08:00
parent 092a9c3f19
commit 7cc7086dbb
4 changed files with 86 additions and 16 deletions
@@ -77,6 +77,7 @@ export interface ItemListStateActions {
clearDragging: () => void; clearDragging: () => void;
clearExpanded: () => void; clearExpanded: () => void;
clearSelected: () => void; clearSelected: () => void;
deselectAll: () => void;
extractRowId: (item: unknown) => string | undefined; extractRowId: (item: unknown) => string | undefined;
findItemIndex: (rowId: string) => number; findItemIndex: (rowId: string) => number;
getData: () => unknown[]; getData: () => unknown[];
@@ -91,9 +92,12 @@ export interface ItemListStateActions {
hasDragging: () => boolean; hasDragging: () => boolean;
hasExpanded: () => boolean; hasExpanded: () => boolean;
hasSelected: () => boolean; hasSelected: () => boolean;
isAllSelected: () => boolean;
isDragging: (rowId: string) => boolean; isDragging: (rowId: string) => boolean;
isExpanded: (rowId: string) => boolean; isExpanded: (rowId: string) => boolean;
isSelected: (rowId: string) => boolean; isSelected: (rowId: string) => boolean;
isSomeSelected: () => boolean;
selectAll: () => void;
setDragging: (items: ItemListStateItemWithRequiredProperties[]) => void; setDragging: (items: ItemListStateItemWithRequiredProperties[]) => void;
setExpanded: (items: ItemListStateItemWithRequiredProperties[]) => void; setExpanded: (items: ItemListStateItemWithRequiredProperties[]) => void;
setSelected: (items: ItemListStateItemWithRequiredProperties[]) => void; setSelected: (items: ItemListStateItemWithRequiredProperties[]) => void;
@@ -587,6 +591,29 @@ export const useItemListState = (
[getDataFn, extractRowId], [getDataFn, extractRowId],
); );
const selectAll = useCallback(() => {
const data = getDataFn ? getDataFn() : [];
const items = data
.filter((d) => d && typeof d === 'object')
.map((d) => d as ItemListStateItemWithRequiredProperties);
store.dispatch({ extractRowId: extractRowIdFn, payload: items, type: 'SET_SELECTED' });
}, [extractRowIdFn, getDataFn, store]);
const deselectAll = useCallback(() => {
store.dispatch({ type: 'CLEAR_SELECTED' });
}, [store]);
const isAllSelected = useCallback(() => {
const state = getCurrentState();
const data = getDataFn ? getDataFn() : [];
return state.selected.size === data.filter((d) => d && typeof d === 'object').length;
}, [getCurrentState, getDataFn]);
const isSomeSelected = useCallback(() => {
const state = getCurrentState();
return state.selected.size > 0;
}, [getCurrentState]);
// Expose the store so components can subscribe if needed // Expose the store so components can subscribe if needed
// Store it in the actions object for access // Store it in the actions object for access
const actions = useMemo(() => { const actions = useMemo(() => {
@@ -597,6 +624,7 @@ export const useItemListState = (
clearDragging, clearDragging,
clearExpanded, clearExpanded,
clearSelected, clearSelected,
deselectAll,
extractRowId: extractRowIdFn, extractRowId: extractRowIdFn,
findItemIndex, findItemIndex,
getData, getData,
@@ -611,9 +639,12 @@ export const useItemListState = (
hasDragging, hasDragging,
hasExpanded, hasExpanded,
hasSelected, hasSelected,
isAllSelected,
isDragging, isDragging,
isExpanded, isExpanded,
isSelected, isSelected,
isSomeSelected,
selectAll,
setDragging, setDragging,
setExpanded, setExpanded,
setSelected, setSelected,
@@ -625,10 +656,15 @@ export const useItemListState = (
}; };
return actionsObj; return actionsObj;
}, [ }, [
getCurrentState,
store,
clearAll, clearAll,
clearDragging, clearDragging,
clearExpanded, clearExpanded,
clearSelected, clearSelected,
isAllSelected,
isSomeSelected,
deselectAll,
extractRowIdFn, extractRowIdFn,
findItemIndex, findItemIndex,
getData, getData,
@@ -646,13 +682,12 @@ export const useItemListState = (
isDragging, isDragging,
isExpanded, isExpanded,
isSelected, isSelected,
selectAll,
setDragging, setDragging,
setExpanded, setExpanded,
setSelected, setSelected,
toggleExpanded, toggleExpanded,
toggleSelected, toggleSelected,
store,
getCurrentState,
]); ]);
return actions; return actions;
@@ -44,6 +44,8 @@ import {
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types'; import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
import { animationProps } from '/@/shared/components/animations/animation-props'; import { animationProps } from '/@/shared/components/animations/animation-props';
import { useElementSize } from '/@/shared/hooks/use-element-size'; import { useElementSize } from '/@/shared/hooks/use-element-size';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
@@ -288,7 +290,7 @@ const BaseItemGridList = ({
const outerRef = useRef(null); const outerRef = useRef(null);
const listRef = useRef<FixedSizeList<GridItemProps>>(null); const listRef = useRef<FixedSizeList<GridItemProps>>(null);
const { ref: containerRef, width: containerWidth } = useElementSize(); const { ref: containerRef, width: containerWidth } = useElementSize();
const containerFocusRef = useRef<HTMLDivElement | null>(null); const { focused, ref: containerFocusRef } = useFocusWithin();
const handleRef = useRef<ItemListHandle | null>(null); const handleRef = useRef<ItemListHandle | null>(null);
const mergedContainerRef = useMergedRef(containerRef, rootRef, containerFocusRef); const mergedContainerRef = useMergedRef(containerRef, rootRef, containerFocusRef);
@@ -654,6 +656,21 @@ const BaseItemGridList = ({
useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]); useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
useHotkeys([
[
'mod+a',
() => {
if (focused) {
if (internalState.isAllSelected()) {
internalState.deselectAll();
} else {
internalState.selectAll();
}
}
},
],
]);
return ( return (
<motion.div <motion.div
className={styles.itemGridContainer} className={styles.itemGridContainer}
@@ -41,6 +41,8 @@ import {
} from '/@/renderer/components/item-list/types'; } from '/@/renderer/components/item-list/types';
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context'; import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
import { animationProps } from '/@/shared/components/animations/animation-props'; import { animationProps } from '/@/shared/components/animations/animation-props';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/types'; import { TableColumn } from '/@/shared/types/types';
@@ -847,7 +849,9 @@ const BaseItemTableList = ({
const [showRightShadow, setShowRightShadow] = useState(false); const [showRightShadow, setShowRightShadow] = useState(false);
const [showTopShadow, setShowTopShadow] = useState(false); const [showTopShadow, setShowTopShadow] = useState(false);
const handleRef = useRef<ItemListHandle | null>(null); const handleRef = useRef<ItemListHandle | null>(null);
const containerFocusRef = useRef<HTMLDivElement | null>(null); const { focused, ref: focusRef } = useFocusWithin();
const containerRef = useRef<HTMLDivElement | null>(null);
const mergedContainerRef = useMergedRef(containerRef, focusRef);
const stickyHeaderRef = useRef<HTMLDivElement | null>(null); const stickyHeaderRef = useRef<HTMLDivElement | null>(null);
const stickyGroupRowRef = useRef<HTMLDivElement | null>(null); const stickyGroupRowRef = useRef<HTMLDivElement | null>(null);
@@ -856,7 +860,7 @@ const BaseItemTableList = ({
const stickyHeaderRightRef = useRef<HTMLDivElement | null>(null); const stickyHeaderRightRef = useRef<HTMLDivElement | null>(null);
const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({ const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({
containerRef: containerFocusRef, containerRef: containerRef,
enabled: enableHeader && enableStickyHeader, enabled: enableHeader && enableStickyHeader,
headerRef: pinnedRowRef, headerRef: pinnedRowRef,
mainGridRef: rowRef, mainGridRef: rowRef,
@@ -867,12 +871,12 @@ const BaseItemTableList = ({
// Update position and width of sticky header (scroll sync is handled in the hook) // Update position and width of sticky header (scroll sync is handled in the hook)
useEffect(() => { useEffect(() => {
if (!shouldShowStickyHeader || !stickyHeaderRef.current || !containerFocusRef.current) { if (!shouldShowStickyHeader || !stickyHeaderRef.current || !containerRef.current) {
return; return;
} }
const stickyHeader = stickyHeaderRef.current; const stickyHeader = stickyHeaderRef.current;
const container = containerFocusRef.current; const container = containerRef.current;
const updatePosition = () => { const updatePosition = () => {
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
@@ -914,7 +918,7 @@ const BaseItemTableList = ({
// Track total container width for autoSizeColumns // Track total container width for autoSizeColumns
useEffect(() => { useEffect(() => {
const el = containerFocusRef.current; const el = containerRef.current;
if (!el || !autoFitColumns) return; if (!el || !autoFitColumns) return;
const updateWidth = () => { const updateWidth = () => {
@@ -1495,7 +1499,7 @@ const BaseItemTableList = ({
stickyGroupIndex, stickyGroupIndex,
stickyTop: stickyGroupTop, stickyTop: stickyGroupTop,
} = useStickyTableGroupRows({ } = useStickyTableGroupRows({
containerRef: containerFocusRef, containerRef: containerRef,
enabled: enableStickyGroupRows && !!groups && groups.length > 0, enabled: enableStickyGroupRows && !!groups && groups.length > 0,
getRowHeight: getRowHeightWrapper, getRowHeight: getRowHeightWrapper,
groups, groups,
@@ -1510,16 +1514,12 @@ const BaseItemTableList = ({
// Update position and width of sticky group row // Update position and width of sticky group row
useEffect(() => { useEffect(() => {
if ( if (!shouldRenderStickyGroupRow || !stickyGroupRowRef.current || !containerRef.current) {
!shouldRenderStickyGroupRow ||
!stickyGroupRowRef.current ||
!containerFocusRef.current
) {
return; return;
} }
const stickyGroupRow = stickyGroupRowRef.current; const stickyGroupRow = stickyGroupRowRef.current;
const container = containerFocusRef.current; const container = containerRef.current;
const updatePosition = () => { const updatePosition = () => {
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
@@ -2109,6 +2109,21 @@ const BaseItemTableList = ({
stickyGroupTop, stickyGroupTop,
]); ]);
useHotkeys([
[
'mod+a',
() => {
if (focused) {
if (internalState.isAllSelected()) {
internalState.deselectAll();
} else {
internalState.selectAll();
}
}
},
],
]);
return ( return (
<motion.div <motion.div
className={styles.itemTableListContainer} className={styles.itemTableListContainer}
@@ -2120,7 +2135,7 @@ const BaseItemTableList = ({
element.focus({ preventScroll: true }); element.focus({ preventScroll: true });
} }
}} }}
ref={containerFocusRef} ref={mergedContainerRef}
tabIndex={0} tabIndex={0}
{...animationProps.fadeIn} {...animationProps.fadeIn}
transition={{ duration: 1, ease: 'anticipate' }} transition={{ duration: 1, ease: 'anticipate' }}
+3
View File
@@ -0,0 +1,3 @@
import { useFocusWithin as useMantineFocusWithin } from '@mantine/hooks';
export const useFocusWithin = useMantineFocusWithin;