From 5f8015c3817b7e9437d3436b65f37552be261da4 Mon Sep 17 00:00:00 2001 From: Zulfeekar Cheriyam Purath <126802050+zulu-eq-bouvet@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:50:23 +0200 Subject: [PATCH] Feat: Table footer in `eds-core-react` & `eds-data-grid-react` (#3624) * feat: add Table footer component to `eds-core-react` - Implemented the Table footer component with optional sticky property for a fixed footer. - Updated `Table.docs.mdx` to include documentation for the Table footer. - Added Table footer to the storybook to showcase various use cases. - Updated test suites to cover edge cases and ensure proper functionality of the Table footer. * feat(eds-data-grid): add Table Footer component feat(eds-data-grid): add Table Footer component - Added the possibility to show a footer in the data grid. - Enabled the option to make the footer fixed. - Added `footerClass` and `footerStyle` as component props for custom styling. - Updated docs and stories to include the new footer functionality. - Updated test suites to cover edge cases and ensure proper functionality of the Table Footer. chore: extract Resizer and TableCell components -`Resizer` is now a separate module to enhance reusability and scalability. - Renamed `Cell` to `TableCell` and moved it to a separate module for better reusability and scalability. --- .../src/components/Table/Cell.tsx | 3 + .../src/components/Table/Foot/Foot.tokens.ts | 26 + .../src/components/Table/Foot/Foot.tsx | 27 + .../src/components/Table/Foot/index.ts | 1 + .../Table/FooterCell/FooterCell.tsx | 69 +++ .../src/components/Table/FooterCell/index.ts | 1 + .../src/components/Table/Inner.context.tsx | 6 +- .../src/components/Table/Table.docs.mdx | 24 + .../src/components/Table/Table.stories.tsx | 191 ++++++- .../src/components/Table/Table.test.tsx | 60 ++- .../src/components/Table/index.tsx | 5 + .../src/EdsDataGrid.docs.mdx | 2 + .../src/EdsDataGrid.stories.tsx | 36 +- .../eds-data-grid-react/src/EdsDataGrid.tsx | 23 +- .../src/EdsDataGridContext.tsx | 4 + .../src/EdsDataGridProps.ts | 19 + .../src/components/Resizer.tsx | 33 ++ .../src/components/TableCell.tsx | 29 ++ .../src/components/TableFooterCell.tsx | 85 +++ .../src/components/TableFooterRow.tsx | 35 ++ .../src/components/TableHeaderCell.tsx | 67 +-- .../src/stories/columns.tsx | 54 +- .../eds-data-grid-react/src/stories/data.ts | 488 +++++++++++++++++- .../src/tests/EdsDataGrid.test.tsx | 52 +- .../src/tests/Filter.test.tsx | 11 +- 25 files changed, 1276 insertions(+), 75 deletions(-) create mode 100644 packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts create mode 100644 packages/eds-core-react/src/components/Table/Foot/Foot.tsx create mode 100644 packages/eds-core-react/src/components/Table/Foot/index.ts create mode 100644 packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx create mode 100644 packages/eds-core-react/src/components/Table/FooterCell/index.ts create mode 100644 packages/eds-data-grid-react/src/components/Resizer.tsx create mode 100644 packages/eds-data-grid-react/src/components/TableCell.tsx create mode 100644 packages/eds-data-grid-react/src/components/TableFooterCell.tsx create mode 100644 packages/eds-data-grid-react/src/components/TableFooterRow.tsx diff --git a/packages/eds-core-react/src/components/Table/Cell.tsx b/packages/eds-core-react/src/components/Table/Cell.tsx index f2c1a1213f..8164e37825 100644 --- a/packages/eds-core-react/src/components/Table/Cell.tsx +++ b/packages/eds-core-react/src/components/Table/Cell.tsx @@ -2,6 +2,7 @@ import { TdHTMLAttributes, ThHTMLAttributes, forwardRef } from 'react' import { Variants, Colors } from './Table.types' import { TableDataCell } from './DataCell' import { TableHeaderCell } from './HeaderCell' +import { TableFooterCell } from './FooterCell' import { InnerContext } from './Inner.context' export type CellProps = { @@ -25,6 +26,8 @@ export const Cell = forwardRef(function Cell( switch (variant) { case 'head': return + case 'foot': + return default: case 'body': return diff --git a/packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts b/packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts new file mode 100644 index 0000000000..438a7fb250 --- /dev/null +++ b/packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts @@ -0,0 +1,26 @@ +import { tokens } from '@equinor/eds-tokens' +import type { ComponentToken } from '@equinor/eds-tokens' + +const { + colors: { + ui: { + background__medium: { rgba: borderColor }, + }, + interactive: { + table__header__fill_resting: { rgba: backgroundColor }, + }, + }, +} = tokens + +export const token: ComponentToken = { + background: backgroundColor, + border: { + type: 'bordergroup', + bottom: { + type: 'border', + width: '2px', + color: borderColor, + style: 'solid', + }, + }, +} diff --git a/packages/eds-core-react/src/components/Table/Foot/Foot.tsx b/packages/eds-core-react/src/components/Table/Foot/Foot.tsx new file mode 100644 index 0000000000..0ca690110d --- /dev/null +++ b/packages/eds-core-react/src/components/Table/Foot/Foot.tsx @@ -0,0 +1,27 @@ +import { HTMLAttributes, forwardRef } from 'react' +import styled from 'styled-components' +import { token } from './Foot.tokens' +import { bordersTemplate } from '@equinor/eds-utils' +import { InnerContext } from '../Inner.context' + +const StyledTableFoot = styled.tfoot` + ${bordersTemplate(token.border)} + background: ${token.background}; +` + +export type FootProps = { + /** Footer will stick to bottom when scrolling */ + sticky?: boolean +} & HTMLAttributes + +export const Foot = forwardRef( + function Foot({ children, sticky, ...props }, ref) { + return ( + + + {children} + + + ) + }, +) diff --git a/packages/eds-core-react/src/components/Table/Foot/index.ts b/packages/eds-core-react/src/components/Table/Foot/index.ts new file mode 100644 index 0000000000..ac5f14fa2d --- /dev/null +++ b/packages/eds-core-react/src/components/Table/Foot/index.ts @@ -0,0 +1 @@ +export * from './Foot' diff --git a/packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx b/packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx new file mode 100644 index 0000000000..f0e9fe7e48 --- /dev/null +++ b/packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx @@ -0,0 +1,69 @@ +import { ThHTMLAttributes, forwardRef } from 'react' +import styled, { css, ThemeProvider } from 'styled-components' +import { + typographyTemplate, + spacingsTemplate, + bordersTemplate, + useToken, +} from '@equinor/eds-utils' +import { + token as tableFoot, + TableHeadToken as TableFootToken, +} from './../HeaderCell/HeaderCell.tokens' // Use Header cell tokens as default +import { useEds } from '../../EdsProvider' + +type BaseProps = { + theme: TableFootToken + $sticky: boolean +} + +const StyledTableCell = styled.th((props: BaseProps) => { + const { theme, $sticky } = props + const { background, height, typography, spacings } = theme + + return css` + min-height: ${height}; + height: ${height}; + background: ${background}; + box-sizing: border-box; + ${spacingsTemplate(spacings)} + ${typographyTemplate(typography)} + ${bordersTemplate(theme.border)} + ${$sticky + ? css` + position: sticky; + bottom: 0; + z-index: 2; + ` + : ''} + ` +}) + +const CellInner = styled.div` + display: flex; + align-items: center; +` + +type CellProps = { + sticky?: boolean +} & ThHTMLAttributes + +export const TableFooterCell = forwardRef( + function TableFooterCell({ children, sticky, ...rest }, ref) { + const { density } = useEds() + const token = useToken({ density }, tableFoot) + const props = { + ref, + $sticky: sticky, + ...rest, + } + + return ( + + + {children} + + + ) + }, +) diff --git a/packages/eds-core-react/src/components/Table/FooterCell/index.ts b/packages/eds-core-react/src/components/Table/FooterCell/index.ts new file mode 100644 index 0000000000..ed29c1d26a --- /dev/null +++ b/packages/eds-core-react/src/components/Table/FooterCell/index.ts @@ -0,0 +1 @@ +export * from './FooterCell' diff --git a/packages/eds-core-react/src/components/Table/Inner.context.tsx b/packages/eds-core-react/src/components/Table/Inner.context.tsx index 61519db0e9..4776b57374 100644 --- a/packages/eds-core-react/src/components/Table/Inner.context.tsx +++ b/packages/eds-core-react/src/components/Table/Inner.context.tsx @@ -1,12 +1,12 @@ import { createContext } from 'react' type State = { - variant: 'body' | 'head' + variant: 'body' | 'head' | 'foot' sticky?: boolean } -const initalState: State = { +const initialState: State = { variant: 'body', } -export const InnerContext = createContext(initalState) +export const InnerContext = createContext(initialState) diff --git a/packages/eds-core-react/src/components/Table/Table.docs.mdx b/packages/eds-core-react/src/components/Table/Table.docs.mdx index 484c28a6a8..d0be31ad68 100644 --- a/packages/eds-core-react/src/components/Table/Table.docs.mdx +++ b/packages/eds-core-react/src/components/Table/Table.docs.mdx @@ -42,6 +42,15 @@ Tables display information in an easy to scan format.
  • Icons can be used for sorting columns as well as a alternative text.
  • + +**Table Footer** +
      +
    • The table footer is used to display summary information, totals, or additional notes that relate to the data presented in the table body.
    • +
    • It serves as the concluding section of the table, often providing context or aggregated data that complements the table's content.
    • +
    • The width of footer cells is customizable using the `colSpan` prop, allowing footer cells to span multiple columns. This is useful for displaying summary data or totals that need to align visually across the table.
    • +
    + + **Table Cell**
    • The cell row can have icons, text, links, inputs, numbers, monospaced numbers and other custom content (by using the placeholder).
    • @@ -80,6 +89,11 @@ const Demo = () => { 2.5 + + + Footer + + ) } @@ -87,6 +101,12 @@ const Demo = () => { ## Examples +### Fixed Table Header & Footer + + + + + ### Fixed table header @@ -108,3 +128,7 @@ To render very large datasets, "virtual scrolling" might sometimes be a good opt combination with 3rd party solutions such as react-virtual to achieve this. + + +### Virtual scrolling with fixed footer + diff --git a/packages/eds-core-react/src/components/Table/Table.stories.tsx b/packages/eds-core-react/src/components/Table/Table.stories.tsx index aca758d904..bb66681e0e 100644 --- a/packages/eds-core-react/src/components/Table/Table.stories.tsx +++ b/packages/eds-core-react/src/components/Table/Table.stories.tsx @@ -22,12 +22,12 @@ import page from './Table.docs.mdx' Icon.add({ arrow_down, arrow_up }) -const { Caption, Body, Row, Cell, Head } = Table +const { Caption, Body, Row, Cell, Head, Foot } = Table const meta: Meta = { title: 'Data Display/Table', component: Table, - subcomponents: { Caption, Head, Body, Cell, Row }, + subcomponents: { Caption, Head, Body, Cell, Row, Foot }, parameters: { docs: { page, @@ -66,6 +66,11 @@ export const introduction: StoryFn = (args) => { ))} + + + Footer + + ) } @@ -113,6 +118,46 @@ export const FixedTableHeader: StoryFn = () => { } FixedTableHeader.storyName = 'Fixed table header' +export const FixedTableHeaderAndFooter: StoryFn = () => { + const cellValues = toCellValues(data, columns) + const total = data.reduce((acc, curr) => acc + curr?.price, 0) + return ( +
      + + + + Fruits cost price + + + + + {columns.map((col) => ( + {col.name} + ))} + + + + {cellValues?.map((row) => ( + + {row.map((cellValue) => ( + {cellValue} + ))} + + ))} + + + + Total + {total} + + +
      +
      + ) +} + +FixedTableHeaderAndFooter.storyName = 'Fixed table header and footer' + export const CompactTable: StoryFn = () => { const cellValues = toCellValues(data, columns) @@ -213,6 +258,11 @@ export const CompactTable: StoryFn = () => { ))} + + + Footer + + @@ -343,6 +393,11 @@ export const Sortable: StoryFn = () => { ))} + + + Footer + + ) } @@ -364,6 +419,133 @@ type Photo = { thumbnailUrl: string } +export const VirtualScrollingWithFixedFooter: StoryFn = () => { + const [data, setData] = useState>([]) + const parentRef = useRef() + + const estimateSize = useCallback(() => { + return 48 + }, []) + + const virtualizer = useVirtualizer({ + count: data.length, + getScrollElement: () => parentRef.current, + estimateSize, + }) + + useEffect(() => { + const abortController = new AbortController() + const signal = abortController.signal + + fetch(`https://jsonplaceholder.typicode.com/photos`, { signal }) + .then((r) => r.json()) + .then((d: Photo[]) => { + setData(d.slice(0, 100)) + }) + .catch((err: Error) => { + console.error(`Error: ${err.message}`) + }) + return () => { + abortController.abort() + } + }, []) + + const virtualRows = virtualizer.getVirtualItems() + const paddingTop = virtualRows.length ? virtualRows[0].start : 0 + const paddingBottom = virtualRows.length + ? virtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end + : 0 + + return ( +
      + + + + +
      ID
      +
      + +
      Album ID
      +
      + +
      Title
      +
      + +
      URL
      +
      + +
      Thumbnail url
      +
      +
      +
      + + + + + {virtualRows.map((virtualRow) => { + const row: Photo = data[virtualRow.index] + + return ( + + +
      {row.id}
      +
      + +
      {row.albumId}
      +
      + +
      + {row.title} +
      +
      + +
      + {' '} + + Open image + +
      +
      + +
      + {' '} + + Open thumbnail + +
      +
      +
      + ) + })} + + + +
      + + + {`Total ${data?.length} items`} + + +
      +
      + ) +} + export const VirtualScrolling: StoryFn = () => { const [data, setData] = useState>([]) const parentRef = useRef() @@ -481,6 +663,11 @@ export const VirtualScrolling: StoryFn = () => { + + + Footer + + ) diff --git a/packages/eds-core-react/src/components/Table/Table.test.tsx b/packages/eds-core-react/src/components/Table/Table.test.tsx index 2a517222e4..02ee8a01a0 100644 --- a/packages/eds-core-react/src/components/Table/Table.test.tsx +++ b/packages/eds-core-react/src/components/Table/Table.test.tsx @@ -1,12 +1,50 @@ /* eslint-disable no-undef */ -import { render, screen } from '@testing-library/react' +import { render, screen, within } from '@testing-library/react' import { axe } from 'jest-axe' import { Table } from '.' import styled from 'styled-components' import { tableCell as dataCellToken } from './DataCell/DataCell.tokens' import { token as headerCellToken } from './HeaderCell/HeaderCell.tokens' -const { Caption, Cell, Head, Row, Body } = Table +const { Caption, Cell, Head, Row, Body, Foot } = Table + +const RenderFooterTable = ({ sticky = false }: { sticky?: boolean }) => { + return ( + + + + Country + Tax + Discount + + + + + Italy + 42 + 12 + + + Norway + 50 + 10 + + + Swedend + 32 + 15 + + + + + Total + 54 + 4 + + +
      + ) +} describe('Caption', () => { it('Renders a caption with provided text', () => { @@ -296,4 +334,22 @@ describe('Table', () => { ) expect(headerCell2).toHaveStyleRule('border-color', borderBottomColor) }) + it('Renders a table with fixed footer if is provided', () => { + render() + const foot = screen.getByTestId('footer') // Assert Footer is available in the document + expect(foot).toBeInTheDocument() + const thElements = within(foot).getAllByRole('columnheader') + thElements.forEach((th) => { + expect(th).toHaveStyle('position: sticky') // Ensure this one is sticky + }) + }) + it('Renders a table with footer ( not fixed ) if is provided', () => { + render() + const foot = screen.getByTestId('footer') // Assert Footer is available in the document + expect(foot).toBeInTheDocument() + const thElements = within(foot).getAllByRole('columnheader') + thElements.forEach((th) => { + expect(th).not.toHaveStyle('position: sticky') // Ensure this one is not sticky + }) + }) }) diff --git a/packages/eds-core-react/src/components/Table/index.tsx b/packages/eds-core-react/src/components/Table/index.tsx index b70971a2f6..03418cde86 100644 --- a/packages/eds-core-react/src/components/Table/index.tsx +++ b/packages/eds-core-react/src/components/Table/index.tsx @@ -2,6 +2,7 @@ import { Table as BaseTable, TableProps } from './Table' import { Body, BodyProps } from './Body' import { Cell, CellProps } from './Cell' import { Head, HeadProps } from './Head' +import { Foot, FootProps } from './Foot' import { Row, RowProps } from './Row' import { Caption, CaptionProps } from './Caption' @@ -9,6 +10,7 @@ type TableCompoundProps = typeof BaseTable & { Body: typeof Body Cell: typeof Cell Head: typeof Head + Foot: typeof Foot Row: typeof Row Caption: typeof Caption } @@ -18,12 +20,14 @@ const Table = BaseTable as TableCompoundProps Table.Body = Body Table.Cell = Cell Table.Head = Head +Table.Foot = Foot Table.Row = Row Table.Caption = Caption Table.Body.displayName = 'Table.Body' Table.Cell.displayName = 'Table.Cell' Table.Head.displayName = 'Table.Head' +Table.Foot.displayName = 'Table.Foot' Table.Row.displayName = 'Table.Row' Table.Caption.displayName = 'Table.Caption' @@ -35,4 +39,5 @@ export type { RowProps, CaptionProps, HeadProps, + FootProps, } diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx b/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx index 10c6c5c24d..8b4d37349c 100644 --- a/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx +++ b/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx @@ -173,5 +173,7 @@ Comes with 4 different ways of styling built in: - cellStyle: Allows to add custom css to a cell (td) based on a function - headerClass: Allows to add a class to a header cell (th) based on a function - headerStyle: Allows to add custom css to a header cell (th) based on a function +- footerClass: Allows to add a class to a footer cell (th) based on a function +- footerStyle: Allows to add custom css to a footer cell (th) based on a function diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx index b87dd7ac2a..4d55213c1a 100644 --- a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx +++ b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx @@ -26,12 +26,14 @@ import { FilterWrapper } from './components/FilterWrapper' import { Photo, PostComment, + Summary, + aggregatedSummaryColumns, columns, expandColumns, groupedColumns, helper, } from './stories/columns' -import { data } from './stories/data' +import { data, summaryData } from './stories/data' import { Virtualizer } from './types' const meta: Meta> = { @@ -392,6 +394,38 @@ ColumnPinning.args = { columns: columns, height: 500, rows: data, + enableFooter: true, + stickyFooter: true, +} + +export const ColumnPinningWithFooter: StoryFn> = ( + args, +) => { + const { columnPinState } = args + return ( + <> + + {JSON.stringify(columnPinState, null, 2)} + + + + ) +} + +ColumnPinningWithFooter.args = { + columnPinState: { + left: [aggregatedSummaryColumns.at(0).id], + }, + scrollbarHorizontal: true, + stickyHeader: true, + width: 700, + columns: aggregatedSummaryColumns, + height: 500, + rows: summaryData, + enableFooter: true, + stickyFooter: true, + headerClass: () => 'header-class', + footerClass: () => 'footer-class', } export const ColumnOrdering: StoryFn> = (args) => { diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.tsx index 52e1b2c15a..9c1d89accb 100644 --- a/packages/eds-data-grid-react/src/EdsDataGrid.tsx +++ b/packages/eds-data-grid-react/src/EdsDataGrid.tsx @@ -32,6 +32,7 @@ import styled from 'styled-components' import { TableProvider } from './EdsDataGridContext' import { EdsDataGridProps } from './EdsDataGridProps' import { TableHeaderRow } from './components/TableHeaderRow' +import { TableFooterRow } from './components/TableFooterRow' import { TableRow } from './components/TableRow' import { addPxSuffixIfInputHasNoPrefix, @@ -54,6 +55,7 @@ export function EdsDataGrid({ enablePagination, enableSorting, stickyHeader, + stickyFooter, onSelectRow, onRowSelectionChange, caption, @@ -69,6 +71,8 @@ export function EdsDataGrid({ rowStyle, headerClass, headerStyle, + footerClass, + footerStyle, externalPaginator, onSortingChange, manualSorting, @@ -88,6 +92,7 @@ export function EdsDataGrid({ defaultColumn, onRowClick, onCellClick, + enableFooter, }: EdsDataGridProps) { logDevelopmentWarningOfPropUse({ virtualHeight: { @@ -365,6 +370,7 @@ export function EdsDataGrid({ // These classes are primarily used to allow for feature-detection in the test-suite const classList = { 'sticky-header': !!stickyHeader, + 'sticky-footer': !!stickyFooter, virtual: !!enableVirtual, paging: !!enablePagination, } @@ -377,10 +383,13 @@ export function EdsDataGrid({ rowStyle={rowStyle} headerClass={headerClass} headerStyle={headerStyle} + footerClass={footerClass} + footerStyle={footerStyle} table={table} enableSorting={!!enableSorting} enableColumnFiltering={!!enableColumnFiltering} stickyHeader={!!stickyHeader} + stickyFooter={!!stickyFooter} > ({ {virtualRows.map((virtualItem) => { const row = table.getRowModel().rows[virtualItem.index] - return ( ({ /> ))} + {enableFooter && ( + + {table.getFooterGroups().map((footerGroup) => ( + + ))} + + )} {externalPaginator ? externalPaginator diff --git a/packages/eds-data-grid-react/src/EdsDataGridContext.tsx b/packages/eds-data-grid-react/src/EdsDataGridContext.tsx index 303f828ca5..b42e0fb7ae 100644 --- a/packages/eds-data-grid-react/src/EdsDataGridContext.tsx +++ b/packages/eds-data-grid-react/src/EdsDataGridContext.tsx @@ -4,6 +4,7 @@ import { Column, Row, Table as TanStackTable } from '@tanstack/react-table' type Context = { enableSorting: boolean stickyHeader: boolean + stickyFooter: boolean enableColumnFiltering: boolean table: TanStackTable | null cellClass?: (row: Row, columnId: string) => string @@ -12,12 +13,15 @@ type Context = { rowStyle?: (row: Row) => CSSProperties headerClass?: (column: Column) => string headerStyle?: (column: Column) => CSSProperties + footerClass?: (column: Column) => string + footerStyle?: (column: Column) => CSSProperties } // eslint-disable-next-line @typescript-eslint/no-explicit-any export const EdsDataGridContext = createContext>({ enableSorting: false, stickyHeader: false, + stickyFooter: false, enableColumnFiltering: false, table: null, }) diff --git a/packages/eds-data-grid-react/src/EdsDataGridProps.ts b/packages/eds-data-grid-react/src/EdsDataGridProps.ts index ca320d304e..460453aa43 100644 --- a/packages/eds-data-grid-react/src/EdsDataGridProps.ts +++ b/packages/eds-data-grid-react/src/EdsDataGridProps.ts @@ -46,6 +46,11 @@ type BaseProps = { * @default false */ stickyHeader?: boolean + /** + * Make the table footer sticky + * @default false + */ + stickyFooter?: boolean /** * Element to display in Table.Caption * @default undefined @@ -95,6 +100,10 @@ type BaseProps = { * @link [API Docs](https://tanstack.com/table/v8/docs/api/core/table#defaultcolumn) */ defaultColumn?: Partial> + /** + * Optional props to show table footer + */ + enableFooter?: boolean } type RowSelectionProps = { @@ -179,6 +188,16 @@ type StyleProps = { * @param column */ headerStyle?: (column: Column) => CSSProperties + /** + * Function that can be used to set custom classes on a footer cell + * @param column + */ + footerClass?: (column: Column) => string + /** + * Function that can be used to set custom styles on a footer cell + * @param column + */ + footerStyle?: (column: Column) => CSSProperties } type FilterProps = { diff --git a/packages/eds-data-grid-react/src/components/Resizer.tsx b/packages/eds-data-grid-react/src/components/Resizer.tsx new file mode 100644 index 0000000000..706b866e8e --- /dev/null +++ b/packages/eds-data-grid-react/src/components/Resizer.tsx @@ -0,0 +1,33 @@ +import { ColumnResizeMode } from '@tanstack/react-table' +import styled from 'styled-components' + +type ResizeProps = { + $columnResizeMode: ColumnResizeMode | null | undefined + $isResizing: boolean +} + +export const ResizeInner = styled.div` + width: 2px; + opacity: 0; + height: 100%; +` + +export const Resizer = styled.div` + transform: ${(props) => + props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'}; + + ${ResizeInner} { + opacity: ${(props) => (props.$isResizing ? 1 : 0)}; + } + + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 5px; + cursor: col-resize; + user-select: none; + touch-action: none; + display: flex; + justify-content: flex-end; +` diff --git a/packages/eds-data-grid-react/src/components/TableCell.tsx b/packages/eds-data-grid-react/src/components/TableCell.tsx new file mode 100644 index 0000000000..984af3b7bf --- /dev/null +++ b/packages/eds-data-grid-react/src/components/TableCell.tsx @@ -0,0 +1,29 @@ +import { Table } from '@equinor/eds-core-react' +import { tokens } from '@equinor/eds-tokens' +import { ColumnPinningPosition } from '@tanstack/react-table' +import styled from 'styled-components' +import { ResizeInner } from './Resizer' + +export const TableCell = styled(Table.Cell)<{ + $sticky: boolean + $pinned: ColumnPinningPosition + $offset: number +}>` + font-weight: bold; + position: ${(p) => (p.$sticky || p.$pinned ? 'sticky' : 'relative')}; + top: 0; + ${(p) => { + if (p.$pinned) { + return `${p.$pinned}: ${p.$offset}px;` + } + return '' + }} + ${(p) => { + if (p.$sticky && p.$pinned) return 'z-index: 13' + if (p.$sticky || p.$pinned) return 'z-index: 12' + }}; + &:hover ${ResizeInner} { + background: ${tokens.colors.interactive.primary__hover.rgba}; + opacity: 1; + } +` diff --git a/packages/eds-data-grid-react/src/components/TableFooterCell.tsx b/packages/eds-data-grid-react/src/components/TableFooterCell.tsx new file mode 100644 index 0000000000..a2418b4d5d --- /dev/null +++ b/packages/eds-data-grid-react/src/components/TableFooterCell.tsx @@ -0,0 +1,85 @@ +import { + ColumnResizeMode, + flexRender, + Header, + Table as TanStackTable, +} from '@tanstack/react-table' +import { useTableContext } from '../EdsDataGridContext' +import { useMemo } from 'react' +import { SortIndicator } from './SortIndicator' +import { ResizeInner, Resizer } from './Resizer' +import { TableCell } from './TableCell' + +type Props = { + footer: Header + columnResizeMode: ColumnResizeMode | null | undefined + // eslint-disable-next-line react/no-unused-prop-types + deltaOffset: number | null + // eslint-disable-next-line react/no-unused-prop-types + table: TanStackTable +} + +export function TableFooterCell({ footer, columnResizeMode }: Props) { + const ctx = useTableContext() + const table = ctx.table + const pinned = footer.column.getIsPinned() + const offset = useMemo(() => { + if (!pinned) { + return null + } + return pinned === 'left' + ? footer.getStart() + : table.getTotalSize() - footer.getStart() - footer.getSize() + }, [pinned, footer, table]) + return footer.isPlaceholder ? ( + + ) : ( + + <> +
      + + {flexRender(footer.column.columnDef.footer, footer.getContext())} + +
      + {!footer.column.columnDef.meta?.customFilterInput && ( + + )} + + {columnResizeMode && ( + e.stopPropagation()} + onMouseDown={footer.getResizeHandler()} + onTouchStart={footer.getResizeHandler()} + $isResizing={footer.column.getIsResizing()} + $columnResizeMode={columnResizeMode} + className={'resize-handle'} + data-testid={'resize-handle'} + > + + + )} +
      + ) +} diff --git a/packages/eds-data-grid-react/src/components/TableFooterRow.tsx b/packages/eds-data-grid-react/src/components/TableFooterRow.tsx new file mode 100644 index 0000000000..9ffdfabd9a --- /dev/null +++ b/packages/eds-data-grid-react/src/components/TableFooterRow.tsx @@ -0,0 +1,35 @@ +import { Table } from '@equinor/eds-core-react' +import { + ColumnResizeMode, + HeaderGroup, + Table as TanStackTable, +} from '@tanstack/react-table' +import { TableFooterCell } from './TableFooterCell' + +type Props = { + footerGroup: HeaderGroup + columnResizeMode?: ColumnResizeMode | null + deltaOffset: number | null + table: TanStackTable +} + +export function TableFooterRow({ + footerGroup, + columnResizeMode, + deltaOffset, + table, +}: Props) { + return ( + + {footerGroup.headers.map((footer) => ( + + ))} + + ) +} diff --git a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx index e264fd3464..aa3b1a4e24 100644 --- a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx +++ b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx @@ -1,18 +1,16 @@ import { - ColumnPinningPosition, ColumnResizeMode, flexRender, Header, SortDirection, Table as TanStackTable, } from '@tanstack/react-table' -import { Table } from '@equinor/eds-core-react' import { useTableContext } from '../EdsDataGridContext' -import styled from 'styled-components' -import { tokens } from '@equinor/eds-tokens' import { useMemo } from 'react' import { FilterWrapper } from './FilterWrapper' import { SortIndicator } from './SortIndicator' +import { ResizeInner, Resizer } from './Resizer' +import { TableCell } from './TableCell' type Props = { header: Header @@ -32,61 +30,6 @@ const getSortLabel = ( return 'none' } -type ResizeProps = { - $columnResizeMode: ColumnResizeMode | null | undefined - $isResizing: boolean -} - -const ResizeInner = styled.div` - width: 2px; - opacity: 0; - height: 100%; -` - -const Resizer = styled.div` - transform: ${(props) => - props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'}; - - ${ResizeInner} { - opacity: ${(props) => (props.$isResizing ? 1 : 0)}; - } - - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 5px; - cursor: col-resize; - user-select: none; - touch-action: none; - display: flex; - justify-content: flex-end; -` - -const Cell = styled(Table.Cell)<{ - $sticky: boolean - $pinned: ColumnPinningPosition - $offset: number -}>` - font-weight: bold; - position: ${(p) => (p.$sticky || p.$pinned ? 'sticky' : 'relative')}; - top: 0; - ${(p) => { - if (p.$pinned) { - return `${p.$pinned}: ${p.$offset}px;` - } - return '' - }} - ${(p) => { - if (p.$sticky && p.$pinned) return 'z-index: 13' - if (p.$sticky || p.$pinned) return 'z-index: 12' - }}; - &:hover ${ResizeInner} { - background: ${tokens.colors.interactive.primary__hover.rgba}; - opacity: 1; - } -` - export function TableHeaderCell({ header, columnResizeMode }: Props) { const ctx = useTableContext() const table = ctx.table @@ -100,7 +43,7 @@ export function TableHeaderCell({ header, columnResizeMode }: Props) { : table.getTotalSize() - header.getStart() - header.getSize() }, [pinned, header, table]) return header.isPlaceholder ? ( - ({ header, columnResizeMode }: Props) { aria-hidden={true} /> ) : ( - ({ header, columnResizeMode }: Props) { )} - +
      ) } diff --git a/packages/eds-data-grid-react/src/stories/columns.tsx b/packages/eds-data-grid-react/src/stories/columns.tsx index f7ad70fccb..2059acdddd 100644 --- a/packages/eds-data-grid-react/src/stories/columns.tsx +++ b/packages/eds-data-grid-react/src/stories/columns.tsx @@ -13,12 +13,64 @@ export type Photo = { timestamp?: Date } -export const helper = createColumnHelper() +export type Summary = { + id: string + country: string + price: number + tax: number + discount: number +} const Link = styled.a` color: ${tokens.colors.interactive.primary__resting.rgba}; ` +export const summaryHelper = createColumnHelper() +export const aggregatedSummaryColumns: Array> = [ + summaryHelper.accessor('country', { + header: 'Country', + footer: () => 'Total', + size: 150, + id: 'country', + }), + summaryHelper.accessor('price', { + header: 'Price', + footer: (props) => { + const price = props.table + .getCoreRowModel() + .rows.reduce((acc, curr) => (acc = acc + curr?.original?.price || 0), 0) + return {price.toFixed(2)} + }, + }), + summaryHelper.accessor('tax', { + header: 'Tax', + footer: (props) => { + const price = props.table + .getCoreRowModel() + .rows.reduce((acc, curr) => (acc = acc + curr?.original?.tax || 0), 0) + return {price.toFixed(2)} + }, + size: 250, + id: 'tax', + }), + summaryHelper.accessor('discount', { + header: 'Discount', + footer: (props) => { + const price = props.table + .getCoreRowModel() + .rows.reduce( + (acc, curr) => (acc = acc + curr?.original?.discount || 0), + 0, + ) + return {price.toFixed(2)} + }, + size: 250, + id: 'discount', + }), +] + +export const helper = createColumnHelper() + export const columns: Array> = [ helper.accessor('id', { header: () => ID, diff --git a/packages/eds-data-grid-react/src/stories/data.ts b/packages/eds-data-grid-react/src/stories/data.ts index df51704403..7e3e74c0d6 100644 --- a/packages/eds-data-grid-react/src/stories/data.ts +++ b/packages/eds-data-grid-react/src/stories/data.ts @@ -1,6 +1,6 @@ // Subset of data from https://jsonplaceholder.typicode.com/photos -import { Photo } from './columns' +import { Photo, Summary } from './columns' const getRandomDate = () => { const date = new Date() @@ -239,3 +239,489 @@ export const data: Array = [ url: 'https://via.placeholder.com/600/372c93', }, ] + +export const summaryData: Array = [ + { + id: 'f29f0487-2266-40d1-947e-0a0926ad2631', + country: 'UK', + price: 101.7, + tax: 9.78, + discount: 28.37, + }, + { + id: 'e51d8bca-afac-48ee-8eb9-72b754abf4a1', + country: 'Japan', + price: 753.76, + tax: 1.62, + discount: 9.67, + }, + { + id: 'ea59a527-e809-416c-acb4-2d576c8239aa', + country: 'Germany', + price: 230.66, + tax: 24.89, + discount: 13.61, + }, + { + id: 'ba888c70-bd85-46e3-b69a-9912c287d6d5', + country: 'Russia', + price: 499.04, + tax: 17.78, + discount: 33.98, + }, + { + id: '542d51be-c32e-41d3-bca0-d3c5daca6673', + country: 'Mexico', + price: 854.58, + tax: 9.66, + discount: 3.19, + }, + { + id: '16a611e9-0981-4469-a1ce-f3915487680f', + country: 'France', + price: 126.41, + tax: 19.56, + discount: 33.5, + }, + { + id: '5a97b0f1-3053-40eb-b235-fbc20707ca5b', + country: 'USA', + price: 465.96, + tax: 19.14, + discount: 17.76, + }, + { + id: '2afbb690-ee72-4b34-9857-b16c8e160f27', + country: 'Norway', + price: 91.85, + tax: 19.4, + discount: 33.42, + }, + { + id: '796b6291-8db9-43ba-863f-91193af94eec', + country: 'Germany', + price: 673.11, + tax: 20.28, + discount: 45.77, + }, + { + id: '57a827ba-125b-49e3-846e-e768edefe1e7', + country: 'USA', + price: 822.44, + tax: 21.1, + discount: 48.9, + }, + { + id: 'f6a84b49-ec30-49c1-82da-4589d62ebe0e', + country: 'Netherlands', + price: 771.57, + tax: 19.92, + discount: 33.5, + }, + { + id: '1b958cf9-0aca-4bcb-afe2-55188b753cf2', + country: 'Canada', + price: 288.32, + tax: 18.23, + discount: 36.97, + }, + { + id: 'ece0a9a4-d066-4400-9d3a-3136454df4e0', + country: 'South Korea', + price: 911.82, + tax: 22.36, + discount: 26.78, + }, + { + id: '704c31b2-b72f-4873-ba29-bde26e0b7584', + country: 'Spain', + price: 348.37, + tax: 4.28, + discount: 23.47, + }, + { + id: 'f2ddcff3-f0cf-4f30-abd2-a78ed0b510e4', + country: 'Russia', + price: 781.64, + tax: 12.1, + discount: 21.56, + }, + { + id: '07ad8172-2345-484f-bed6-fcbc8a013ae9', + country: 'Norway', + price: 329.0, + tax: 17.0, + discount: 49.56, + }, + { + id: '8793db11-695d-45db-8859-9941a6d4faa9', + country: 'Netherlands', + price: 583.69, + tax: 0.16, + discount: 49.4, + }, + { + id: '36ec6234-0a25-4b1e-99eb-9c4dbf961264', + country: 'Germany', + price: 151.66, + tax: 24.4, + discount: 3.45, + }, + { + id: '75950ad7-c19f-48a5-8bb8-28e189fe1d5c', + country: 'Italy', + price: 389.55, + tax: 1.0, + discount: 17.48, + }, + { + id: '4cfb6bb1-cd34-4876-b665-ce2aac85a583', + country: 'Mexico', + price: 641.99, + tax: 18.29, + discount: 22.03, + }, + { + id: '0ad452e0-a49d-4f5f-b765-d9466749066a', + country: 'Australia', + price: 27.97, + tax: 23.87, + discount: 1.96, + }, + { + id: '893e0eee-1166-4797-860f-a493026a35e9', + country: 'Russia', + price: 699.92, + tax: 17.37, + discount: 2.75, + }, + { + id: '9232bd68-3e68-4c6d-8d6a-53e8c7cb7abc', + country: 'China', + price: 514.05, + tax: 2.78, + discount: 2.22, + }, + { + id: '32375c1d-fc8a-43f7-9b0f-40ebb05f69c3', + country: 'UK', + price: 388.83, + tax: 14.91, + discount: 26.45, + }, + { + id: 'bda46595-aa88-483d-995e-0fa92e46e698', + country: 'Canada', + price: 830.41, + tax: 21.29, + discount: 15.46, + }, + { + id: '2f1f0ace-fdab-4691-b130-2396728b337e', + country: 'Russia', + price: 225.25, + tax: 22.8, + discount: 10.36, + }, + { + id: '9f2202a1-b016-43e5-a132-cc47d4662f4f', + country: 'USA', + price: 515.03, + tax: 21.51, + discount: 0.31, + }, + { + id: '2f8a7eab-b64a-449e-8663-20d3ac19652e', + country: 'South Korea', + price: 313.73, + tax: 1.87, + discount: 40.86, + }, + { + id: 'a138596f-475f-49ed-a587-fa5b357dea98', + country: 'Italy', + price: 176.2, + tax: 21.84, + discount: 14.48, + }, + { + id: '9e57f377-3b62-42c7-9453-ae00bf8b5ff7', + country: 'Australia', + price: 829.53, + tax: 13.69, + discount: 8.74, + }, + { + id: '307896aa-c8ec-43d1-9e8f-971820dab26c', + country: 'India', + price: 254.35, + tax: 1.98, + discount: 37.25, + }, + { + id: 'c60441d4-9a4f-4f37-875c-0c1ea04ff95d', + country: 'South Africa', + price: 273.98, + tax: 1.08, + discount: 1.72, + }, + { + id: '29c30075-2dd3-4bc7-a127-cd4065a8d9e6', + country: 'South Korea', + price: 26.92, + tax: 13.98, + discount: 29.95, + }, + { + id: 'dd4d5455-2e0f-4e32-b48d-06f20b91a020', + country: 'Netherlands', + price: 202.29, + tax: 20.34, + discount: 48.25, + }, + { + id: 'b1b36b59-9d5e-4c04-971c-28a3e9b20ecb', + country: 'Japan', + price: 370.15, + tax: 12.5, + discount: 1.73, + }, + { + id: '18dd3a96-df57-4b07-bb93-87eb5fbedc32', + country: 'France', + price: 413.47, + tax: 7.98, + discount: 26.65, + }, + { + id: 'add8b930-fc3e-464b-82c8-23b885b3107e', + country: 'India', + price: 844.84, + tax: 12.58, + discount: 9.32, + }, + { + id: 'e0d5a7f3-c9e3-4e33-b96c-994a8c9c4268', + country: 'Mexico', + price: 196.99, + tax: 1.93, + discount: 33.64, + }, + { + id: '41a4ec3f-6896-4f42-937b-423adbf33fe0', + country: 'Brazil', + price: 23.38, + tax: 1.12, + discount: 25.8, + }, + { + id: '2f3b21ab-0f53-4513-b93a-49a51b808b6d', + country: 'Spain', + price: 348.83, + tax: 5.15, + discount: 22.16, + }, + { + id: 'bbf2387e-35ab-4bc7-9262-8d3c6c50706d', + country: 'New Zealand', + price: 185.01, + tax: 8.92, + discount: 37.17, + }, + { + id: 'd4552e2a-3a96-4084-9304-67c0a18889ff', + country: 'Italy', + price: 43.5, + tax: 22.71, + discount: 13.82, + }, + { + id: '197f9bb7-abb4-4806-b40b-7386660fcbb4', + country: 'India', + price: 655.65, + tax: 18.18, + discount: 40.35, + }, + { + id: 'caee1992-d625-43cf-88db-f8d3e74f96b1', + country: 'UK', + price: 265.54, + tax: 2.59, + discount: 23.67, + }, + { + id: '0db80996-cd3e-46f1-9a14-295214f8a597', + country: 'China', + price: 321.94, + tax: 7.94, + discount: 15.19, + }, + { + id: '9b1a7357-573a-439b-99a1-f96d0a75dbf1', + country: 'Canada', + price: 649.0, + tax: 14.16, + discount: 38.62, + }, + { + id: '17fbbab6-6224-487f-a120-2901fd4b1076', + country: 'South Africa', + price: 201.31, + tax: 4.87, + discount: 17.03, + }, + { + id: '2d38b07a-1ad6-4d59-9cf4-8df7261154f2', + country: 'China', + price: 128.56, + tax: 2.65, + discount: 6.94, + }, + { + id: '2e5a5122-567e-45c7-8e93-c2201d3de35e', + country: 'Brazil', + price: 803.6, + tax: 11.86, + discount: 24.17, + }, + { + id: '01936e9a-5c3e-4066-95d4-5825e7be1bc5', + country: 'France', + price: 402.83, + tax: 16.93, + discount: 40.58, + }, + { + id: 'b389ae22-ae21-4ff5-9d90-5726b9c1038f', + country: 'Norway', + price: 218.74, + tax: 13.06, + discount: 34.36, + }, + { + id: '99d9f8ec-7c8f-4f8d-83f3-4aaba3dd5fe1', + country: 'Russia', + price: 292.01, + tax: 12.67, + discount: 3.29, + }, + { + id: '9da27a8e-3258-4ec5-8f19-34e79d755674', + country: 'New Zealand', + price: 887.08, + tax: 9.17, + discount: 47.83, + }, + { + id: '63b220b8-80e0-46d5-8b73-9cb7a7d201c6', + country: 'India', + price: 530.98, + tax: 2.13, + discount: 16.66, + }, + { + id: 'eead89cc-5eeb-44d6-8e89-978d637f4984', + country: 'Italy', + price: 563.91, + tax: 19.78, + discount: 12.53, + }, + { + id: '116bde9b-35b3-4d07-90b4-7db716d4b36c', + country: 'Norway', + price: 147.97, + tax: 12.81, + discount: 26.16, + }, + { + id: 'd5e44dff-67c1-4ee2-94e2-15861e5ea554', + country: 'Brazil', + price: 953.99, + tax: 0.92, + discount: 5.47, + }, + { + id: 'be5284e3-09c1-4e38-9e3b-b5cc49b3be63', + country: 'France', + price: 184.66, + tax: 17.06, + discount: 8.57, + }, + { + id: 'ad666b5e-93f3-4960-89a7-8f4a9c931d8d', + country: 'Mexico', + price: 76.76, + tax: 22.17, + discount: 6.22, + }, + { + id: '08a4c1f3-045d-42ca-8ce4-bc22dd5f3839', + country: 'USA', + price: 874.53, + tax: 11.99, + discount: 47.92, + }, + { + id: 'b407f110-0ca5-48ef-957e-c10dbd77fabb', + country: 'South Korea', + price: 396.3, + tax: 11.63, + discount: 20.15, + }, + { + id: '95ffae7c-4978-4872-a621-88d07c685c6a', + country: 'China', + price: 457.85, + tax: 1.03, + discount: 11.91, + }, + { + id: '9d5e1f77-e2bc-4e70-9f00-4ab2692a946d', + country: 'Japan', + price: 822.96, + tax: 11.2, + discount: 17.53, + }, + { + id: '8473cde8-0b8a-44e1-93b3-b7e0e62ef56c', + country: 'Canada', + price: 601.38, + tax: 17.67, + discount: 26.5, + }, + { + id: 'a58b18e4-c8ca-4752-9b58-daf9ef5cd7f4', + country: 'South Africa', + price: 207.02, + tax: 22.85, + discount: 14.08, + }, + { + id: 'c6d60e0c-8ef6-4b9d-a4d2-b22a4c6d06c7', + country: 'Spain', + price: 141.1, + tax: 16.67, + discount: 47.06, + }, + { + id: 'bc1b6117-1c05-40ec-9b83-7e50c3bfb95c', + country: 'Mexico', + price: 255.49, + tax: 0.6, + discount: 36.29, + }, + { + id: '87b6f255-fb7e-4e02-933d-9f4debb6fa69', + country: 'Japan', + price: 77.17, + tax: 2.58, + discount: 8.95, + }, + { + id: '4d21ad85-eab6-404f-bc99-576a46560a4b', + country: 'UK', + price: 647.35, + tax: 1.69, + discount: 7.51, + }, +] diff --git a/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx b/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx index 02a1b65894..c8a61956e7 100644 --- a/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx +++ b/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx @@ -136,6 +136,37 @@ describe('EdsDataGrid', () => { expect(window.getComputedStyle(column).position).toBe('sticky') }) }) + + /** + * Footer + */ + it('should not render footer by default when enableFooter is not provided', () => { + render() + const foot = screen.queryByTestId('eds-grid-footer') + expect(foot).toBeNull() // Assert Footer is not available in the document + }) + + it('should render footer when `enableFooter` is provided', () => { + render() + const foot = screen.getByTestId('eds-grid-footer') + expect(foot).toBeTruthy() // Assert Footer is not available in the document + const thElements = within(foot).getAllByRole('columnheader') + thElements.forEach((th) => { + expect(window.getComputedStyle(th).position).not.toBe('sticky') + }) + }) + + it('should render sticky footer when `enableFooter` & `stickyFooter` provided', () => { + render( + , + ) + const foot = screen.getByTestId('eds-grid-footer') + expect(foot).toBeTruthy() // Assert Footer is not available in the document + const thElements = within(foot).getAllByRole('columnheader') + thElements.forEach((th) => { + expect(window.getComputedStyle(th).position).toBe('sticky') + }) + }) }) describe('Sorting', () => { @@ -390,10 +421,13 @@ describe('EdsDataGrid', () => { const cellStyle = () => ({ backgroundColor: 'red' }) const rowStyle = () => ({ backgroundColor: 'blue' }) const headerStyle = () => ({ backgroundColor: 'green' }) + const footerStyle = () => ({ backgroundColor: 'yellow', color: 'black' }) render( { expect(screen.getAllByRole('columnheader')[0].style.backgroundColor).toBe( 'green', ) + const foot = screen.getByTestId('eds-grid-footer') + expect(foot).toBeTruthy() // Assert Footer is available in the document + const thElements = within(foot).getAllByRole('columnheader') + thElements.forEach((th) => { + expect(th.style.backgroundColor).toBe('yellow') + expect(th.style.color).toBe('black') + }) }) - it('should apply classes to the table', () => { + it('should apply classes to the table + footer enabled', () => { const cellClass = () => 'cell-class' const rowClass = () => 'row-class' const headerClass = () => 'header-class' + const footerClass = () => 'footer-class' render( , @@ -429,6 +473,12 @@ describe('EdsDataGrid', () => { expect(firstCell.classList.contains('cell-class')).toBeTruthy() const firstHeader = screen.getAllByRole('columnheader')[0] expect(firstHeader.classList.contains('header-class')).toBeTruthy() + const foot = screen.getByTestId('eds-grid-footer') + expect(foot).toBeTruthy() // Assert Footer is available in the document + const thElements = within(foot).getAllByRole('columnheader') + thElements.forEach((th) => { + expect(th.classList.contains('footer-class')).toBeTruthy() + }) }) }) }) diff --git a/packages/eds-data-grid-react/src/tests/Filter.test.tsx b/packages/eds-data-grid-react/src/tests/Filter.test.tsx index 4f7e0ed435..79c4cf864d 100644 --- a/packages/eds-data-grid-react/src/tests/Filter.test.tsx +++ b/packages/eds-data-grid-react/src/tests/Filter.test.tsx @@ -62,6 +62,7 @@ describe('Filter', () => { @@ -75,6 +76,7 @@ describe('Filter', () => { @@ -89,6 +91,7 @@ describe('Filter', () => { @@ -106,6 +109,7 @@ describe('Filter', () => { @@ -136,6 +140,7 @@ describe('Filter', () => { @@ -158,6 +163,7 @@ describe('Filter', () => { @@ -186,6 +192,7 @@ describe('Filter', () => { @@ -218,6 +225,7 @@ describe('Filter', () => { @@ -225,7 +233,7 @@ describe('Filter', () => { , ) openPopover(baseElement) - // eslint complains about unneccessary cast, but HTMLElement != HTMLInputElement + // eslint complains about unnecessary cast, but HTMLElement != HTMLInputElement // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const inputs = within(baseElement).getAllByRole( 'spinbutton', @@ -256,6 +264,7 @@ describe('Filter', () => {