From 0bd1ee38218f0fc5ae4de881a1697e21f7b9d55b Mon Sep 17 00:00:00 2001 From: Vineeth Asok Kumar Date: Wed, 8 Nov 2023 12:01:55 +0100 Subject: [PATCH] Add actions to table (#201) * Add actions to table * Fix IconButton error * Add gap to actions * Add actions test cases * Update naming checked to isSelected * Update naming deleted to isDeleted * Optimise table code * Add delete table changes * Add delete row changes --- src/App.tsx | 63 ++++++---- src/components/Table/Table.test.tsx | 30 +++++ src/components/Table/Table.tsx | 183 ++++++++++++++++++++++++---- src/components/types.ts | 1 + 4 files changed, 230 insertions(+), 47 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e786287b..c3e79176 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,29 +33,12 @@ import { Text, EllipsisContent, Table, + TableRowType, } from "@/components"; import { Dialog } from "@/components/Dialog/Dialog"; import ConfirmationDialog from "@/components/ConfirmationDialog/ConfirmationDialog"; const headers = [{ label: "Company" }, { label: "Contact" }, { label: "Country" }]; -const rows = [ - { - id: "row-1", - items: [ - { label: "Alfreds Futterkiste" }, - { label: "Maria Anders" }, - { label: "Germany" }, - ], - }, - { - id: "row-2", - items: [ - { label: "Centro comercial Moctezuma" }, - { label: "Francisco Chang" }, - { label: "Mexico" }, - ], - }, -]; const App = () => { const [currentTheme, setCurrentTheme] = useState("dark"); const [selectedButton, setSelectedButton] = useState("center1"); @@ -63,7 +46,42 @@ const App = () => { const [disabled] = useState(false); const [open, setOpen] = useState(false); const ref = useRef(null); + const [rows, setRows] = useState>([ + { + id: "row-1", + items: [ + { label: "Alfreds Futterkiste" }, + { label: "Maria Anders" }, + { label: "Germany" }, + ], + }, + { + id: "row-2", + items: [ + { label: "Centro comercial Moctezuma" }, + { label: "Francisco Chang" }, + { label: "Mexico" }, + ], + }, + { + id: "row-3", + isDisabled: true, + items: [ + { label: "Centro comercial Moctezuma" }, + { label: "Francisco Chang" }, + { label: "Mexico" }, + ], + }, + ]); + const onTableDelete = (row: TableRowType, index: number) => { + setRows(rows => { + rows[index] = row; + return [...rows]; + }); + }; + + console.log(currentTheme); return (
{ turpis ex imperdiet enim, ac finibus nunc ante non est. Ut mattis ex magna, ac faucibus mi egestas interdum. + -
); }; diff --git a/src/components/Table/Table.test.tsx b/src/components/Table/Table.test.tsx index f61a7e71..46347ce1 100644 --- a/src/components/Table/Table.test.tsx +++ b/src/components/Table/Table.test.tsx @@ -68,4 +68,34 @@ describe("Table", () => { fireEvent.click(selectAllCheckbox); expect(onSelect).toBeCalledTimes(2); }); + + it("should trigger onDelete on clicking closeButton", () => { + const onDelete = jest.fn(); + const { queryByTestId, queryAllByTestId } = renderTable({ + isSelectable: true, + onDelete, + }); + expect(queryByTestId("table")).not.toBeNull(); + expect(queryAllByTestId("table-row-delete")).toHaveLength(2); + expect(queryByTestId("table-row-edit")).toBeNull(); + const rowCheckbox = queryAllByTestId("table-row-delete")[0]; + expect(rowCheckbox).not.toBeNull(); + fireEvent.click(rowCheckbox); + expect(onDelete).toBeCalledTimes(1); + }); + + it("should trigger onEdit on clicking editButton", () => { + const onEdit = jest.fn(); + const { queryByTestId, queryAllByTestId } = renderTable({ + isSelectable: true, + onEdit, + }); + expect(queryByTestId("table")).not.toBeNull(); + expect(queryAllByTestId("table-row-edit")).toHaveLength(2); + expect(queryByTestId("table-row-delete")).toBeNull(); + const rowCheckbox = queryAllByTestId("table-row-edit")[0]; + expect(rowCheckbox).not.toBeNull(); + fireEvent.click(rowCheckbox); + expect(onEdit).toBeCalledTimes(1); + }); }); diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 35b0afbf..713e7601 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,12 +1,13 @@ -import { Checkbox, HorizontalDirection, Icon, IconName } from "@/components"; +import { Checkbox, HorizontalDirection, Icon, IconButton, IconName } from "@/components"; import { HTMLAttributes, ReactNode, forwardRef } from "react"; import styled from "styled-components"; -interface TableHeaderProps extends HTMLAttributes { +export interface TableHeaderType extends HTMLAttributes { icon?: IconName; iconDir?: HorizontalDirection; label: ReactNode; } + const StyledHeader = styled.th` ${({ theme }) => ` padding: ${theme.click.table.header.cell.space.md.y} @@ -25,12 +26,7 @@ const HeaderContentWrapper = styled.div` gap: inherit; `; -const TableHeader = ({ - icon, - iconDir = "end", - label, - ...delegated -}: TableHeaderProps) => ( +const TableHeader = ({ icon, iconDir = "end", label, ...delegated }: TableHeaderType) => ( {icon && iconDir == "start" && ( @@ -50,11 +46,12 @@ const TableHeader = ({ ); interface TheadProps { - headers: Array; + headers: Array; isSelectable?: boolean; onSelectAll: (checked: boolean) => void; + showActionsHeader?: boolean; } -const Thead = ({ headers, isSelectable, onSelectAll }: TheadProps) => { +const Thead = ({ headers, isSelectable, onSelectAll, showActionsHeader }: TheadProps) => { return ( @@ -69,22 +66,32 @@ const Thead = ({ headers, isSelectable, onSelectAll }: TheadProps) => { {...headerProps} /> ))} + {showActionsHeader && } ); }; -const TableRow = styled.tr<{ $isSelectable?: boolean }>` +const TableRow = styled.tr<{ + $isSelectable?: boolean; + $isDeleted?: boolean; + $isDisabled?: boolean; + $showActions?: boolean; +}>` overflow: hidden; - ${({ theme }) => ` + ${({ theme, $isDeleted, $isDisabled }) => ` background-color: ${theme.click.table.row.color.background.default}; - border-bottom: ${theme.click.table.cell.stroke} solid ${theme.click.table.row.color.stroke.default}; + border-bottom: ${theme.click.table.cell.stroke} solid ${ + theme.click.table.row.color.stroke.default + }; &:active { background-color: ${theme.click.table.row.color.background.active}; } &:hover { background-color: ${theme.click.table.row.color.background.hover}; } + opacity: ${$isDeleted || $isDisabled ? 0.5 : 1}; + cursor: ${$isDeleted || $isDisabled ? "not-allowed" : "default"} `} &:last-of-type { @@ -95,7 +102,7 @@ const TableRow = styled.tr<{ $isSelectable?: boolean }>` position: relative; display: flex; flex-wrap: wrap; - ${({ theme, $isSelectable = false }) => ` + ${({ theme, $isSelectable = false, $showActions = false }) => ` border: ${theme.click.table.cell.stroke} solid ${ theme.click.table.row.color.stroke.default }; @@ -105,6 +112,11 @@ const TableRow = styled.tr<{ $isSelectable?: boolean }>` ? `padding-left: calc(${theme.click.table.body.cell.space.sm.x} + ${theme.click.table.body.cell.space.sm.x} + ${theme.click.checkbox.size.all});` : "" } + ${ + $showActions + ? `padding-right: calc(${theme.click.table.body.cell.space.sm.x} + ${theme.click.table.body.cell.space.sm.x} + ${theme.click.image.sm.size.width} + ${theme.click.button.iconButton.default.space.x} + ${theme.click.button.iconButton.default.space.x});` + : "" + } `} } `; @@ -172,6 +184,38 @@ const SelectData = styled.td` `} } `; +const ActionsList = styled.td` + overflow: hidden; + ${({ theme }) => ` + color: ${theme.click.table.row.color.text.default}; + font: ${theme.click.table.cell.text.default}; + padding: ${theme.click.table.body.cell.space.md.y} ${theme.click.table.body.cell.space.md.x}; + `} + @media (max-width: 768px) { + width: auto; + align-self: stretch; + position: absolute; + right: 0; + top: 0; + bottom: 0; + ${({ theme }) => ` + padding: ${theme.click.table.body.cell.space.sm.y} ${theme.click.table.body.cell.space.sm.x}; + border-left: 1px solid ${theme.click.table.row.color.stroke.default}; + `} + } +`; + +const ActionsContainer = styled.span` + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + overflow: hidden; + @media (max-width: 768px) { + flex-direction: column; + overflow: auto; + flex-wrap: nowrap; + } +`; const TableWrapper = styled.div` width: 100%; @@ -194,20 +238,41 @@ const MobileActions = styled.div` padding: 0 ${({ theme }) => theme.click.table.body.cell.space.sm.x}; } `; - +const EditButton = styled.button` + &:disabled { + background: transparent; + } +`; +const TableRowCloseButton = styled.button<{ + $isDeleted?: boolean; +}>` + svg { + transition: transform 200ms; + ${({ $isDeleted }) => ` + ${$isDeleted ? "transform: rotate(45deg)" : ""}; + `} + } + &:disabled { + background: transparent; + } +`; interface TableCellType extends HTMLAttributes { label: ReactNode; } -interface TableRowType +export interface TableRowType extends Omit, "onSelect" | "id"> { id: string | number; items: Array; + isDisabled?: boolean; + isDeleted?: boolean; } interface CommonTableProps extends Omit, "children" | "onSelect"> { - headers: Array; + headers: Array; rows: Array; + onDelete?: (item: TableRowType, index: number) => void; + onEdit?: (item: TableRowType, index: number) => void; } type SelectReturnValue = { @@ -230,10 +295,12 @@ interface NoSelectionType { export type TableProps = CommonTableProps & (SelectionType | NoSelectionType); interface TableBodyRowProps extends Omit { - headers: Array; + headers: Array; onSelect: (checked: boolean) => void; isSelectable?: boolean; - checked: boolean; + isSelected: boolean; + onDelete?: () => void; + onEdit?: () => void; } const TableBodyRow = ({ @@ -241,18 +308,27 @@ const TableBodyRow = ({ items, onSelect, isSelectable, - checked, + isSelected, + onDelete, + onEdit, + isDeleted, + isDisabled, ...rowProps }: TableBodyRowProps) => { + const isDeletable = typeof onDelete === "function"; + const isEditable = typeof onEdit === "function"; return ( {isSelectable && ( @@ -266,12 +342,53 @@ const TableBodyRow = ({ {label} ))} + {(isDeletable || isEditable) && ( + + + {isEditable && ( + + )} + {isDeletable && ( + + )} + + + )} ); }; const Table = forwardRef( - ({ headers, rows, isSelectable, selectedIds = [], onSelect, ...props }, ref) => { + ( + { + headers, + rows, + isSelectable, + selectedIds = [], + onSelect, + onDelete, + onEdit, + ...props + }, + ref + ) => { + const isDeletable = typeof onDelete === "function"; + const isEditable = typeof onEdit === "function"; const onSelectAll = (checked: boolean): void => { if (typeof onSelect === "function") { const ids = checked @@ -324,6 +441,7 @@ const Table = forwardRef( headers={headers} isSelectable={isSelectable} onSelectAll={onSelectAll} + showActionsHeader={isDeletable || isEditable} /> {rows.map(({ id, ...rowProps }, rowIndex) => ( @@ -331,8 +449,20 @@ const Table = forwardRef( key={`table-body-row-${rowIndex}`} headers={headers} isSelectable={isSelectable} - checked={selectedIds?.includes(id)} + isSelected={selectedIds?.includes(id)} onSelect={onRowSelect(id)} + onDelete={ + isDeletable + ? () => + onDelete( + { id, ...rowProps, isDeleted: !rowProps.isDeleted }, + rowIndex + ) + : undefined + } + onEdit={ + isEditable ? () => onEdit({ id, ...rowProps }, rowIndex) : undefined + } {...rowProps} /> ))} @@ -347,10 +477,13 @@ const Table = forwardRef( const StyledTable = styled.table` border-spacing: 0; overflow: hidden; - ${({ theme }) => ` + ${({ theme }) => { + console.log(theme); + return ` border-radius: ${theme.click.table.radii.all}; border: ${theme.click.table.cell.stroke} solid ${theme.click.table.global.color.stroke.default}; - `} + `; + }} @media (max-width: 768px) { border: none; diff --git a/src/components/types.ts b/src/components/types.ts index 68340e41..b82fb48f 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -35,6 +35,7 @@ export type { FlyoutProps, FlyoutFooterProps, FlyoutHeaderProps } from "./Flyout export type { DialogContentProps } from "./Dialog/Dialog"; export type { DialogProps, DialogTriggerProps } from "@radix-ui/react-dialog"; export type { FileTabStatusType } from "./FileTabs/FileTabs"; +export type { TableHeaderType, TableRowType, TableProps } from "./Table/Table"; export type States = "default" | "active" | "disabled" | "error" | "hover"; export type HorizontalDirection = "start" | "end";