diff --git a/packages/store-ui/src/index.ts b/packages/store-ui/src/index.ts index b7214dfc39..d530aa434b 100644 --- a/packages/store-ui/src/index.ts +++ b/packages/store-ui/src/index.ts @@ -60,17 +60,35 @@ export type { IconButtonProps } from './molecules/IconButton' export { default as Modal } from './molecules/Modal' export type { ModalProps } from './molecules/Modal' -export { default as Accordion } from './molecules/Accordion' -export type { AccordionProps } from './molecules/Accordion' - -export { AccordionItem } from './molecules/Accordion' -export type { AccordionItemProps } from './molecules/Accordion' - -export { AccordionButton } from './molecules/Accordion' -export type { AccordionButtonProps } from './molecules/Accordion' - -export { AccordionPanel } from './molecules/Accordion' -export type { AccordionPanelProps } from './molecules/Accordion' +export { + default as Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, +} from './molecules/Accordion' +export type { + AccordionProps, + AccordionItemProps, + AccordionButtonProps, + AccordionPanelProps, +} from './molecules/Accordion' + +export { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableRow, +} from './molecules/Table' +export type { + TableProps, + TableBodyProps, + TableCellProps, + TableFooterProps, + TableHeadProps, + TableRowProps, +} from './molecules/Table' // Hooks export { default as useSlider } from './hooks/useSlider' diff --git a/packages/store-ui/src/molecules/Table/Table.test.tsx b/packages/store-ui/src/molecules/Table/Table.test.tsx new file mode 100644 index 0000000000..f02adea1a7 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/Table.test.tsx @@ -0,0 +1,237 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { axe } from 'jest-axe' + +import Table from './Table' +import TableHead from './TableHead' +import TableRow from './TableRow' +import TableBody from './TableBody' +import TableCell from './TableCell' +import TableFooter from './TableFooter' + +describe('Table', () => { + describe('Data attributes', () => { + it('should render a simple table with all expected data-attributes', () => { + const { getByTestId, queryAllByTestId } = render( + + + + Name + Age + + + + + John + 25 + + + Carter + 25 + + + + + Name + Age + + +
+ ) + + const table = getByTestId('store-table') + + expect(table).toHaveAttribute('data-store-table') + + const tableHead = getByTestId('store-table-head') + + expect(tableHead).toHaveAttribute('data-store-table-head') + + const tableBody = getByTestId('store-table-body') + + expect(tableBody).toHaveAttribute('data-store-table-body') + + const tableFooter = getByTestId('store-table-footer') + + expect(tableFooter).toHaveAttribute('data-store-table-footer') + + const tableRows = queryAllByTestId('store-table-row') + + expect(tableRows).toHaveLength(4) + tableRows.forEach((row) => { + expect(row).toHaveAttribute('data-store-table-row') + }) + + const tableCells = queryAllByTestId('store-table-cell') + + // Make sure 8 cells were rendered and all contain the + // data-store-table-cell attribute. + expect(tableCells).toHaveLength(8) + tableCells.forEach((row) => { + expect(row).toHaveAttribute('data-store-table-cell') + }) + + // Make sure that 2 header cells and 6 data cells were rendered, with their + // corresponding attributes. + expect( + table.querySelectorAll('[data-store-table-cell=header]') + ).toHaveLength(2) + expect( + table.querySelectorAll('[data-store-table-cell=data]') + ).toHaveLength(6) + }) + }) + + // WAI-ARIA tests + // https://www.w3.org/WAI/tutorials/tables/ + describe('Table WAI-ARIA Specifications', () => { + describe('Tables with one header', () => { + it('should have no violations on a table with header cells in the top row only', async () => { + const { container } = render( + + + + Name + Age + + + + + John + 25 + + + Carter + 25 + + +
+ ) + + expect(await axe(container)).toHaveNoViolations() + }) + + it('should have no violations on a table with header cells in the first column only', async () => { + const { container } = render( + + + + Date + 12 February + 24 March + 14 April + + + Event + Waltz with Strauss + The Obelisks + The What + + + Venue + Main Hall + West Wing + Main Hall + + +
+ ) + + expect(await axe(container)).toHaveNoViolations() + }) + + // https://www.w3.org/WAI/WCAG21/Techniques/html/H63 + it('should have no violations on a table with ambiguous data, where scope should be used', async () => { + const { container } = render( + + + + + Last Name + + + First Name + + + City + + + + + + Phoenix + Imari + Henry + + + Zeki + Rome + Min + + + Apirka + Kelly + Brynn + + +
+ ) + + expect(await axe(container)).toHaveNoViolations() + }) + }) + + describe('Tables with two headers', () => { + it('should have no violations on a table with header cells in the top row and first column', async () => { + const { container } = render( + + + + + + Monday + + + Tuesday + + + Wednesday + + + Thursday + + + Friday + + + + + + + 09:00 - 11:00 + + Closed + Open + Open + Closed + Closed + + + + 11:00 - 13:00 + + Open + Open + Closed + Closed + Closed + + +
+ ) + + expect(await axe(container)).toHaveNoViolations() + }) + }) + }) +}) diff --git a/packages/store-ui/src/molecules/Table/Table.tsx b/packages/store-ui/src/molecules/Table/Table.tsx new file mode 100644 index 0000000000..64aae04ecd --- /dev/null +++ b/packages/store-ui/src/molecules/Table/Table.tsx @@ -0,0 +1,23 @@ +import type { HTMLAttributes } from 'react' +import React, { forwardRef } from 'react' + +export interface TableProps extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string + children: React.ReactNode +} + +const Table = forwardRef(function Table( + { testId = 'store-table', children, ...otherProps }, + ref +) { + return ( + + {children} +
+ ) +}) + +export default Table diff --git a/packages/store-ui/src/molecules/Table/TableBody.tsx b/packages/store-ui/src/molecules/Table/TableBody.tsx new file mode 100644 index 0000000000..40c6066495 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/TableBody.tsx @@ -0,0 +1,31 @@ +import type { HTMLAttributes } from 'react' +import React, { forwardRef } from 'react' + +export interface TableBodyProps + extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string + children: React.ReactNode +} + +const TableBody = forwardRef( + function TableBody( + { children, testId = 'store-table-body', ...otherProps }, + ref + ) { + return ( + + {children} + + ) + } +) + +export default TableBody diff --git a/packages/store-ui/src/molecules/Table/TableCell.tsx b/packages/store-ui/src/molecules/Table/TableCell.tsx new file mode 100644 index 0000000000..ba7a4b82d2 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/TableCell.tsx @@ -0,0 +1,49 @@ +import type { HTMLAttributes } from 'react' +import React, { forwardRef } from 'react' + +type TableCellVariant = 'data' | 'header' + +export interface TableCellProps extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string + /** + * Specify if this component should be rendered as a header (``) or as a data cell (``). + */ + variant?: TableCellVariant + /** + * Defines the cells that the header element (``) relates to. + * @see scope https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th#attr-scope + */ + scope?: 'col' | 'row' | 'rowgroup' | 'colgroup' +} + +const TableCell = forwardRef( + function TableCell( + { + testId = 'store-table-cell', + children, + variant = 'data', + scope, + ...otherProps + }, + ref + ) { + const Cell = variant === 'header' ? 'th' : 'td' + + return ( + + {children} + + ) + } +) + +export default TableCell diff --git a/packages/store-ui/src/molecules/Table/TableFooter.tsx b/packages/store-ui/src/molecules/Table/TableFooter.tsx new file mode 100644 index 0000000000..310fef50a3 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/TableFooter.tsx @@ -0,0 +1,31 @@ +import type { HTMLAttributes } from 'react' +import React, { forwardRef } from 'react' + +export interface TableFooterProps + extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string + children: React.ReactNode +} + +const TableFooter = forwardRef( + function TableFooter( + { children, testId = 'store-table-footer', ...otherProps }, + ref + ) { + return ( + + {children} + + ) + } +) + +export default TableFooter diff --git a/packages/store-ui/src/molecules/Table/TableHead.tsx b/packages/store-ui/src/molecules/Table/TableHead.tsx new file mode 100644 index 0000000000..ba7de08ed2 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/TableHead.tsx @@ -0,0 +1,31 @@ +import type { HTMLAttributes } from 'react' +import React, { forwardRef } from 'react' + +export interface TableHeadProps + extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string + children: React.ReactNode +} + +const TableHead = forwardRef( + function TableHead( + { children, testId = 'store-table-head', ...otherProps }, + ref + ) { + return ( + + {children} + + ) + } +) + +export default TableHead diff --git a/packages/store-ui/src/molecules/Table/TableRow.tsx b/packages/store-ui/src/molecules/Table/TableRow.tsx new file mode 100644 index 0000000000..b53b73ff94 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/TableRow.tsx @@ -0,0 +1,25 @@ +import type { HTMLAttributes } from 'react' +import React, { forwardRef } from 'react' + +export interface TableRowProps extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string + children: React.ReactNode +} + +const TableRow = forwardRef( + function TableRow( + { testId = 'store-table-row', children, ...otherProps }, + ref + ) { + return ( + + {children} + + ) + } +) + +export default TableRow diff --git a/packages/store-ui/src/molecules/Table/index.ts b/packages/store-ui/src/molecules/Table/index.ts new file mode 100644 index 0000000000..aeed79e8af --- /dev/null +++ b/packages/store-ui/src/molecules/Table/index.ts @@ -0,0 +1,17 @@ +export { default as Table } from './Table' +export type { TableProps } from './Table' + +export { default as TableRow } from './TableRow' +export type { TableRowProps } from './TableRow' + +export { default as TableCell } from './TableCell' +export type { TableCellProps } from './TableCell' + +export { default as TableBody } from './TableBody' +export type { TableBodyProps } from './TableBody' + +export { default as TableHead } from './TableHead' +export type { TableHeadProps } from './TableHead' + +export { default as TableFooter } from './TableFooter' +export type { TableFooterProps } from './TableFooter' diff --git a/packages/store-ui/src/molecules/Table/stories/Table.mdx b/packages/store-ui/src/molecules/Table/stories/Table.mdx new file mode 100644 index 0000000000..50ad62be14 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/stories/Table.mdx @@ -0,0 +1,73 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs' + +import Table from '../Table' +import TableBody from '../TableBody' +import TableCell from '../TableCell' +import TableHead from '../TableHead' +import TableRow from '../TableRow' +import TableFooter from '../TableFooter' + +# Table + + + + + +## Components + +The `Table` uses the [Compound Component](https://kentcdodds.com/blog/compound-components-with-react-hooks) pattern, its components are: + +- `Table`, renders a `` tag. +- `TableBody`, renders a `` tag. +- `TableHead`, renders a `` tag. +- `TableRow`, renders a `` tag. +- `TableFooter`, renders a `` tag. +- `TableCell`, can render a `
` or `` tag, according to a `variant` prop that supports values `"header"` and `"data"`. + +## Props + +All table-related components support all attributes also supported by their respective HTML tags. + +Besides those attributes, the following props are also supported: + +### `Table` component + + + +### `TableBody` component + + + +### `TableHead` component + + + +### `TableRow` component + + + +### `TableFooter` component + + + +### `TableCell` component + + + +## CSS Selectors + +```css +[data-store-table] {} + +[data-store-table-head] {} + +[data-store-table-row] {} + +[data-store-table-footer] {} + +[data-store-table-body] {} + +[data-store-table-cell='head'] {} + +[data-store-table-cell='data'] {} +``` diff --git a/packages/store-ui/src/molecules/Table/stories/Table.stories.tsx b/packages/store-ui/src/molecules/Table/stories/Table.stories.tsx new file mode 100644 index 0000000000..b2b52fb4c1 --- /dev/null +++ b/packages/store-ui/src/molecules/Table/stories/Table.stories.tsx @@ -0,0 +1,98 @@ +import type { Story } from '@storybook/react' +import React from 'react' + +import type { TableProps } from '../Table' +import TableComponent from '../Table' +import TableHead from '../TableHead' +import TableRow from '../TableRow' +import TableBody from '../TableBody' +import TableCell from '../TableCell' +import TableFooter from '../TableFooter' +import Price from '../../../atoms/Price' +import mdx from './Table.mdx' + +function priceFormatter(price: number) { + const formattedPrice = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(price) + + return formattedPrice +} + +const options = [ + { + numberOfInstallments: 1, + monthlyPayment: 200, + total: 200, + }, + { + numberOfInstallments: 2, + monthlyPayment: 100, + total: 200, + }, + { + numberOfInstallments: 3, + monthlyPayment: 68, + total: 204, + }, + { + numberOfInstallments: 4, + monthlyPayment: 52, + total: 208, + }, + { + numberOfInstallments: 5, + monthlyPayment: 43, + total: 215, + }, +] + +const TableTemplate: Story = () => ( + + + + + Installments + + + Amount + + + Total + + + + + {options.map((option) => ( + + {option.numberOfInstallments}x + + + + + + + + ))} + + + + Installments + Amount + Total + + + +) + +export const Table = TableTemplate.bind({}) + +export default { + title: 'Molecules/Table', + parameters: { + docs: { + page: mdx, + }, + }, +} diff --git a/themes/theme-b2c-tailwind/src/molecules/index.css b/themes/theme-b2c-tailwind/src/molecules/index.css index 71333119a1..0ad04b428e 100644 --- a/themes/theme-b2c-tailwind/src/molecules/index.css +++ b/themes/theme-b2c-tailwind/src/molecules/index.css @@ -4,4 +4,5 @@ @import './price-range.css'; @import "./carousel.css"; @import "./modal.css"; -@import "./accordion.css"; \ No newline at end of file +@import "./accordion.css"; +@import "./table.css"; diff --git a/themes/theme-b2c-tailwind/src/molecules/table.css b/themes/theme-b2c-tailwind/src/molecules/table.css new file mode 100644 index 0000000000..35072609d1 --- /dev/null +++ b/themes/theme-b2c-tailwind/src/molecules/table.css @@ -0,0 +1,31 @@ +[data-store-table]{ + @apply table-auto divide-y divide-gray-200; +} + +[data-store-table-head]{ + @apply bg-gray-50; +} + +[data-store-table-body] { + @apply bg-white divide-y divide-gray-200; +} + +[data-store-table-cell="header"]{ + @apply px-6 py-3 text-left text-xs text-gray-900 font-bold uppercase tracking-wider; +} + +[data-store-table-cell="data"]{ + @apply px-6 py-4; +} + +[data-store-table-cell] > [data-store-price][data-selling] { + @apply text-sm font-medium text-gray-900; +} + +[data-store-table-footer]{ + @apply bg-gray-50; +} + +[data-store-table-footer] [data-store-table-cell="data"]{ + @apply px-6 py-3 text-left text-xs text-gray-900 font-bold uppercase tracking-wider; +}