pass generics onto the components themselves

This commit is contained in:
Nate Kelley 2025-08-02 23:38:48 -06:00
parent 38e2bbafba
commit 21b3c22e5c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 221 additions and 235 deletions

View File

@ -29,9 +29,6 @@ function BusterInfiniteListComponent<T = any>({
rowClassName = '', rowClassName = '',
scrollEndThreshold = 48 // Default threshold of 200px scrollEndThreshold = 48 // Default threshold of 200px
}: BusterInfiniteListProps<T>) { }: BusterInfiniteListProps<T>) {
const Header = BusterListHeader<T>();
const RowSelector = BusterListRowComponentSelector<T>();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
const lastChildIndex = useMemo(() => { const lastChildIndex = useMemo(() => {
@ -139,7 +136,7 @@ function BusterInfiniteListComponent<T = any>({
return ( return (
<div ref={containerRef} className="infinite-list-container relative"> <div ref={containerRef} className="infinite-list-container relative">
{showHeader && !showEmptyState && ( {showHeader && !showEmptyState && (
<Header <BusterListHeader<T>
columns={columns} columns={columns}
onGlobalSelectChange={onSelectChange ? onGlobalSelectChange : undefined} onGlobalSelectChange={onSelectChange ? onGlobalSelectChange : undefined}
globalCheckStatus={globalCheckStatus} globalCheckStatus={globalCheckStatus}
@ -153,7 +150,7 @@ function BusterInfiniteListComponent<T = any>({
rows rows
.filter((row) => !row.hidden) .filter((row) => !row.hidden)
.map((row, index) => ( .map((row, index) => (
<RowSelector <BusterListRowComponentSelector<T>
key={row.id} key={row.id}
row={row} row={row}
id={row.id} id={row.id}

View File

@ -5,70 +5,70 @@ import { CheckboxColumn } from './CheckboxColumn';
import { HEIGHT_OF_HEADER } from './config'; import { HEIGHT_OF_HEADER } from './config';
import type { BusterListColumn } from './interfaces'; import type { BusterListColumn } from './interfaces';
export const BusterListHeader = interface BusterListHeaderProps<T> {
<T = unknown,>(): React.FC<{ columns: BusterListColumn<T>[];
columns: BusterListColumn<T>[]; onGlobalSelectChange?: (v: boolean) => void;
onGlobalSelectChange?: (v: boolean) => void; globalCheckStatus?: 'checked' | 'unchecked' | 'indeterminate';
globalCheckStatus?: 'checked' | 'unchecked' | 'indeterminate'; showSelectAll?: boolean;
showSelectAll?: boolean; rowsLength: number;
rowsLength: number; rowClassName: string;
rowClassName: string; }
}> =>
({
columns,
rowClassName,
showSelectAll = true,
onGlobalSelectChange,
globalCheckStatus,
rowsLength
}) => {
const showCheckboxColumn = !!onGlobalSelectChange;
const showGlobalCheckbox =
globalCheckStatus === 'indeterminate' || globalCheckStatus === 'checked';
return ( export const BusterListHeader = <T = unknown,>({
<div columns,
className={cn( rowClassName,
'group border-border flex items-center justify-start border-b pr-6', showSelectAll = true,
{ onGlobalSelectChange,
'pl-3.5': !onGlobalSelectChange globalCheckStatus,
}, rowsLength
rowClassName }: BusterListHeaderProps<T>) => {
)} const showCheckboxColumn = !!onGlobalSelectChange;
style={{ const showGlobalCheckbox =
height: `${HEIGHT_OF_HEADER}px`, globalCheckStatus === 'indeterminate' || globalCheckStatus === 'checked';
minHeight: `${HEIGHT_OF_HEADER}px`
}}>
{showCheckboxColumn && (
<CheckboxColumn
checkStatus={globalCheckStatus}
onChange={onGlobalSelectChange}
className={cn({
'opacity-100': showGlobalCheckbox,
'invisible!': rowsLength === 0,
'pointer-events-none invisible!': !showSelectAll
})}
/>
)}
{columns.map((column, index) => ( return (
<div <div
className="header-cell flex h-full items-center p-0" className={cn(
key={String(column.dataIndex)} 'group border-border flex items-center justify-start border-b pr-6',
style={{ {
width: column.width || '100%', 'pl-3.5': !onGlobalSelectChange
flex: column.width ? 'none' : 1, },
paddingLeft: showCheckboxColumn ? undefined : '0px' rowClassName
}}> )}
{column.headerRender ? ( style={{
column.headerRender(column.title) height: `${HEIGHT_OF_HEADER}px`,
) : ( minHeight: `${HEIGHT_OF_HEADER}px`
<Text size="sm" variant="secondary" truncate> }}>
{column.title} {showCheckboxColumn && (
</Text> <CheckboxColumn
)} checkStatus={globalCheckStatus}
</div> onChange={onGlobalSelectChange}
))} className={cn({
</div> 'opacity-100': showGlobalCheckbox,
); 'invisible!': rowsLength === 0,
}; 'pointer-events-none invisible!': !showSelectAll
})}
/>
)}
{columns.map((column, index) => (
<div
className="header-cell flex h-full items-center p-0"
key={String(column.dataIndex)}
style={{
width: column.width || '100%',
flex: column.width ? 'none' : 1,
paddingLeft: showCheckboxColumn ? undefined : '0px'
}}>
{column.headerRender ? (
column.headerRender(column.title)
) : (
<Text size="sm" variant="secondary" truncate>
{column.title}
</Text>
)}
</div>
))}
</div>
);
};

View File

@ -12,104 +12,101 @@ import type {
BusterListRowItem BusterListRowItem
} from './interfaces'; } from './interfaces';
export const BusterListRowComponent = <T = unknown,>() => interface BusterListRowComponentProps<T> {
React.memo( row: BusterListRow;
React.forwardRef< columns: BusterListColumn<T>[];
HTMLDivElement, checked: boolean;
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void;
onContextMenuClick?: (e: React.MouseEvent<HTMLDivElement>, id: string) => void;
style?: React.CSSProperties;
hideLastRowBorder: NonNullable<BusterListProps['hideLastRowBorder']>;
useRowClickSelectChange: boolean;
rowClassName?: string;
isLastChild: boolean;
}
export const BusterListRowComponent = React.memo(
React.forwardRef(
<T,>(
{ {
row: BusterListRow; style,
columns: BusterListColumn<T>[]; hideLastRowBorder,
checked: boolean; row,
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void; columns,
onContextMenuClick?: (e: React.MouseEvent<HTMLDivElement>, id: string) => void; onSelectChange,
style?: React.CSSProperties; checked,
hideLastRowBorder: NonNullable<BusterListProps['hideLastRowBorder']>; onContextMenuClick,
useRowClickSelectChange: boolean; rowClassName = '',
rowClassName?: string; isLastChild,
isLastChild: boolean; useRowClickSelectChange
} }: BusterListRowComponentProps<T>,
>( ref: React.ForwardedRef<HTMLDivElement>
( ) => {
{ const link = row.link;
style,
hideLastRowBorder,
row,
columns,
onSelectChange,
checked,
onContextMenuClick,
rowClassName = '',
isLastChild,
useRowClickSelectChange
},
ref
) => {
const link = row.link;
const onContextMenu = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => { const onContextMenu = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => {
onContextMenuClick?.(e, row.id); onContextMenuClick?.(e, row.id);
}); });
const onChange = useMemoizedFn((newChecked: boolean, e: React.MouseEvent) => { const onChange = useMemoizedFn((newChecked: boolean, e: React.MouseEvent) => {
onSelectChange?.(newChecked, row.id, e); onSelectChange?.(newChecked, row.id, e);
}); });
const onContainerClick = useMemoizedFn((e: React.MouseEvent) => { const onContainerClick = useMemoizedFn((e: React.MouseEvent) => {
if (useRowClickSelectChange) { if (useRowClickSelectChange) {
onChange(!checked, e); onChange(!checked, e);
} }
row.onClick?.(); row.onClick?.();
}); });
const rowStyles = { const rowStyles = {
height: `${HEIGHT_OF_ROW}px`, height: `${HEIGHT_OF_ROW}px`,
minHeight: `${HEIGHT_OF_ROW}px`, minHeight: `${HEIGHT_OF_ROW}px`,
...style ...style
}; };
return ( return (
<LinkWrapper href={link}> <LinkWrapper href={link}>
<div <div
onClick={onContainerClick} onClick={onContainerClick}
style={rowStyles} style={rowStyles}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
data-testid={row.dataTestId} data-testid={row.dataTestId}
className={cn( className={cn(
'border-border flex items-center border-b pr-6', 'border-border flex items-center border-b pr-6',
checked ? 'bg-primary-background hover:bg-primary-background-hover' : '', checked ? 'bg-primary-background hover:bg-primary-background-hover' : '',
isLastChild && hideLastRowBorder ? 'border-b-0!' : '', isLastChild && hideLastRowBorder ? 'border-b-0!' : '',
!onSelectChange ? 'pl-3.5' : '', !onSelectChange ? 'pl-3.5' : '',
link || row.onClick || (onSelectChange && useRowClickSelectChange) link || row.onClick || (onSelectChange && useRowClickSelectChange)
? 'hover:bg-item-hover cursor-pointer' ? 'hover:bg-item-hover cursor-pointer'
: '', : '',
rowClassName, rowClassName,
'group' 'group'
)} )}
ref={ref}> ref={ref}>
{onSelectChange ? ( {onSelectChange ? (
<CheckboxColumn <CheckboxColumn checkStatus={checked ? 'checked' : 'unchecked'} onChange={onChange} />
checkStatus={checked ? 'checked' : 'unchecked'} ) : null}
onChange={onChange} {columns.map((column, columnIndex) => (
/> <BusterListCellComponent
) : null} key={String(column.dataIndex)}
{columns.map((column, columnIndex) => ( data={get(row.data, column.dataIndex)}
<BusterListCellComponent row={row}
key={String(column.dataIndex)} render={column.render as any}
data={get(row.data, column.dataIndex)} isFirstCell={columnIndex === 0}
row={row} isLastCell={columnIndex === columns.length - 1}
render={column.render as any} width={column.width}
isFirstCell={columnIndex === 0} onSelectChange={onSelectChange}
isLastCell={columnIndex === columns.length - 1} />
width={column.width} ))}
onSelectChange={onSelectChange} </div>
/> </LinkWrapper>
))} );
</div> }
</LinkWrapper> )
); ) as any as <T = unknown>(
} props: BusterListRowComponentProps<T> & React.RefAttributes<HTMLDivElement>
) ) => React.ReactElement | null;
);
const BusterListCellComponent: React.FC<{ const BusterListCellComponent: React.FC<{
data: string | number | React.ReactNode; data: string | number | React.ReactNode;

View File

@ -3,80 +3,75 @@ import { BusterListRowComponent } from './BusterListRowComponent';
import { BusterListSectionComponent } from './BusterListSectionComponent'; import { BusterListSectionComponent } from './BusterListSectionComponent';
import type { BusterListColumn, BusterListProps, BusterListRow } from './interfaces'; import type { BusterListColumn, BusterListProps, BusterListRow } from './interfaces';
export const BusterListRowComponentSelector = <T = unknown,>() => { interface BusterListRowComponentSelectorProps<T> {
// const RowComponent = BusterListRowComponent<T>(); row: BusterListRow;
columns: BusterListColumn<T>[];
id: string;
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void;
onSelectSectionChange?: (v: boolean, id: string) => void;
onContextMenuClick?: (e: React.MouseEvent<HTMLDivElement>, id: string) => void;
selectedRowKeys?: string[];
rows: BusterListRow[];
style?: React.CSSProperties;
hideLastRowBorder: NonNullable<BusterListProps['hideLastRowBorder']>;
rowClassName?: string;
isLastChild: boolean;
useRowClickSelectChange?: boolean;
}
return React.forwardRef< export const BusterListRowComponentSelector = React.forwardRef(
HTMLDivElement, <T,>(
{ {
row: BusterListRow; style,
columns: BusterListColumn<T>[]; row,
id: string; rows,
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void; columns,
onSelectSectionChange?: (v: boolean, id: string) => void; isLastChild,
onContextMenuClick?: (e: React.MouseEvent<HTMLDivElement>, id: string) => void; onSelectChange,
selectedRowKeys?: string[]; onSelectSectionChange,
rows: BusterListRow[]; selectedRowKeys,
style?: React.CSSProperties; onContextMenuClick,
hideLastRowBorder: NonNullable<BusterListProps['hideLastRowBorder']>; hideLastRowBorder,
rowClassName?: string; rowClassName,
isLastChild: boolean; useRowClickSelectChange = false
useRowClickSelectChange?: boolean; }: BusterListRowComponentSelectorProps<T>,
} ref: React.ForwardedRef<HTMLDivElement>
>( ) => {
( if (row.hidden) return null;
{
style,
row,
rows,
columns,
isLastChild,
onSelectChange,
onSelectSectionChange,
selectedRowKeys,
onContextMenuClick,
hideLastRowBorder,
rowClassName,
useRowClickSelectChange = false
},
ref
) => {
if (row.hidden) return null;
if (row.rowSection) {
return (
<BusterListSectionComponent
style={style}
rowSection={row.rowSection}
ref={ref}
id={row.id}
key={row.id}
rows={rows}
selectedRowKeys={selectedRowKeys}
rowClassName={rowClassName}
onSelectSectionChange={onSelectSectionChange}
/>
);
}
const RowComponent = BusterListRowComponent<T>();
if (row.rowSection) {
return ( return (
<RowComponent <BusterListSectionComponent
style={style} style={style}
row={row} rowSection={row.rowSection}
columns={columns}
key={row.id}
rowClassName={rowClassName}
onSelectChange={onSelectChange}
checked={!!selectedRowKeys?.includes(row.id)}
ref={ref} ref={ref}
onContextMenuClick={onContextMenuClick} id={row.id}
hideLastRowBorder={hideLastRowBorder} key={row.id}
useRowClickSelectChange={useRowClickSelectChange} rows={rows}
isLastChild={isLastChild} selectedRowKeys={selectedRowKeys}
rowClassName={rowClassName}
onSelectSectionChange={onSelectSectionChange}
/> />
); );
} }
);
}; return (
<BusterListRowComponent<T>
style={style}
row={row}
columns={columns}
key={row.id}
rowClassName={rowClassName}
onSelectChange={onSelectChange}
checked={!!selectedRowKeys?.includes(row.id)}
ref={ref}
onContextMenuClick={onContextMenuClick}
hideLastRowBorder={hideLastRowBorder}
useRowClickSelectChange={useRowClickSelectChange}
isLastChild={isLastChild}
/>
);
}
) as any as <T = unknown>(
props: BusterListRowComponentSelectorProps<T> & React.RefAttributes<HTMLDivElement>
) => React.ReactElement | null;

View File

@ -23,9 +23,6 @@ function BusterListVirtuaComponent<T = any>({
rowClassName = '', rowClassName = '',
hideLastRowBorder = false hideLastRowBorder = false
}: BusterListProps<T>) { }: BusterListProps<T>) {
const Header = BusterListHeader<T>();
const RowSelector = BusterListRowComponentSelector<T>();
const showEmptyState = (!rows || rows.length === 0) && !!emptyState; const showEmptyState = (!rows || rows.length === 0) && !!emptyState;
const lastChildIndex = rows.length - 1; const lastChildIndex = rows.length - 1;
const lastSelectedIdRef = useRef<string | null>(null); const lastSelectedIdRef = useRef<string | null>(null);
@ -126,7 +123,7 @@ function BusterListVirtuaComponent<T = any>({
<WrapperNode {...wrapperNodeProps}> <WrapperNode {...wrapperNodeProps}>
<div className="list-container relative flex h-full w-full flex-col overflow-hidden"> <div className="list-container relative flex h-full w-full flex-col overflow-hidden">
{showHeader && !showEmptyState && ( {showHeader && !showEmptyState && (
<Header <BusterListHeader<T>
columns={columns} columns={columns}
onGlobalSelectChange={onSelectChange ? onGlobalSelectChange : undefined} onGlobalSelectChange={onSelectChange ? onGlobalSelectChange : undefined}
globalCheckStatus={globalCheckStatus} globalCheckStatus={globalCheckStatus}
@ -140,7 +137,7 @@ function BusterListVirtuaComponent<T = any>({
<VList overscan={10}> <VList overscan={10}>
{rows.map((row, index) => ( {rows.map((row, index) => (
<div key={row.id + index.toString()} style={{ height: itemSize(index) }}> <div key={row.id + index.toString()} style={{ height: itemSize(index) }}>
<RowSelector <BusterListRowComponentSelector<T>
row={row} row={row}
id={row.id} id={row.id}
isLastChild={index === lastChildIndex} isLastChild={index === lastChildIndex}