Skip to content

Commit

Permalink
feat(Table): adding TableFooter and better sticky visual clues (#437)
Browse files Browse the repository at this point in the history
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a `TableFooter` component for enhanced table customization.
	- Added a `stickyFooter` option to make the table footer sticky.

- **Enhancements**
- Improved table styling and functionality to support the new sticky
footer feature.

- **Tests**
	- Implemented tests to ensure the sticky footer behaves as expected.

- **Documentation**
- Updated storybook examples to demonstrate the usage of the new
`TableFooter` component and `stickyFooter` feature.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
aversini authored Mar 19, 2024
1 parent a25890f commit 33d73c6
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 10 deletions.
18 changes: 17 additions & 1 deletion packages/documentation/src/Components/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableRow,
} from "@versini/ui-components";
Expand Down Expand Up @@ -134,6 +135,13 @@ export const WithAction: Story<any> = (args) => {
);
})}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3} className="text-center uppercase">
hello footer
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
Expand Down Expand Up @@ -189,7 +197,7 @@ export const WithStickyHeader: Story<any> = (args) => {
return (
<div className="min-h-10">
<div className="flex flex-wrap gap-2">
<Table maxHeight="238px" stickyHeader {...args}>
<Table maxHeight="285px" stickyHeader stickyFooter {...args}>
<TableHead className="uppercase">
<TableRow>
<TableCell scope="col">Date</TableCell>
Expand Down Expand Up @@ -235,6 +243,14 @@ export const WithStickyHeader: Story<any> = (args) => {
);
})}
</TableBody>

<TableFooter>
<TableRow>
<TableCell colSpan={3} className="text-center uppercase">
hello footer
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
Expand Down
26 changes: 25 additions & 1 deletion packages/ui-components/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import type {
} from "./TableTypes";
import {
CELL_WRAPPER_BODY,
CELL_WRAPPER_FOOTER,
CELL_WRAPPER_HEAD,
getTableCellClasses,
getTableClasses,
getTableFooterClasses,
getTableHeadClasses,
getTableRowClasses,
} from "./utilities";
Expand All @@ -26,6 +28,7 @@ export const Table = ({
wrapperClassName,
maxHeight,
stickyHeader,
stickyFooter,
spacing,
...otherProps
}: TableProps) => {
Expand All @@ -34,10 +37,11 @@ export const Table = ({
className,
wrapperClassName,
stickyHeader,
stickyFooter,
spacing,
});
return (
<TableContext.Provider value={{ mode, stickyHeader }}>
<TableContext.Provider value={{ mode, stickyHeader, stickyFooter }}>
<div
className={tableClass.wrapper}
{...(maxHeight && {
Expand All @@ -64,6 +68,7 @@ export const TableHead = ({
context.cellWrapper = CELL_WRAPPER_HEAD;
const tableHeadClass = getTableHeadClasses({
className,
mode: context.mode,
stickyHeader: context.stickyHeader,
});
return (
Expand All @@ -73,6 +78,25 @@ export const TableHead = ({
);
};

export const TableFooter = ({
children,
className,
...otherProps
}: TableHeadProps) => {
const context = useContext(TableContext);
context.cellWrapper = CELL_WRAPPER_FOOTER;
const tableFooterClass = getTableFooterClasses({
className,
mode: context.mode,
stickyFooter: context.stickyFooter,
});
return (
<tfoot className={tableFooterClass} {...otherProps}>
{children}
</tfoot>
);
};

export const TableBody = ({ children, ...otherProps }: TableBodyProps) => {
const context = useContext(TableContext);
context.cellWrapper = CELL_WRAPPER_BODY;
Expand Down
4 changes: 3 additions & 1 deletion packages/ui-components/src/components/Table/TableContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import React from "react";

export const TableContext = React.createContext<{
mode: "light" | "dark" | "system" | "alt-system";
cellWrapper?: "thead" | "tbody";
cellWrapper?: "thead" | "tbody" | "tfoot";
stickyFooter?: boolean;
stickyHeader?: boolean;
}>({
mode: "light",
cellWrapper: "thead",
stickyHeader: false,
stickyFooter: false,
});
6 changes: 6 additions & 0 deletions packages/ui-components/src/components/Table/TableTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export type TableProps = {
* The mode of table. It defines the color of the table.
*/
mode?: "dark" | "light" | "system" | "alt-system";
/**
* If true, the table footer will be sticky.
* Note: It is required to configure 'maxHeight' prop for the prop
* 'stickyHeader' to work.
*/
stickyFooter?: boolean;
/**
* If true, the table header will be sticky.
* Note: It is required to configure 'maxHeight' prop for the prop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import {
expectToHaveClasses,
expectToHaveStyles,
} from "../../../../../../configuration/tests-helpers";
import { Table, TableBody, TableCell, TableHead, TableRow } from "../..";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableRow,
} from "../..";

describe("Table (exceptions)", () => {
it("should be able to require/import from root", () => {
Expand Down Expand Up @@ -253,6 +260,96 @@ describe("Table classes", () => {
}
});

it("should render a table with a sticky footer", async () => {
render(
<Table
data-testid="table"
caption="the caption"
maxHeight="100px"
stickyFooter
>
<TableHead data-testid="table-head">
<TableRow data-testid="table-row-head">
<TableCell data-testid="table-cell-head">the header</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow data-testid="table-row-body">
<TableCell data-testid="table-cell-body">the body</TableCell>
</TableRow>
</TableBody>
<TableFooter data-testid="table-footer">
<TableRow data-testid="table-row-footer">
<TableCell data-testid="table-cell-footer">the footer</TableCell>
</TableRow>
</TableFooter>
</Table>,
);
const table = await screen.findByTestId("table");
const wrapper = table.parentElement;
const caption = table.querySelector("caption");
const tableFooter = await screen.findByTestId("table-footer");
const tableRowFooter = await screen.findByTestId("table-row-footer");
const tableRowBody = await screen.findByTestId("table-row-body");
const tableCellFooter = await screen.findByTestId("table-cell-footer");
const tableCellBody = await screen.findByTestId("table-cell-body");

expect(table).toBeInTheDocument();
expect(table.tagName).toBe("TABLE");

expectToHaveClasses(table, [
"w-full",
"text-left",
"text-sm",
"text-copy-light",
]);
if (wrapper) {
expectToHaveClasses(wrapper, [
"relative",
"w-full",
"overflow-y-scroll",
"rounded-lg",
"shadow-md",
"bg-surface-darker",
"text-copy-light",
]);
expectToHaveStyles(wrapper, { "max-height": "100px" });
}
if (caption) {
expectToHaveClasses(caption, [
"py-2",
"text-sm",
"font-bold",
"text-copy-light",
]);
}
if (tableFooter) {
expectToHaveClasses(tableFooter, ["sticky", "bottom-0", "z-10"]);
}
if (tableRowFooter) {
expectToHaveClasses(tableRowFooter, [
"border-b",
"last:border-0",
"border-table-dark",
"bg-table-dark",
]);
}
if (tableRowBody) {
expectToHaveClasses(tableRowBody, [
"border-b",
"last:border-0",
"border-table-dark",
"odd:bg-table-dark-odd",
"even:bg-table-dark-even",
]);
}
expect(tableCellFooter).toBeInTheDocument();

if (tableCellBody) {
expectToHaveClasses(tableCellBody, ["p-4"]);
}
});

it("should render a table with a custom wrapper class", async () => {
render(
<Table
Expand Down
57 changes: 51 additions & 6 deletions packages/ui-components/src/components/Table/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { getSpacing } from "@versini/ui-private/dist/utilities";
import clsx from "clsx";

export const CELL_WRAPPER_HEAD = "thead";
export const CELL_WRAPPER_FOOTER = "tfoot";
export const CELL_WRAPPER_BODY = "tbody";

export const getTableClasses = ({
mode,
className,
wrapperClassName,
stickyHeader,
stickyFooter,
spacing,
}: {
mode: string;
className?: string;
stickyFooter?: boolean;
stickyHeader?: boolean;
wrapperClassName?: string;
} & SpacingProps) => {
Expand All @@ -22,8 +25,8 @@ export const getTableClasses = ({
"not-prose relative w-full rounded-lg shadow-md",
getSpacing(spacing),
{
"overflow-x-auto": !stickyHeader,
"overflow-y-scroll": stickyHeader,
"overflow-x-auto": !stickyHeader && !stickyFooter,
"overflow-y-scroll": stickyHeader || stickyFooter,
"bg-surface-darker text-copy-light": mode === "dark",
"bg-surface-light text-copy-dark": mode === "light",
"bg-surface-darker text-copy-light dark:bg-surface-light dark:text-copy-dark":
Expand Down Expand Up @@ -51,12 +54,44 @@ export const getTableClasses = ({
export const getTableHeadClasses = ({
className,
stickyHeader,
mode,
}: {
mode: string;
className?: string;
stickyHeader?: boolean;
}) => {
return clsx(className, {
"sticky top-0 z-10": stickyHeader,
"shadow-[rgb(190_190_190_/30%)_0_0.5rem_1rem]":
stickyHeader && mode === "dark",
"shadow-[rgb(190_190_190_/30%)_0_0.5rem_1rem] dark:shadow-[rgb(65_65_65_/30%)_0_0.5rem_1rem]":
stickyHeader && mode === "system",
"shadow-[rgb(65_65_65_/30%)_0_0.5rem_1rem]":
stickyHeader && mode === "light",
"shadow-[rgb(65_65_65_/30%)_0_0.5rem_1rem] dark:shadow-[rgb(190_190_190_/30%)_0_0.5rem_1rem]":
stickyHeader && mode === "alt-system",
});
};

export const getTableFooterClasses = ({
className,
stickyFooter,
mode,
}: {
mode: string;
className?: string;
stickyFooter?: boolean;
}) => {
return clsx(className, {
"sticky bottom-0 z-10": stickyFooter,
"shadow-[rgb(190_190_190_/30%)_0_-0.5rem_1rem]":
stickyFooter && mode === "dark",
"shadow-[rgb(190_190_190_/30%)_0_-0.5rem_1rem] dark:shadow-[rgb(65_65_65_/30%)_0_-0.5rem_1rem]":
stickyFooter && mode === "system",
"shadow-[rgb(65_65_65_/30%)_0_-0.5rem_1rem]":
stickyFooter && mode === "light",
"shadow-[rgb(65_65_65_/30%)_-0_0.5rem_1rem] dark:shadow-[rgb(190_190_190_/30%)_0_-0.5rem_1rem]":
stickyFooter && mode === "alt-system",
});
};

Expand All @@ -71,24 +106,34 @@ export const getTableRowClasses = ({
}) => {
return clsx("border-b last:border-0", className, {
"border-table-dark": mode === "dark",
"bg-table-dark": cellWrapper === CELL_WRAPPER_HEAD && mode === "dark",
"bg-table-dark":
(cellWrapper === CELL_WRAPPER_HEAD ||
cellWrapper === CELL_WRAPPER_FOOTER) &&
mode === "dark",
"odd:bg-table-dark-odd even:bg-table-dark-even":
cellWrapper === CELL_WRAPPER_BODY && mode === "dark",

"border-table-light": mode === "light",
"bg-table-light": cellWrapper === CELL_WRAPPER_HEAD && mode === "light",
"bg-table-light":
(cellWrapper === CELL_WRAPPER_HEAD ||
cellWrapper === CELL_WRAPPER_FOOTER) &&
mode === "light",
"odd:bg-table-light-odd even:bg-table-light-even":
cellWrapper === CELL_WRAPPER_BODY && mode === "light",

"border-table-dark dark:border-table-light": mode === "system",
"bg-table-dark dark:bg-table-light":
cellWrapper === CELL_WRAPPER_HEAD && mode === "system",
(cellWrapper === CELL_WRAPPER_HEAD ||
cellWrapper === CELL_WRAPPER_FOOTER) &&
mode === "system",
"odd:bg-table-dark-odd even:bg-table-dark-even dark:odd:bg-table-light-odd dark:even:bg-table-light-even":
cellWrapper === CELL_WRAPPER_BODY && mode === "system",

"border-table-light dark:border-table-dark": mode === "alt-system",
"bg-table-light dark:bg-table-dark":
cellWrapper === CELL_WRAPPER_HEAD && mode === "alt-system",
(cellWrapper === CELL_WRAPPER_HEAD ||
cellWrapper === CELL_WRAPPER_FOOTER) &&
mode === "alt-system",
"odd:bg-table-light-odd even:bg-table-light-even dark:odd:bg-table-dark-odd dark:even:bg-table-dark-even":
cellWrapper === CELL_WRAPPER_BODY && mode === "alt-system",
});
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableRow,
} from "./Table/Table";
Expand All @@ -40,6 +41,7 @@ export {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableRow,
};

0 comments on commit 33d73c6

Please sign in to comment.