Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions core/src/components/Table/Table.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
--rex-table-cell-hover-background-color: #efefef;
--rex-table-cell-active-background-color: #dfdfdf;
--rex-table-cell-selected-background-color: #c4d9ef;
--rex-table-cell-selected-even-background-color: #b3cae2;
--rex-table-cell-selected-hover-background-color: #b2c9e2;
--rex-table-cell-selected-active-background-color: #9eb5ce;
--rex-table-cell-padding: 4px 8px;
Expand Down Expand Up @@ -41,6 +42,13 @@
}
}

& .noDataContent {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

&.withTableBorders {
--rex-table-border-width: 1px;
}
Expand Down
48 changes: 39 additions & 9 deletions core/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import clsx from "clsx";
import { type ElementRef, useRef } from "react";
import { type ElementRef, useCallback, useMemo, useRef } from "react";
import { Scrollbar } from "..";
import styles from "./Table.module.css";
import { TableHeader } from "./components/TableHeader/TableHeader";
import { TableRow } from "./components/TableRow/TableRow";
import { useIsScrolled } from "./hooks/useIsScrolled";
import { useTableProps } from "./hooks/useTableProps";
import type { DefaultTableRow } from "./interface/DefaultTableRow";
import type { TableRowClassNames } from "./interface/TableClassNames";
import type { TableProps } from "./interface/TableProps";

const Table = <TRow extends DefaultTableRow>(props: TableProps<TRow>) => {
Expand All @@ -17,18 +18,32 @@ const Table = <TRow extends DefaultTableRow>(props: TableProps<TRow>) => {
const isScrolled = useIsScrolled(viewportRef);
const hasHeader = tableProps.columns.some((column) => Boolean(column.header));

const estimateSize = useCallback(
(index: number) => {
return tableProps.estimateRowHeight?.(tableProps.data[index], index) ?? 36;
},
[tableProps.estimateRowHeight, tableProps.data],
);

const virtualizer = useVirtualizer({
count: tableProps.data.length,
getScrollElement: () => viewportRef.current,
estimateSize: (index) => tableProps.getRowHeight(tableProps.data[index], index),
estimateSize,
overscan: tableProps.overscan,
});

const virtualizedItems = virtualizer.getVirtualItems();
const topOffset = virtualizedItems[0]?.start ?? 0;

const showNoDataContent = Boolean(tableProps.data.length === 0 && tableProps.noDataContent);

const headerRef = useRef<HTMLTableSectionElement>(null);
const headerHeight = headerRef.current?.clientHeight ?? 0;
const totalHeight = virtualizer.getTotalSize() + headerHeight;

const containerClasses = clsx(
tableProps.className,
tableProps.classNames?.scrollContainer,
styles.container,
isScrolled && styles.scrolled,
tableProps.borders.table && styles.withTableBorders,
Expand All @@ -37,22 +52,34 @@ const Table = <TRow extends DefaultTableRow>(props: TableProps<TRow>) => {
tableProps.stickyHeader && styles.stickyHeader,
);

const rowClassNames: TableRowClassNames = useMemo(
() => ({
tableRow: tableProps.classNames?.tableRow,
tableCell: tableProps.classNames?.tableCell,
tableCellContent: tableProps.classNames?.tableCellContent,
}),
[tableProps.classNames],
);

const noDataContentClasses = clsx(styles.noDataContent, tableProps.classNames?.noDataContent);

return (
<Scrollbar {...tableProps.divProps} className={containerClasses} viewportRef={viewportRef}>
<div style={{ height: virtualizer.getTotalSize() }}>
<table>
<div style={{ height: totalHeight }} className={tableProps.classNames?.root}>
<table cellPadding={0} className={tableProps.classNames?.table}>
{hasHeader && (
<thead>
<tr>
<thead className={tableProps.classNames?.tableHeader} ref={headerRef}>
<tr className={tableProps.classNames?.tableRow}>
{tableProps.columns.map((column) => {
return <TableHeader key={column.id} label={column.header} />;
})}
</tr>
</thead>
)}

<tbody>
<tbody className={tableProps.classNames?.tableBody}>
<tr style={{ height: topOffset, overflowAnchor: "none" }} />

{virtualizedItems.map((virtualItem) => {
const row = tableProps.data[virtualItem.index];
const rowId = tableProps.getRowId(row);
Expand All @@ -61,20 +88,23 @@ const Table = <TRow extends DefaultTableRow>(props: TableProps<TRow>) => {
<TableRow
key={rowId}
id={rowId}
index={virtualItem.index}
row={row}
virtualItem={virtualItem}
measureElement={virtualizer.measureElement}
columns={tableProps.columns}
height={virtualItem.size}
striped={tableProps.stripedRows}
isSelected={tableProps.selectedRows.includes(rowId)}
onSelectRow={tableProps.onSelectRow}
onDeselectRow={tableProps.onDeselectRow}
classNames={rowClassNames}
/>
);
})}
</tbody>
</table>
</div>

{showNoDataContent && <div className={noDataContentClasses}>{tableProps.noDataContent}</div>}
</Scrollbar>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,10 @@
}

& .content {
display: flex;
align-items: center;
padding: var(--rex-table-cell-padding);
box-sizing: border-box;

& .inner {
height: 100%;
display: flex;
align-items: center;
overflow: hidden;
}
}
}
}
13 changes: 6 additions & 7 deletions core/src/components/Table/components/TableCell/TableCell.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
import clsx from "clsx";
import { useCellValue } from "../../hooks/useCellValue";
import type { DefaultTableRow } from "../../interface/DefaultTableRow";
import type { TableCellClassNames } from "../../interface/TableClassNames";
import type { TableColumn } from "../../interface/TableColumn";
import styles from "./TableCell.module.css";

export type TableCellProps<TRow extends DefaultTableRow> = {
row: TRow;
column: TableColumn<TRow>;
isLast: boolean;
height: string | number;
isSelected: boolean;
classNames?: string;
classNames?: TableCellClassNames;
};

const TableCell = <TRow extends DefaultTableRow>(props: TableCellProps<TRow>) => {
const { row, column, height, isSelected, classNames } = props;
const { row, column, isSelected, classNames } = props;

const cellValue = useCellValue({ row, column, isSelected });

let width = column.width ?? 0;
if (width === "content") width = "0.1%";

const cellClasses = clsx(styles.cell, classNames);
const cellClasses = clsx(styles.cell, classNames?.tableCell);
const contentClasses = clsx(styles.content, classNames?.tableCellContent);

return (
<td
className={cellClasses}
style={{ width, maxWidth: column.maxWidth, minWidth: column.minWidth }}
>
<div className={styles.content} style={{ height }}>
<div className={styles.inner}>{cellValue}</div>
</div>
<div className={contentClasses}>{cellValue}</div>
</td>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,30 @@
&:hover {
--rex-table-cell-background-color: var(--rex-table-cell-hover-background-color);
--rex-table-cell-even-background-color: var(--rex-table-cell-hover-background-color);

--rex-table-cell-border-color: var(--rex-table-cell-hover-border-color);
}

&:active {
--rex-table-cell-background-color: var(--rex-table-cell-active-background-color);
--rex-table-cell-even-background-color: var(--rex-table-cell-active-background-color);

--rex-table-cell-border-color: var(--rex-table-cell-active-border-color);
}
}

&.selected {
--rex-table-cell-background-color: var(--rex-table-cell-selected-background-color);

--rex-table-cell-even-background-color: var(--rex-table-cell-selected-even-background-color);
--rex-table-cell-border-color: var(--rex-table-cell-selected-border-color);

&:hover {
--rex-table-cell-background-color: var(--rex-table-cell-selected-hover-background-color);
--rex-table-cell-even-background-color: var(--rex-table-cell-selected-hover-background-color);

--rex-table-cell-border-color: var(--rex-table-cell-selected-hover-border-color);
}

&:active {
--rex-table-cell-background-color: var(--rex-table-cell-selected-active-background-color);
--rex-table-cell-even-background-color: var(--rex-table-cell-selected-active-background-color);

--rex-table-cell-border-color: var(--rex-table-cell-selected-active-border-color);
}
}
Expand Down
43 changes: 35 additions & 8 deletions core/src/components/Table/components/TableRow/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import type { VirtualItem } from "@tanstack/react-virtual";
import clsx from "clsx";
import { useMemo } from "react";
import type { DefaultTableRow } from "../../interface/DefaultTableRow";
import type { TableCellClassNames, TableRowClassNames } from "../../interface/TableClassNames";
import type { TableColumn } from "../../interface/TableColumn";
import type { OnDeselectRow, OnSelectRow } from "../../interface/TableProps";
import { TableCell } from "../TableCell/TableCell";
import styles from "./TableRow.module.css";

export type TableRowProps<TRow extends DefaultTableRow> = {
id: string | number;
index: number;
row: TRow;
columns: TableColumn<TRow>[];
height: string | number;
virtualItem: VirtualItem;
striped: boolean;
isSelected: boolean;
classNames?: TableRowClassNames;
measureElement: (node: Element | null | undefined) => void;
onSelectRow: OnSelectRow<TRow> | undefined;
onDeselectRow: OnDeselectRow<TRow> | undefined;
};

const TableRow = <TRow extends DefaultTableRow>(props: TableRowProps<TRow>) => {
const { id, index, row, columns, height, striped, isSelected, onSelectRow, onDeselectRow } =
props;
const {
id,
virtualItem,
row,
columns,
striped,
isSelected,
classNames,
measureElement,
onSelectRow,
onDeselectRow,
} = props;

const isStriped = striped && index % 2 === 0;
const isStriped = striped && virtualItem.index % 2 === 0;

const handleRowClick = () => {
if (isSelected) {
Expand All @@ -33,24 +47,37 @@ const TableRow = <TRow extends DefaultTableRow>(props: TableRowProps<TRow>) => {

const selectable = (onSelectRow && !isSelected) || (onDeselectRow && isSelected);
const rowClasses = clsx(
classNames?.tableRow,
styles.row,
selectable && styles.selectable,
isSelected && styles.selected,
isStriped && styles.striped,
);

const cellClasses: TableCellClassNames = useMemo(
() => ({
tableCell: clsx(styles.rowCell, classNames?.tableCell),
tableCellContent: classNames?.tableCellContent,
}),
[classNames],
);

return (
<tr className={rowClasses} onClick={handleRowClick}>
<tr
className={rowClasses}
onClick={handleRowClick}
ref={measureElement}
data-index={virtualItem.index}
>
{columns.map((column, index) => {
return (
<TableCell
key={`${id}_${column.id}`}
column={column}
row={row}
isSelected={isSelected}
height={height}
isLast={columns.length - 1 === index}
classNames={styles.rowCell}
classNames={cellClasses}
/>
);
})}
Expand Down
26 changes: 17 additions & 9 deletions core/src/components/Table/hooks/useTableProps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ReactNode } from "react";
import type { DefaultTableRow } from "../interface/DefaultTableRow";
import type { TableBorders } from "../interface/TableBorders";
import type { TableClassNames } from "../interface/TableClassNames";
import type { TableColumn } from "../interface/TableColumn";
import type {
GetRowHeight,
EstimateRowHeight,
GetRowId,
OnDeselectRow,
OnSelectRow,
Expand All @@ -18,11 +20,13 @@ export type ComposedTableProps<TRow extends DefaultTableRow> = {
stripedRows: boolean;
borders: TableBorders;
selectedRows: TableRowId[];
className?: string;
noDataContent?: ReactNode;
className: string | undefined;
classNames: TableClassNames | undefined;
getRowId: GetRowId<TRow>;
getRowHeight: GetRowHeight<TRow>;
onSelectRow?: OnSelectRow<TRow>;
onDeselectRow?: OnDeselectRow<TRow>;
estimateRowHeight: EstimateRowHeight<TRow> | undefined;
onSelectRow: OnSelectRow<TRow> | undefined;
onDeselectRow: OnDeselectRow<TRow> | undefined;
divProps: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
};

Expand All @@ -31,13 +35,15 @@ export const useTableProps = <TRow extends DefaultTableRow>(props: TableProps<TR
columns,
data,
className,
classNames,
getRowId,
overscan,
getRowHeight,
estimateRowHeight,
stickyHeader,
stripedRows,
borders,
selectedRows,
noDataContent,
onSelectRow,
onDeselectRow,
...divProps
Expand All @@ -62,14 +68,16 @@ export const useTableProps = <TRow extends DefaultTableRow>(props: TableProps<TR
data: data,
getRowId,
overscan: overscan ?? 5,
getRowHeight: getRowHeight ?? (() => 36),
estimateRowHeight,
stickyHeader: stickyHeader ?? true,
stripedRows: stripedRows ?? false,
borders: _borders,
selectedRows: _selectedRows,
onSelectRow: onSelectRow,
onDeselectRow: onDeselectRow,
noDataContent,
onSelectRow,
onDeselectRow,
className,
classNames,
divProps: divProps,
} as const satisfies ComposedTableProps<TRow>;
};
Loading