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

View File

@ -5,70 +5,70 @@ import { CheckboxColumn } from './CheckboxColumn';
import { HEIGHT_OF_HEADER } from './config';
import type { BusterListColumn } from './interfaces';
export const BusterListHeader =
<T = unknown,>(): React.FC<{
columns: BusterListColumn<T>[];
onGlobalSelectChange?: (v: boolean) => void;
globalCheckStatus?: 'checked' | 'unchecked' | 'indeterminate';
showSelectAll?: boolean;
rowsLength: number;
rowClassName: string;
}> =>
({
columns,
rowClassName,
showSelectAll = true,
onGlobalSelectChange,
globalCheckStatus,
rowsLength
}) => {
const showCheckboxColumn = !!onGlobalSelectChange;
const showGlobalCheckbox =
globalCheckStatus === 'indeterminate' || globalCheckStatus === 'checked';
interface BusterListHeaderProps<T> {
columns: BusterListColumn<T>[];
onGlobalSelectChange?: (v: boolean) => void;
globalCheckStatus?: 'checked' | 'unchecked' | 'indeterminate';
showSelectAll?: boolean;
rowsLength: number;
rowClassName: string;
}
return (
<div
className={cn(
'group border-border flex items-center justify-start border-b pr-6',
{
'pl-3.5': !onGlobalSelectChange
},
rowClassName
)}
style={{
height: `${HEIGHT_OF_HEADER}px`,
minHeight: `${HEIGHT_OF_HEADER}px`
}}>
{showCheckboxColumn && (
<CheckboxColumn
checkStatus={globalCheckStatus}
onChange={onGlobalSelectChange}
className={cn({
'opacity-100': showGlobalCheckbox,
'invisible!': rowsLength === 0,
'pointer-events-none invisible!': !showSelectAll
})}
/>
)}
export const BusterListHeader = <T = unknown,>({
columns,
rowClassName,
showSelectAll = true,
onGlobalSelectChange,
globalCheckStatus,
rowsLength
}: BusterListHeaderProps<T>) => {
const showCheckboxColumn = !!onGlobalSelectChange;
const showGlobalCheckbox =
globalCheckStatus === 'indeterminate' || globalCheckStatus === 'checked';
{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>
);
};
return (
<div
className={cn(
'group border-border flex items-center justify-start border-b pr-6',
{
'pl-3.5': !onGlobalSelectChange
},
rowClassName
)}
style={{
height: `${HEIGHT_OF_HEADER}px`,
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) => (
<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
} from './interfaces';
export const BusterListRowComponent = <T = unknown,>() =>
React.memo(
React.forwardRef<
HTMLDivElement,
interface BusterListRowComponentProps<T> {
row: BusterListRow;
columns: BusterListColumn<T>[];
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;
columns: BusterListColumn<T>[];
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;
}
>(
(
{
style,
hideLastRowBorder,
row,
columns,
onSelectChange,
checked,
onContextMenuClick,
rowClassName = '',
isLastChild,
useRowClickSelectChange
},
ref
) => {
const link = row.link;
style,
hideLastRowBorder,
row,
columns,
onSelectChange,
checked,
onContextMenuClick,
rowClassName = '',
isLastChild,
useRowClickSelectChange
}: BusterListRowComponentProps<T>,
ref: React.ForwardedRef<HTMLDivElement>
) => {
const link = row.link;
const onContextMenu = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => {
onContextMenuClick?.(e, row.id);
});
const onContextMenu = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => {
onContextMenuClick?.(e, row.id);
});
const onChange = useMemoizedFn((newChecked: boolean, e: React.MouseEvent) => {
onSelectChange?.(newChecked, row.id, e);
});
const onChange = useMemoizedFn((newChecked: boolean, e: React.MouseEvent) => {
onSelectChange?.(newChecked, row.id, e);
});
const onContainerClick = useMemoizedFn((e: React.MouseEvent) => {
if (useRowClickSelectChange) {
onChange(!checked, e);
}
row.onClick?.();
});
const onContainerClick = useMemoizedFn((e: React.MouseEvent) => {
if (useRowClickSelectChange) {
onChange(!checked, e);
}
row.onClick?.();
});
const rowStyles = {
height: `${HEIGHT_OF_ROW}px`,
minHeight: `${HEIGHT_OF_ROW}px`,
...style
};
const rowStyles = {
height: `${HEIGHT_OF_ROW}px`,
minHeight: `${HEIGHT_OF_ROW}px`,
...style
};
return (
<LinkWrapper href={link}>
<div
onClick={onContainerClick}
style={rowStyles}
onContextMenu={onContextMenu}
data-testid={row.dataTestId}
className={cn(
'border-border flex items-center border-b pr-6',
checked ? 'bg-primary-background hover:bg-primary-background-hover' : '',
isLastChild && hideLastRowBorder ? 'border-b-0!' : '',
!onSelectChange ? 'pl-3.5' : '',
link || row.onClick || (onSelectChange && useRowClickSelectChange)
? 'hover:bg-item-hover cursor-pointer'
: '',
rowClassName,
'group'
)}
ref={ref}>
{onSelectChange ? (
<CheckboxColumn
checkStatus={checked ? 'checked' : 'unchecked'}
onChange={onChange}
/>
) : null}
{columns.map((column, columnIndex) => (
<BusterListCellComponent
key={String(column.dataIndex)}
data={get(row.data, column.dataIndex)}
row={row}
render={column.render as any}
isFirstCell={columnIndex === 0}
isLastCell={columnIndex === columns.length - 1}
width={column.width}
onSelectChange={onSelectChange}
/>
))}
</div>
</LinkWrapper>
);
}
)
);
return (
<LinkWrapper href={link}>
<div
onClick={onContainerClick}
style={rowStyles}
onContextMenu={onContextMenu}
data-testid={row.dataTestId}
className={cn(
'border-border flex items-center border-b pr-6',
checked ? 'bg-primary-background hover:bg-primary-background-hover' : '',
isLastChild && hideLastRowBorder ? 'border-b-0!' : '',
!onSelectChange ? 'pl-3.5' : '',
link || row.onClick || (onSelectChange && useRowClickSelectChange)
? 'hover:bg-item-hover cursor-pointer'
: '',
rowClassName,
'group'
)}
ref={ref}>
{onSelectChange ? (
<CheckboxColumn checkStatus={checked ? 'checked' : 'unchecked'} onChange={onChange} />
) : null}
{columns.map((column, columnIndex) => (
<BusterListCellComponent
key={String(column.dataIndex)}
data={get(row.data, column.dataIndex)}
row={row}
render={column.render as any}
isFirstCell={columnIndex === 0}
isLastCell={columnIndex === columns.length - 1}
width={column.width}
onSelectChange={onSelectChange}
/>
))}
</div>
</LinkWrapper>
);
}
)
) as any as <T = unknown>(
props: BusterListRowComponentProps<T> & React.RefAttributes<HTMLDivElement>
) => React.ReactElement | null;
const BusterListCellComponent: React.FC<{
data: string | number | React.ReactNode;

View File

@ -3,80 +3,75 @@ import { BusterListRowComponent } from './BusterListRowComponent';
import { BusterListSectionComponent } from './BusterListSectionComponent';
import type { BusterListColumn, BusterListProps, BusterListRow } from './interfaces';
export const BusterListRowComponentSelector = <T = unknown,>() => {
// const RowComponent = BusterListRowComponent<T>();
interface BusterListRowComponentSelectorProps<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<
HTMLDivElement,
export const BusterListRowComponentSelector = React.forwardRef(
<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;
}
>(
(
{
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>();
style,
row,
rows,
columns,
isLastChild,
onSelectChange,
onSelectSectionChange,
selectedRowKeys,
onContextMenuClick,
hideLastRowBorder,
rowClassName,
useRowClickSelectChange = false
}: BusterListRowComponentSelectorProps<T>,
ref: React.ForwardedRef<HTMLDivElement>
) => {
if (row.hidden) return null;
if (row.rowSection) {
return (
<RowComponent
<BusterListSectionComponent
style={style}
row={row}
columns={columns}
key={row.id}
rowClassName={rowClassName}
onSelectChange={onSelectChange}
checked={!!selectedRowKeys?.includes(row.id)}
rowSection={row.rowSection}
ref={ref}
onContextMenuClick={onContextMenuClick}
hideLastRowBorder={hideLastRowBorder}
useRowClickSelectChange={useRowClickSelectChange}
isLastChild={isLastChild}
id={row.id}
key={row.id}
rows={rows}
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 = '',
hideLastRowBorder = false
}: BusterListProps<T>) {
const Header = BusterListHeader<T>();
const RowSelector = BusterListRowComponentSelector<T>();
const showEmptyState = (!rows || rows.length === 0) && !!emptyState;
const lastChildIndex = rows.length - 1;
const lastSelectedIdRef = useRef<string | null>(null);
@ -126,7 +123,7 @@ function BusterListVirtuaComponent<T = any>({
<WrapperNode {...wrapperNodeProps}>
<div className="list-container relative flex h-full w-full flex-col overflow-hidden">
{showHeader && !showEmptyState && (
<Header
<BusterListHeader<T>
columns={columns}
onGlobalSelectChange={onSelectChange ? onGlobalSelectChange : undefined}
globalCheckStatus={globalCheckStatus}
@ -140,7 +137,7 @@ function BusterListVirtuaComponent<T = any>({
<VList overscan={10}>
{rows.map((row, index) => (
<div key={row.id + index.toString()} style={{ height: itemSize(index) }}>
<RowSelector
<BusterListRowComponentSelector<T>
row={row}
id={row.id}
isLastChild={index === lastChildIndex}