add utils to handle list multiselect / expand states

This commit is contained in:
jeffvli
2025-09-25 19:20:12 -07:00
parent 69b31e15f7
commit 074867fbe5
6 changed files with 638 additions and 124 deletions
@@ -0,0 +1,217 @@
import clsx from 'clsx';
import { AnimatePresence, motion, Variants } from 'motion/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import {
CSSProperties,
forwardRef,
memo,
ReactNode,
Ref,
RefObject,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { GridComponents, VirtuosoGrid, VirtuosoGridHandle } from 'react-virtuoso';
import { ItemListItem, ItemListStateActions, useItemListState } from '../helpers/item-list-state';
import styles from './item-grid.module.css';
import { ItemCard } from '/@/renderer/components/item-card/item-card';
const gridComponents: GridComponents<any> = {
Item: forwardRef<
HTMLDivElement,
{
children?: ReactNode;
className?: string;
context?: ItemContext;
'data-index': number;
enableExpanded?: boolean;
style?: CSSProperties;
virtuosoRef?: RefObject<VirtuosoGridHandle>;
}
>((props, ref) => {
const { children, context, 'data-index': index } = props;
return (
<div className={clsx(styles.gridItemComponent)} ref={ref}>
{children}
</div>
);
}),
List: forwardRef<
HTMLDivElement,
{ children?: ReactNode; className?: string; style?: CSSProperties }
>((props, ref) => {
const { children, className, style, ...rest } = props;
return (
<div
className={clsx(styles.gridListComponent, className)}
ref={ref}
style={{ ...style }}
{...rest}
>
{children}
</div>
);
}),
};
interface ItemContext {
actions: ItemListStateActions;
enableExpansion?: boolean;
enableSelection?: boolean;
onItemClick?: (item: unknown, index: number) => void;
onItemContextMenu?: (item: unknown, index: number) => void;
onItemDoubleClick?: (item: unknown, index: number) => void;
}
interface ItemGridProps {
data: unknown[];
enableExpansion?: boolean;
enableSelection?: boolean;
onItemClick?: (item: unknown, index: number) => void;
onItemContextMenu?: (item: unknown, index: number) => void;
onItemDoubleClick?: (item: unknown, index: number) => void;
ref: Ref<VirtuosoGridHandle>;
totalItemCount?: number;
}
const expandedAnimationVariants: Variants = {
hidden: {
height: 0,
maxHeight: 0,
},
show: {
height: '40dvh',
maxHeight: '500px',
transition: {
duration: 0.3,
ease: 'easeInOut',
},
},
};
export const ItemGrid = ({
data,
enableExpansion = false,
enableSelection = false,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
ref,
totalItemCount,
}: ItemGridProps) => {
const rootRef = useRef(null);
const [scroller, setScroller] = useState<HTMLElement | null>(null);
const actions = useItemListState();
const [initialize, osInstance] = useOverlayScrollbars({
defer: true,
options: {
overflow: { x: 'hidden', y: 'scroll' },
paddingAbsolute: true,
scrollbars: {
autoHide: 'leave',
autoHideDelay: 500,
pointers: ['mouse', 'pen', 'touch'],
theme: 'feishin-os-scrollbar',
visibility: 'visible',
},
},
});
useEffect(() => {
const { current: root } = rootRef;
if (scroller && root) {
initialize({
elements: { viewport: scroller },
target: root,
});
}
return () => osInstance()?.destroy();
}, [scroller, initialize, osInstance]);
const itemContext = useMemo(
() => ({
actions,
enableExpansion,
enableSelection,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
}),
[
actions,
enableExpansion,
enableSelection,
onItemClick,
onItemDoubleClick,
onItemContextMenu,
],
);
const hasExpanded = actions.hasExpanded();
return (
<div className={styles.itemGridContainer}>
<div
className={styles.gridListContainer}
data-overlayscrollbars-initialize=""
ref={rootRef}
>
<VirtuosoGrid
components={gridComponents}
context={itemContext}
data={data}
increaseViewportBy={200}
itemContent={itemContent}
ref={ref}
scrollerRef={setScroller}
totalCount={totalItemCount || data.length}
/>
</div>
<AnimatePresence>
{hasExpanded && (
<motion.div
animate="show"
className={styles.gridExpandedContainer}
exit="hidden"
initial="hidden"
variants={expandedAnimationVariants}
>
Hello World
</motion.div>
)}
</AnimatePresence>
</div>
);
};
const itemContent = (index: number, item: any, context: ItemContext) => {
return <InnerItem context={context} index={index} item={item} />;
};
const InnerItem = memo(
({ context, index, item }: { context: ItemContext; index: number; item: ItemListItem }) => {
const handleClick = () => {
context.actions.toggleExpanded({ id: item.id, itemType: item.itemType });
};
return (
<ItemCard
data={item as any}
onClick={handleClick}
onItemExpand={() => context.onItemDoubleClick?.(item, index)}
withControls
/>
);
},
);