Skip to content

Commit

Permalink
feat(Table): adding TableCellSort to support sortable data (#501)
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 sorting functionality in tables with new `TableCellSort`
component, allowing users to sort data by clicking on table headers.
- **Documentation**
	- Updated documentation to include new sorting features in tables.
- **Tests**
	- Added tests for new sorting capabilities in table components.
- **Refactor**
	- Modified the timestamp in data.
	- Changed `ButtonIcon` props from `noBorder` to `noBsortDirection`.
- **Chores**
	- Added `TableCellSortDirections` and `TableCellSort` imports.
	- Updated the `importName` meta field to include `TableCellSort`.
- Added new constant `TableCellSortDirections` with sorting directions
`ASC` and `DESC`.
- Added `TableCellSort` and `TableCellSortDirections` exports to the
list of exports from the `Table` module.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
aversini authored Apr 15, 2024
1 parent d982767 commit 5115669
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 2 deletions.
117 changes: 115 additions & 2 deletions packages/documentation/src/Components/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import {
Table,
TableBody,
TableCell,
TableCellSort,
TableCellSortDirections,
TableFooter,
TableHead,
TableRow,
} from "@versini/ui-components";
import { IconDelete, IconRestore } from "@versini/ui-icons";
import { useState } from "react";

export default {
title: "Components/Table",
meta: {
importName: "Table, TableBody, TableCell, TableHead, TableRow",
importName:
"Table, TableBody, TableCell, TableCellSort, TableHead, TableRow",
},
args: {
mode: "system",
Expand All @@ -39,7 +43,7 @@ const data = [
id: 1,
character: "Paul Atreides",
actor: "Timothée Chalamet",
timestamp: "10/16/2023 08:46 PM EDT",
timestamp: "10/17/2023 08:46 PM EDT",
},
{
id: 2,
Expand Down Expand Up @@ -258,3 +262,112 @@ export const WithRowNumbers: Story<any> = (args) => {
</div>
);
};

export const Sortable: Story<any> = (args) => {
const [sortState, setSortState] = useState<{
cell: string;
direction: boolean | string;
}>({ direction: false, cell: "" });

const sortedData = data.sort((a, b) => {
switch (sortState.cell) {
case "actor":
case "character":
if (sortState.direction === TableCellSortDirections.ASC) {
return a[sortState.cell].localeCompare(b[sortState.cell]);
} else if (sortState.direction === TableCellSortDirections.DESC) {
return b[sortState.cell].localeCompare(a[sortState.cell]);
}
break;

case "timestamp":
if (sortState.direction === TableCellSortDirections.ASC) {
return (
new Date(a[sortState.cell]).getTime() -
new Date(b[sortState.cell]).getTime()
);
} else if (sortState.direction === TableCellSortDirections.DESC) {
return (
new Date(b[sortState.cell]).getTime() -
new Date(a[sortState.cell]).getTime()
);
}
break;

default:
return 0;
}
return 0;
});

const onClickSort = (key: string) => {
switch (sortState.direction) {
case false:
setSortState({ cell: key, direction: TableCellSortDirections.ASC });
break;
case TableCellSortDirections.ASC:
setSortState({ cell: key, direction: TableCellSortDirections.DESC });
break;
default:
setSortState({ cell: key, direction: TableCellSortDirections.ASC });
break;
}
};

return (
<div className="min-h-10">
<div className="flex flex-wrap gap-2">
<Table caption="Dune" {...args}>
<TableHead>
<TableRow>
<TableCellSort
scope="col"
cellId="timestamp"
align="left"
sortDirection={sortState.direction}
sortedCell={sortState.cell}
onClick={() => {
onClickSort("timestamp");
}}
>
Date
</TableCellSort>
<TableCellSort
cellId="character"
align="left"
sortDirection={sortState.direction}
sortedCell={sortState.cell}
onClick={() => {
onClickSort("character");
}}
>
Character
</TableCellSort>
<TableCellSort
cellId="actor"
align="left"
sortDirection={sortState.direction}
sortedCell={sortState.cell}
onClick={() => {
onClickSort("actor");
}}
>
Actor
</TableCellSort>
</TableRow>
</TableHead>

<TableBody>
{sortedData.map((row) => (
<TableRow key={row.id}>
<TableCell>{row.timestamp}</TableCell>
<TableCell>{row.character}</TableCell>
<TableCell>{row.actor}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
56 changes: 56 additions & 0 deletions packages/ui-components/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { IconDown, IconSort, IconUp } from "@versini/ui-icons";
import { useContext } from "react";

import { ButtonIcon } from "../Button/ButtonIcon";
import { TableContext } from "./TableContext";
import type {
TableBodyProps,
TableCellProps,
TableCellSortProps,
TableHeadProps,
TableProps,
TableRowProps,
Expand All @@ -17,6 +20,7 @@ import {
getTableFooterClasses,
getTableHeadClasses,
getTableRowClasses,
TableCellSortDirections,
} from "./utilities";

export const Table = ({
Expand Down Expand Up @@ -145,3 +149,55 @@ export const TableCell = ({
</Component>
);
};

export const TableCellSort = ({
align,
children,
className,
component,
focusMode = "alt-system",
mode = "alt-system",
onClick,
sortDirection,
sortedCell,
cellId,
...otherProps
}: TableCellSortProps) => {
return (
<TableCell
component={component}
className={className}
role="columnheader"
aria-sort={
sortDirection === TableCellSortDirections.ASC && sortedCell === cellId
? "ascending"
: sortDirection === TableCellSortDirections.DESC &&
sortedCell === cellId
? "descending"
: "other"
}
{...otherProps}
>
<ButtonIcon
className="rounded-none"
onClick={onClick}
align={align}
noBorder
focusMode={focusMode}
mode={mode}
fullWidth
labelRight={children}
>
{sortDirection === TableCellSortDirections.ASC &&
sortedCell === cellId ? (
<IconUp className="h-3 w-3" monotone />
) : sortDirection === TableCellSortDirections.DESC &&
sortedCell === cellId ? (
<IconDown className="h-3 w-3" monotone />
) : (
<IconSort className="h-3 w-3" monotone />
)}
</ButtonIcon>
</TableCell>
);
};
14 changes: 14 additions & 0 deletions packages/ui-components/src/components/Table/TableTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ export type TableCellProps = {
component?: "td" | "th";
} & React.ThHTMLAttributes<HTMLTableCellElement> &
React.TdHTMLAttributes<HTMLTableCellElement>;

export type TableCellSortProps = {
cellId: string;
children: string;
onClick: (event: React.MouseEvent<unknown>) => void;
sortDirection: "asc" | "desc" | false;
sortedCell: string;

align?: "left" | "center" | "right";
component?: "td" | "th";
focusMode?: "system" | "light" | "dark" | "alt-system";
mode?: "system" | "light" | "dark" | "alt-system";
} & React.ThHTMLAttributes<HTMLTableCellElement> &
React.TdHTMLAttributes<HTMLTableCellElement>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Table,
TableBody,
TableCell,
TableCellSort,
TableFooter,
TableHead,
TableRow,
Expand Down Expand Up @@ -540,6 +541,81 @@ describe("Table components", () => {
expect(tableCell.tagName).toBe("TD");
});

it("should render a sortable table cell with ASC icon", async () => {
render(
<table>
<tbody>
<tr>
<TableCellSort
cellId="cell-id"
sortDirection="asc"
sortedCell="cell-id"
onClick={() => {}}
data-testid="table-cell-sort"
>
hello
</TableCellSort>
</tr>
</tbody>
</table>,
);
const tableCell = await screen.findByTestId("table-cell-sort");
expect(tableCell).toBeInTheDocument();
expect(tableCell.tagName).toBe("TD");
expect(tableCell.querySelector("svg")).toBeInTheDocument();
expect(tableCell).toHaveAttribute("aria-sort", "ascending");
});

it("should render a sortable table cell with DESC icon", async () => {
render(
<table>
<tbody>
<tr>
<TableCellSort
cellId="cell-id"
sortDirection="desc"
sortedCell="cell-id"
onClick={() => {}}
data-testid="table-cell-sort"
>
hello
</TableCellSort>
</tr>
</tbody>
</table>,
);
const tableCell = await screen.findByTestId("table-cell-sort");
expect(tableCell).toBeInTheDocument();
expect(tableCell.tagName).toBe("TD");
expect(tableCell.querySelector("svg")).toBeInTheDocument();
expect(tableCell).toHaveAttribute("aria-sort", "descending");
});

it("should render a sortable table cell with non-sorted icon", async () => {
render(
<table>
<tbody>
<tr>
<TableCellSort
cellId="cell-id"
sortDirection={false}
sortedCell="cell-id"
onClick={() => {}}
data-testid="table-cell-sort"
>
hello
</TableCellSort>
</tr>
</tbody>
</table>,
);
const tableCell = await screen.findByTestId("table-cell-sort");
expect(tableCell).toBeInTheDocument();
expect(tableCell.tagName).toBe("TD");
expect(tableCell.querySelector("svg")).toBeInTheDocument();
expect(tableCell).toHaveAttribute("aria-sort", "other");
});

it("should render a generated table cell (th)", async () => {
render(
<table>
Expand Down
5 changes: 5 additions & 0 deletions packages/ui-components/src/components/Table/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export const CELL_WRAPPER_HEAD = "thead";
export const CELL_WRAPPER_FOOTER = "tfoot";
export const CELL_WRAPPER_BODY = "tbody";

export const TableCellSortDirections = {
ASC: "asc",
DESC: "desc",
};

export const getTableClasses = ({
mode,
className,
Expand Down
4 changes: 4 additions & 0 deletions packages/ui-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import {
Table,
TableBody,
TableCell,
TableCellSort,
TableFooter,
TableHead,
TableRow,
} from "./Table/Table";
import { TableCellSortDirections } from "./Table/utilities";

export {
Anchor,
Expand All @@ -39,6 +41,8 @@ export {
Table,
TableBody,
TableCell,
TableCellSort,
TableCellSortDirections,
TableFooter,
TableHead,
TableRow,
Expand Down

0 comments on commit 5115669

Please sign in to comment.