Skip to content
95 changes: 95 additions & 0 deletions packages/ui/src/components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryFn } from "@storybook/react";
import { useState } from "react";
import type { TableProps } from "./Table";
import { Table } from "./Table";

Expand Down Expand Up @@ -86,3 +87,97 @@ const Template: StoryFn<TableProps> = (args) => (

export const DefaultTable = Template.bind({});
DefaultTable.storyName = "Default";

const TPageTemplate: StoryFn<TableProps> = (args) => {
const [pageNo, setPageNo] = useState(1);
const [rowsPerPage] = useState(10);

const handlePageChange = (newPage: number) => setPageNo(newPage);

return (
<>
<Table {...args}>
<Table.Head>
<Table.HeadCell>Product name</Table.HeadCell>
<Table.HeadCell>Color</Table.HeadCell>
<Table.HeadCell>Category</Table.HeadCell>
<Table.HeadCell>Price</Table.HeadCell>
<Table.HeadCell>
<span className="sr-only">Edit</span>
</Table.HeadCell>
</Table.Head>
<Table.Body className="divide-y">
<Table.Row className="bg-white dark:border-gray-700 dark:bg-gray-800">
<Table.Cell className="whitespace-nowrap font-medium text-gray-900 dark:text-white">
{'Apple MacBook Pro 17"'}
</Table.Cell>
<Table.Cell>Sliver</Table.Cell>
<Table.Cell>Laptop</Table.Cell>
<Table.Cell>$2999</Table.Cell>
<Table.Cell>
<a href="/tables" className="font-medium text-cyan-600 hover:underline dark:text-cyan-500">
Edit
</a>
</Table.Cell>
</Table.Row>
<Table.Row className="bg-white dark:border-gray-700 dark:bg-gray-800">
<Table.Cell className="whitespace-nowrap font-medium text-gray-900 dark:text-white">
Microsoft Surface Pro
</Table.Cell>
<Table.Cell>White</Table.Cell>
<Table.Cell>Laptop PC</Table.Cell>
<Table.Cell>$1999</Table.Cell>
<Table.Cell>
<a href="/tables" className="font-medium text-cyan-600 hover:underline dark:text-cyan-500">
Edit
</a>
</Table.Cell>
</Table.Row>
<Table.Row className="bg-white dark:border-gray-700 dark:bg-gray-800">
<Table.Cell className="whitespace-nowrap font-medium text-gray-900 dark:text-white">
Magic Mouse 2
</Table.Cell>
<Table.Cell>Black</Table.Cell>
<Table.Cell>Accessories</Table.Cell>
<Table.Cell>$99</Table.Cell>
<Table.Cell>
<a href="/tables" className="font-medium text-cyan-600 hover:underline dark:text-cyan-500">
Edit
</a>
</Table.Cell>
</Table.Row>
<Table.Row className="bg-white dark:border-gray-700 dark:bg-gray-800">
<Table.Cell className="whitespace-nowrap font-medium text-gray-900 dark:text-white">
Google Pixel Phone
</Table.Cell>
<Table.Cell>Gray</Table.Cell>
<Table.Cell>Phone</Table.Cell>
<Table.Cell>$799</Table.Cell>
<Table.Cell>
<a href="/tables" className="font-medium text-cyan-600 hover:underline dark:text-cyan-500">
Edit
</a>
</Table.Cell>
</Table.Row>
<Table.Row className="bg-white dark:border-gray-700 dark:bg-gray-800">
<Table.Cell className="whitespace-nowrap font-medium text-gray-900 dark:text-white">
Apple Watch 5
</Table.Cell>
<Table.Cell>Red</Table.Cell>
<Table.Cell>Wearables</Table.Cell>
<Table.Cell>$999</Table.Cell>
<Table.Cell>
<a href="/tables" className="font-medium text-cyan-600 hover:underline dark:text-cyan-500">
Edit
</a>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
<Table.Pagination count={100} onPageChange={handlePageChange} page={pageNo} rowsPerPage={rowsPerPage} />
</>
);
};

export const PaginationTable = TPageTemplate.bind({});
PaginationTable.storyName = "Pagination";
3 changes: 3 additions & 0 deletions packages/ui/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { TableCell } from "./TableCell";
import { TableContext } from "./TableContext";
import { TableHead, type FlowbiteTableHeadTheme } from "./TableHead";
import { TableHeadCell } from "./TableHeadCell";
import { TablePagination, type FlowbiteTablePaginationTheme } from "./TablePagination";
import { TableRow, type FlowbiteTableRowTheme } from "./TableRow";

export interface FlowbiteTableTheme {
root: FlowbiteTableRootTheme;
head: FlowbiteTableHeadTheme;
row: FlowbiteTableRowTheme;
body: FlowbiteTableBodyTheme;
pagination: FlowbiteTablePaginationTheme;
}

export interface FlowbiteTableRootTheme {
Expand Down Expand Up @@ -56,4 +58,5 @@ export const Table = Object.assign(TableComponent, {
Row: TableRow,
Cell: TableCell,
HeadCell: TableHeadCell,
Pagination: TablePagination,
});
107 changes: 107 additions & 0 deletions packages/ui/src/components/Table/TablePagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { forwardRef, type ComponentPropsWithRef } from "react";
import { twMerge } from "tailwind-merge";
import { mergeDeep } from "../../helpers/merge-deep";
import { getTheme } from "../../theme-store";
import type { DeepPartial } from "../../types";

export interface FlowbiteTablePaginationTheme {
base: string;
totalPages: {
base: string;
pageRange: string;
outOf: string;
};
page: {
base: string;
previous: string;
pageNo: string;
next: string;
};
}

export interface TablePaginationProps extends ComponentPropsWithRef<"tfoot"> {
count: number;
onPageChange: (newPage: number) => void;
page: number;
rowsPerPage: number;
theme?: DeepPartial<FlowbiteTablePaginationTheme>;
}

export const TablePagination = forwardRef<HTMLDivElement, TablePaginationProps>(
({ count, onPageChange, page, rowsPerPage, className, theme: customTheme = {}, ...props }, ref) => {
const theme = mergeDeep(getTheme().table.pagination, customTheme);

const nPages = Math.ceil(count / rowsPerPage);
const pageNumbers = [...Array(nPages + 1).keys()].slice(1);

const goToPrevPage = (): void => {
if (page !== 1) {
onPageChange(page - 1);
}
};

const goToNextPage = (): void => {
if (page !== nPages) {
onPageChange(page + 1);
}
};

const directPageChange = (customPageNo: number): void => {
if (page !== customPageNo) {
onPageChange(customPageNo);
}
};

const getLabelDisplayedRowsTo = (): number => {
if (count === -1) {
return (page + 1) * rowsPerPage;
}

return rowsPerPage === -1 ? count : Math.min(count, (page + 1) * rowsPerPage);
};

return (
<div className={twMerge(theme.base, className)} aria-label="Table pagination" {...props} ref={ref}>
<span className={theme.totalPages.base}>
Showing{" "}
<span className={theme.totalPages.pageRange}>
{`${count === 0 ? 0 : page * rowsPerPage + 1}-${getLabelDisplayedRowsTo()}`}
</span>
of
<span className={theme.totalPages.outOf}>{count === -1 ? -1 : count}</span>
</span>

<ul className={theme.page.base}>
<button onClick={goToPrevPage} className={theme.page.previous} disabled={page === 1}>
Previous
</button>
{pageNumbers.map((pgNumber, index) => {
return (
<button
onClick={() => {
directPageChange(index + 1);
}}
key={index + 1}
className={twMerge(theme.page.pageNo, index + 1 === page ? "bg-blue-50 text-blue-600" : "")}
>
{pgNumber}
</button>
);
})}
<button
role="button"
onClick={goToNextPage}
className={theme.page.next}
disabled={count !== -1 ? page >= Math.ceil(count / rowsPerPage) - 1 : false}
>
Next
</button>
</ul>
</div>
);
},
);

TablePagination.displayName = "Table.Pagination";
2 changes: 2 additions & 0 deletions packages/ui/src/components/Table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export { TableHeadCell } from "./TableHeadCell";
export type { FlowbiteTableHeadCellTheme, TableHeadCellProps } from "./TableHeadCell";
export { TableRow } from "./TableRow";
export type { FlowbiteTableRowTheme, TableRowProps } from "./TableRow";
export { TablePagination } from "./TablePagination";
export type { FlowbiteTablePaginationTheme, TablePaginationProps } from "./TablePagination";
16 changes: 16 additions & 0 deletions packages/ui/src/components/Table/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ export const tableTheme: FlowbiteTableTheme = createTheme({
hovered: "hover:bg-gray-50 dark:hover:bg-gray-600",
striped: "odd:bg-white even:bg-gray-50 odd:dark:bg-gray-800 even:dark:bg-gray-700",
},
pagination: {
base: "mt-5 flex w-full flex-col flex-wrap items-center justify-between py-4 md:flex-row",
totalPages: {
base: "mb-4 block w-full text-sm font-normal text-gray-500 dark:text-gray-400 md:mb-0 md:inline md:w-auto",
pageRange: "mr-1 font-semibold text-gray-900 dark:text-white",
outOf: "ml-1 font-semibold text-gray-900 dark:text-white",
},
page: {
base: "inline-flex h-8 -space-x-px text-sm rtl:space-x-reverse",
next: "flex h-8 items-center justify-center rounded-e-lg border border-gray-300 bg-white px-3 leading-tight text-gray-500 hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white",
pageNo:
"flex h-8 items-center justify-center border border-gray-300 bg-white px-3 leading-tight text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white",
previous:
"ms-0 flex h-8 items-center justify-center rounded-s-lg border border-gray-300 bg-white px-3 leading-tight text-gray-500 hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white",
},
},
});