Skip to content

Commit

Permalink
Merge pull request #769 from buildo/allow_grouped_table_headers
Browse files Browse the repository at this point in the history
Allow grouped table headers
  • Loading branch information
federico-ercoles authored Sep 21, 2023
2 parents 205860e + 1e98a25 commit 7922fe3
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 67 deletions.
14 changes: 10 additions & 4 deletions packages/bento-design-system/src/Table/Table.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ export const sortIconContainer = style({
filter: "opacity(80%)",
});

export const stickyColumnHeader = bentoSprinkles({
position: "sticky",
top: 0,
});
export const stickyTopHeight = createVar();

export const stickyColumnHeader = style([
{
top: stickyTopHeight,
},
bentoSprinkles({
position: "sticky",
}),
]);

export const rowContainer = style({
// NOTE(gabro): this allows us to use the entire row as a parent selector,
Expand Down
186 changes: 131 additions & 55 deletions packages/bento-design-system/src/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
useGroupBy,
Cell,
SortingRule,
HeaderGroup,
} from "react-table";
import { IconProps } from "../Icons/IconProps";
import { useDefaultMessages } from "../util/useDefaultMessages";
Expand Down Expand Up @@ -38,21 +39,32 @@ import {
selectedRowBackgroundColor,
sortIconContainer,
stickyColumnHeader,
stickyTopHeight,
table,
} from "./Table.css";
import { Column as ColumnType, GridWidth, Row as RowType } from "./types";
import {
Column as SimpleColumnType,
GroupedColumn as GroupedColumnType,
GridWidth,
Row as RowType,
} from "./types";
import { useLayoutEffect, useMemo, useState, CSSProperties, useEffect } from "react";
import { IconQuestionSolid, IconInfo } from "../Icons";
import { match, __ } from "ts-pattern";
import { useBentoConfig } from "../BentoConfigContext";
import { assignInlineVars } from "@vanilla-extract/dynamic";

type SortFn<C extends ReadonlyArray<ColumnType<string, {}, any>>> = (
a: Row<RowType<C>>,
b: Row<RowType<C>>
) => number;

type SortingProps<C extends ReadonlyArray<ColumnType<string, {}, any>>> =
type SortFn<
C extends
| ReadonlyArray<SimpleColumnType<string, any, any>>
| ReadonlyArray<GroupedColumnType<string, any, any>>
> = (a: Row<RowType<C>>, b: Row<RowType<C>>) => number;

type SortingProps<
C extends
| ReadonlyArray<SimpleColumnType<string, any, any>>
| ReadonlyArray<GroupedColumnType<string, any, any>>
> =
| {
/**
* `customSorting` can be used to customize the sorting logic of the table. It supports cross-columns comparison.
Expand All @@ -74,10 +86,18 @@ type SortingProps<C extends ReadonlyArray<ColumnType<string, {}, any>>> =
onSort?: never;
};

type Props<C extends ReadonlyArray<ColumnType<string, {}, any>>> = {
type Props<
C extends
| ReadonlyArray<SimpleColumnType<string, any, any>>
| ReadonlyArray<GroupedColumnType<string, any, any>>
> = {
columns: C;
data: ReadonlyArray<RowType<C>>;
groupBy?: C[number]["accessor"];
groupBy?: C extends ReadonlyArray<SimpleColumnType<string, any, any>>
? C[number]["accessor"]
: C extends ReadonlyArray<GroupedColumnType<string, any, any>>
? C[number]["columns"][number]["accessor"]
: never;
noResultsTitle?: LocalizedString;
noResultsDescription?: LocalizedString;
noResultsFeedbackSize?: FeedbackProps["size"];
Expand Down Expand Up @@ -106,7 +126,11 @@ type Props<C extends ReadonlyArray<ColumnType<string, {}, any>>> = {
* <Table columns={[tableColumn(...), tableColumn(...)]} data={data} />
* ```
*/
export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
export function Table<
C extends
| ReadonlyArray<SimpleColumnType<string, any, any>>
| ReadonlyArray<GroupedColumnType<string, any, any>>
>({
columns,
data,
groupBy,
Expand Down Expand Up @@ -146,6 +170,11 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
[customSorting]
);

const flatColumns = useMemo(
() => columns.flatMap((c) => ("columns" in c ? c.columns : [c])),
[columns]
);

const {
getTableProps,
headerGroups,
Expand Down Expand Up @@ -179,15 +208,19 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
// Determine the ids of the sticky columns to the left
const stickyLeftColumnsIds = useMemo(
() =>
columns
.filter((c) => c.sticky === "left")
.map((c) => headerGroups[0].headers.find((h) => h.id === c.accessor)?.id)
.filter((id): id is string => id !== undefined),
[columns, headerGroups]
headerGroups[0].headers
.filter((h) => h.sticky)
.flatMap((h) => h.columns ?? [h])
.map((h) => h.id),
[headerGroups]
);
const stickyLeftColumnGroupsIds = useMemo(
() => headerGroups[0].headers.filter((h) => h.sticky).map((h) => h.id),
[headerGroups]
);

// Determine the id of the last left sticky column (used to draw a visual separator in the UI)
const lastStickyColumnIndex = columns
const lastStickyColumnIndex = flatColumns
.map((c) => c.accessor)
.indexOf(stickyLeftColumnsIds[stickyLeftColumnsIds.length - 1]);

Expand All @@ -196,6 +229,9 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
{} as Record<string, CSSProperties>
);

// Keep a state for the height of the first row of headers, which will be updated by the useLayoutEffect below
const [stickyHeaderHeight, setStickyHeaderHeight] = useState(0);

/** Get the width of each sticky column (using the header width as reference) and use it to set the `left` of each sticky column.
* Each sticky column must have as `left` the total width of the previous sticky columns.
*/
Expand All @@ -206,35 +242,55 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
const columnWidths = stickyLeftColumnsIds.map(
(id) => document.getElementById(`header-cell-${id}`)!.clientWidth
);
const columnGroupWidths = stickyLeftColumnGroupsIds.map(
(id) => document.getElementById(`header-cell-${id}`)!.clientWidth
);
const columnGroupHeight = Math.max(
...headerGroups[0].headers.map((h) =>
h.columns ? document.getElementById(`header-cell-${h.id}`)!.clientHeight : 0
)
);

const columnStyles = stickyLeftColumnsIds.reduce((styles, id, index) => {
if (index > 0) {
const totalLeftWidth = columnWidths
.filter((_w, i) => i < index)
.reduce((acc, w) => acc + w, 0);
return {
...styles,
[id]: {
left: totalLeftWidth,
zIndex: zIndexes.leftStickyCell,
position: "sticky",
} as CSSProperties,
};
} else {
return {
...styles,
[id]: {
left: 0,
zIndex: zIndexes.leftStickyCell,
position: "sticky",
} as CSSProperties,
};
}
}, {} as Record<string, CSSProperties>);
const styleReducer =
(widths: number[]) =>
(styles: Record<string, CSSProperties>, id: string, index: number) => {
if (index > 0) {
const totalLeftWidth = widths
.filter((_w, i) => i < index)
.reduce((acc, w) => acc + w, 0);
return {
...styles,
[id]: {
left: totalLeftWidth,
zIndex: zIndexes.leftStickyCell,
position: "sticky",
} as CSSProperties,
};
} else {
return {
...styles,
[id]: {
left: 0,
zIndex: zIndexes.leftStickyCell,
position: "sticky",
} as CSSProperties,
};
}
};

const columnStyles = stickyLeftColumnsIds.reduce(
styleReducer(columnWidths),
{} as Record<string, CSSProperties>
);
const columnGroupStyles = stickyLeftColumnGroupsIds.reduce(
styleReducer(columnGroupWidths),
{} as Record<string, CSSProperties>
);

setStickyLeftColumnStyle(columnStyles);
setStickyLeftColumnStyle({ ...columnStyles, ...columnGroupStyles });
setStickyHeaderHeight(columnGroupHeight);
}
}, [data.length, stickyLeftColumnsIds]);
}, [data.length, headerGroups, stickyLeftColumnsIds, stickyLeftColumnGroupsIds]);

if (data.length === 0) {
return (
Expand Down Expand Up @@ -273,7 +329,7 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
.exhaustive();
}

const gridTemplateColumns = columns
const gridTemplateColumns = flatColumns
.filter(({ accessor }) => accessor !== groupBy)
.map(({ gridWidth = "fit-content" }) => gridWidthStyle(gridWidth))
.join(" ");
Expand All @@ -290,7 +346,7 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
lastLeftSticky={index === lastStickyColumnIndex}
style={stickyLeftColumnStyle[cell.column.id]}
first={index === 0}
last={(index + 1) % columns.length === 0}
last={(index + 1) % flatColumns.length === 0}
interactiveRow={interactiveRow}
>
{cell.render("Cell")}
Expand All @@ -307,16 +363,28 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
style={{ ...getTableProps().style, gridTemplateColumns, height: tableHeight(height) }}
>
{headerGroups.map((headerGroup) =>
headerGroup.headers.map((column, index) => (
headerGroup.headers.map((header, index) => (
<ColumnHeader
column={column}
key={column.id}
style={stickyLeftColumnStyle[column.id]}
lastLeftSticky={index === lastStickyColumnIndex}
column={header}
key={header.id}
style={{
...stickyLeftColumnStyle[header.id],
...assignInlineVars({
[stickyTopHeight]: header.columns ? "0" : `${stickyHeaderHeight}px`,
}),
}}
lastLeftSticky={
header.columns
? header.id === stickyLeftColumnGroupsIds.at(-1)
: index === lastStickyColumnIndex
}
stickyHeaders={stickyHeaders}
sticky={stickyLeftColumnsIds.includes(column.id)}
sticky={
stickyLeftColumnsIds.includes(header.id) ||
stickyLeftColumnGroupsIds.includes(header.id)
}
first={index === 0}
last={index + 1 === columns.length}
last={index + 1 === flatColumns.length}
/>
))
)}
Expand Down Expand Up @@ -347,7 +415,11 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
);
}

function RowContainer<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
function RowContainer<
C extends
| ReadonlyArray<SimpleColumnType<string, any, any>>
| ReadonlyArray<GroupedColumnType<string, any, any>>
>({
row,
children,
onPress,
Expand Down Expand Up @@ -379,7 +451,7 @@ function ColumnHeader<D extends Record<string, unknown>>({
first,
last,
}: {
column: ColumnInstance<D>;
column: ColumnInstance<D> | HeaderGroup<D>;
style: CSSProperties;
lastLeftSticky: boolean;
stickyHeaders?: boolean;
Expand Down Expand Up @@ -432,7 +504,11 @@ function ColumnHeader<D extends Record<string, unknown>>({
return (
<Box
className={[lastLeftSticky && lastLeftStickyColumn, stickyHeaders && stickyColumnHeader]}
style={{ ...style, zIndex: sticky ? zIndexes.leftStickyHeader : zIndexes.header }}
style={{
...style,
gridColumnEnd: column.columns ? `span ${column.columns.length}` : undefined,
zIndex: sticky ? zIndexes.leftStickyHeader : zIndexes.header,
}}
>
<Box
className={columnHeader}
Expand Down Expand Up @@ -557,7 +633,7 @@ export type {
Row as TableRow,
} from "react-table";

export type { Column as ColumnType, Row as RowType } from "./types";
export type { Column as SimpleColumnType, Row as RowType } from "./types";

export type { ColumnOptionsBase } from "./tableColumn";
export type { Props as TableProps };
Expand Down
6 changes: 3 additions & 3 deletions packages/bento-design-system/src/Table/tableColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export type ColumnOptionsBase<A> = {
accessor: A;
headerLabel?: LocalizedString;
missingValue?: LocalizedString;
width?: Column_<{}>["gridWidth"];
} & Omit<Column_<{}>, "accessor" | "Header" | "Cell" | "sortType" | "width" | "gridWidth">;
width?: Column_<any>["gridWidth"];
} & Omit<Column_<any>, "accessor" | "Header" | "Cell" | "sortType" | "width" | "gridWidth">;

export function custom<A extends string, V, D extends Record<string, unknown>>({
headerLabel,
Expand All @@ -47,7 +47,7 @@ export function custom<A extends string, V, D extends Record<string, unknown>>({
const column = {
...options,
gridWidth: width,
Cell: (props) => {
Cell: (props: CellProps<D, V>) => {
const { defaultMessages } = useDefaultMessages();
const config = useBentoConfig().table;
if (props.value == null) {
Expand Down
27 changes: 24 additions & 3 deletions packages/bento-design-system/src/Table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,33 @@ export type Column<A extends string, D extends object, V> = {
Cell: (props: CellProps<D, V>) => ReturnType<FunctionComponent>;
} & Omit<Column_<D>, "accessor" | "Cell" | "width">;

export type GroupedColumn<A extends string, D extends object, V> = {
Header: string;
columns: Array<Column<A, D, V>>;
align?: "left" | "right" | "center";
sticky?: "left";
hint?: LocalizedString | { onPress: () => void };
};

type ColumnValueByAccessor<C, K extends string> = C extends Column<K, infer _D, infer V>
? V
: never;

export type Row<Columns extends ReadonlyArray<Column<string, {}, any>>> = {
[k in Columns[number]["accessor"]]?: ColumnValueByAccessor<Columns[number], k>;
};
export type Row<
Columns extends
| ReadonlyArray<Column<string, any, any>>
| ReadonlyArray<GroupedColumn<string, any, any>>
> = Columns[number] extends GroupedColumn<string, any, any>
? {
[k in Columns[number]["columns"][number]["accessor"]]?: ColumnValueByAccessor<
Columns[number]["columns"][number],
k
>;
}
: Columns[number] extends Column<string, any, any>
? {
[k in Columns[number]["accessor"]]?: ColumnValueByAccessor<Columns[number], k>;
}
: never;

export type GridWidth = "fit-content" | "fill-available" | { custom: string | number };
2 changes: 1 addition & 1 deletion packages/bento-design-system/src/useComponentsShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ export function useComponentsShowcase({ action }: { action: (s: string) => () =>
experiments: 0,
status: { label: "pending", color: "yellow" },
},
],
] as any,
},
],
],
Expand Down
Loading

0 comments on commit 7922fe3

Please sign in to comment.