diff --git a/component-overview/examples/tables/Table-advanced.jsx b/component-overview/examples/tables/Table-advanced.jsx deleted file mode 100644 index cbcfa0b514..0000000000 --- a/component-overview/examples/tables/Table-advanced.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import Table from '@sb1/ffe-tables-react'; -import { TertiaryButton } from '@sb1/ffe-buttons-react'; -import { formatNumber } from '@sb1/ffe-formatters'; - -() => { - - const Button = ({ children }) => ( - // stopPropagation hindrer at raden ekspanderer/kollapser når vi trykker på knappen - e.stopPropagation()}> - {children} - - ); - - const data = [ - { - id: 0, - name: 'Ola Normann', - email: 'ola@normann.no', - address: 'Gateveien 2', - age: 23, - networth: formatNumber(12693005.93, { decimals: 2 }), - button: , - }, - { - id: 1, - name: 'Sivert Svensk', - email: 'sivert@svenska.se', - age: 45, - networth: formatNumber(9005.93, { decimals: 2 }), - button: , - }, - { - id: 2, - name: 'Daniel Dansk', - email: 'daniel@dansk.dk', - address: , - age: 39, - networth: formatNumber(8693005.93, { decimals: 2 }), - button: , - }, - { - id: 3, - name: 'Lille Ole', - email: 'lilleole@gmail.com', - address: , - age: 9, - networth: '23,12', - button: , - }, - ]; - const currencyCompare = (a, b) => a.replace(/[\s.,]+/g, '') - b.replace(/[\s.,]+/g, ''); - - const columns = [ - { key: 'name', header: 'Navn', footer: 'Gjennomsnitt' }, - { - key: 'email', - header: 'E-post', - hideOnTablet: true, - hideOnMobile: true, - }, - { - key: 'age', - header: 'Alder', - footer: '29', - alignRight: true, - hideOnMobile: true, - }, - { - key: 'networth', - header: 'Formue', - footer: '5 348 760,23', - alignRight: true, - compare: currencyCompare, - }, - { key: 'button', header: 'Poke', notSortable: true }, - ]; - - // Rader uten address vil ikke kunne ekspanderes fordi funksjonen returnerer falsy - const expandedContentMapper = row => - row.address && Adresse: {row.address}; - - // function is called when table sorting changes. - const onSort = ({ sortBy, descending, tableData }) => {}; - - return ( - - ); -} diff --git a/component-overview/examples/tables/Table-customRender.jsx b/component-overview/examples/tables/Table-customRender.jsx deleted file mode 100644 index 37c9977c5c..0000000000 --- a/component-overview/examples/tables/Table-customRender.jsx +++ /dev/null @@ -1,292 +0,0 @@ -import Table from '@sb1/ffe-tables-react'; -import { TertiaryButton } from '@sb1/ffe-buttons-react'; -import { Icon } from '@sb1/ffe-icons-react'; -import { formatNumber } from '@sb1/ffe-formatters'; - -() => { - - const checkIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjQiPjxwYXRoIGQ9Im0zODItMzM5LjM4NCAzNDUuNTM5LTM0NS41MzhxOC45MjMtOC45MjMgMjEuMTkyLTkuMTE2IDEyLjI2OS0uMTkyIDIxLjU3NiA5LjExNiA5LjMwOCA5LjMwNyA5LjMwOCAyMS4zODQgMCAxMi4wNzYtOS4zMDggMjEuMzg0bC0zNjMgMzYyLjk5OVEzOTYuNDYxLTI2OC4zMDkgMzgyLTI2OC4zMDlxLTE0LjQ2MSAwLTI1LjMwNy0xMC44NDZsLTE2Ny0xNjYuOTk5cS04LjkyMy04LjkyMy04LjgwOC0yMS4xOTIuMTE2LTEyLjI2OSA5LjQyMy0yMS41NzYgOS4zMDgtOS4zMDggMjEuMzg0LTkuMzA4IDEyLjA3NyAwIDIxLjM4NCA5LjMwOEwzODItMzM5LjM4NFoiLz48L3N2Zz4=" - - const closeIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjQiPjxwYXRoIGQ9Ik00ODAtNDM3Ljg0NyAyNzcuMDc2LTIzNC45MjRxLTguMzA3IDguMzA4LTIwLjg4NCA4LjUtMTIuNTc2LjE5My0yMS4yNjgtOC41LTguNjkzLTguNjkyLTguNjkzLTIxLjA3NnQ4LjY5My0yMS4wNzZMNDM3Ljg0Ny00ODAgMjM0LjkyNC02ODIuOTI0cS04LjMwOC04LjMwNy04LjUtMjAuODg0LS4xOTMtMTIuNTc2IDguNS0yMS4yNjggOC42OTItOC42OTMgMjEuMDc2LTguNjkzdDIxLjA3NiA4LjY5M0w0ODAtNTIyLjE1M2wyMDIuOTI0LTIwMi45MjNxOC4zMDctOC4zMDggMjAuODg0LTguNSAxMi41NzYtLjE5MyAyMS4yNjggOC41IDguNjkzIDguNjkyIDguNjkzIDIxLjA3NnQtOC42OTMgMjEuMDc2TDUyMi4xNTMtNDgwbDIwMi45MjMgMjAyLjkyNHE4LjMwOCA4LjMwNyA4LjUgMjAuODg0LjE5MyAxMi41NzYtOC41IDIxLjI2OC04LjY5MiA4LjY5My0yMS4wNzYgOC42OTN0LTIxLjA3Ni04LjY5M0w0ODAtNDM3Ljg0N1oiLz48L3N2Zz4=" - - - const generateCheckbox = value => { - return ( -
- {value ? ( - - ) : ( - - )} -
- ); - }; - - const Button = ({ children }) => ( - // stopPropagation hindrer at raden ekspanderer/kollapser når vi trykker på knappen - e.stopPropagation()}> - {children} - - ); - - const data = [ - { - id: 0, - name: 'Ola Normann', - email: 'ola@normann.no', - address: 'Gateveien 2', - age: 23, - networth: 12693005.93, - button: , - }, - { - id: 1, - name: 'Sivert Svensk', - email: 'sivert@svenska.se', - age: 45, - networth: 9005.93, - button: , - }, - { - id: 2, - name: 'Daniel Dansk', - email: 'daniel@dansk.dk', - address: , - age: 39, - networth: 8693005.93, - button: , - }, - { - id: 3, - name: 'Lille Ole', - email: 'lilleole@gmail.com', - address: , - age: 9, - networth: 23.12, - button: , - }, - ].map(d => ({ - syntetic: d.age > 18 && d.networth > 10000, - ...d, - })); - - const currencyCompare = (a, b) => a - b; - - const ageSum = data - .map(e => e.age) - .reduce((total, num) => total + num); - - const networthSum = data - .map(e => e.networth) - .reduce((total, num) => total + num); - - const columnsAdvanced = [ - { key: 'name', header: 'Navn', footer: 'Gjennomsnitt' }, - { - key: 'email', - header: 'E-post', - hideOnTablet: true, - hideOnMobile: true, - }, - { - key: 'age', - header: 'Alder', - footer: ageSum / data.length, - alignRight: true, - cellRender: (value, col, props) => { - return ( - <> - {value} {value > 18 ? ' (voksen)' : ''} - - ); - }, - }, - { - key: 'syntetic', - header: 'Syntetisk felt', - alignRight: true, - notSortable: true, - cellRender: (value, col, props) => { - return ( - <> - {props.cells.age > 18 && - props.cells.networth > 10000 - ? `voksen, formue > ${formatNumber(10000, { decimals: 0 })}` - : ''} - - ); - }, - }, - { - key: 'syntetic', - header: 'Sparing', - alignRight: true, - columnHeaderRender: ( - value, - dataWindow, - spanProps, - thProps, - columns, - ) => { - const dataWindowSum = dataWindow - .map(e => (e.syntetic ? 1 : 0)) - .reduce((total, num) => { - return total + num; - }); - return ( - - ); - }, - cellRender: (value, col, props) => { - return ( - <> - {value - ? generateCheckbox(value) - : generateCheckbox(false)} - - ); - }, - }, - { - key: 'networth', - header: 'Formue', - footer: 5348760.23, - alignRight: true, - compare: currencyCompare, - cellRender: (value, col, props) => { - return ( - <> - {formatNumber(value, { decimals: 2 })} - - ); - }, - columnFooterRender: ( - value, - dataWindow, - tdPorps, - spanProps, - columns, - index, - ) => { - return ( - - ); - }, - }, - { key: 'button', header: 'Poke', notSortable: true }, - ]; - - // Rader uten address vil ikke kunne ekspanderes fordi funksjonen returnerer falsy - const expandedContentMapper = row => - row.address && Adresse: {row.address}; - - // function is called when table sorting changes. - const onSort = ({ sortBy, descending, tableData }) => {}; - - return ( -
- - {value} ({dataWindowSum}) - - - - {formatNumber(networthSum / data.length, { decimals: 2 })} - -
{ - return ( - <> - - - - - - - ); - }} - footerRender={(trprops, columns) => { - return ( - <> - - - - - - - - - - - - ); - }} - rowRender={(trprops, props, index) => { - const oddEvenClassName = index % 2 === 1 ? 'is-odd' : 'is-even'; - return ( - { - console.log('rowRenderClick!', index); - trprops.onClick && trprops.onClick(event); - }} - title={props.cells.name} - > - {trprops.children} - - ); - }} - /> - ); -} diff --git a/component-overview/examples/tables/Table-expandable.jsx b/component-overview/examples/tables/Table-expandable.jsx new file mode 100644 index 0000000000..1a76c141ae --- /dev/null +++ b/component-overview/examples/tables/Table-expandable.jsx @@ -0,0 +1,53 @@ +import { + Table, + TableHead, + TableBody, + TableHeaderCell, + TableRow, + TableDataCell, + TableCaption, + TableRowExpandable, +} from '@sb1/ffe-tables-react'; + +() => { + const data = [ + { + name: 'Ola Normann', + email: 'ola@normann.no', + expand: 'Info: mer spennende info om Ola', + }, + { + name: 'Sivert Svensk', + email: 'sivert@svenska.se', + expand: 'Info: mer spennende info om Sivert', + }, + { + name: 'Daniel Dansk', + email: 'daniel@dansk.dk', + expand: 'Info: mer spennende info om Daniel', + }, + ]; + + return ( +
- Personlig - - - Overskrift -
- - Total - - - - - - - - - - - {formatNumber(networthSum, { decimals: 2 })} - - - - - -
+ Tabel utvidbare rader + + + Navn + Epost + + + + {data.map(it => ( + + {it.name} + {it.email} + + ))} + +
+ ); +}; diff --git a/component-overview/examples/tables/Table-expandedContentMapper.jsx b/component-overview/examples/tables/Table-expandedContentMapper.jsx deleted file mode 100644 index d78da6003e..0000000000 --- a/component-overview/examples/tables/Table-expandedContentMapper.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import Table from '@sb1/ffe-tables-react'; - -() => { - const data = [ - { - id: 0, - name: 'Ola Normann', - email: 'ola@normann.no', - info: 'mer spennende info', - }, - { - id: 1, - name: 'Sivert Svensk', - email: 'sivert@svenska.se', - info: 'mer spennende info', - defaultExpanded: true, - }, - { - id: 2, - name: 'Daniel Dansk', - email: 'daniel@dansk.dk', - }, - { - id: 3, - name: 'Lille Ole', - email: 'lilleole@gmail.com', - info: 'mer spennende info', - }, - ]; - - const columns = [ - { key: 'name', header: 'Navn', footer: 'Gjennomsnitt' }, - { - key: 'email', - header: 'E-post', - hideOnTablet: true, - hideOnMobile: true, - }, - ]; - - // Rader uten address vil ikke kunne ekspanderes fordi funksjonen returnerer falsy - const expandedContentMapper = row => - row.info && Info: {row.info}; - - return ( - - ); -} diff --git a/component-overview/examples/tables/Table-expandedWithTable.jsx b/component-overview/examples/tables/Table-expandedWithTable.jsx deleted file mode 100644 index d350ff9374..0000000000 --- a/component-overview/examples/tables/Table-expandedWithTable.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import Table from '@sb1/ffe-tables-react'; -import { formatNumber } from '@sb1/ffe-formatters'; - -() => { - const data = [ - { - id: 0, - year: '2022', - sum: formatNumber(13545, { decimals: 2 }), - avdrag: formatNumber(6570.50, { decimals: 2 }), - renterGebyrer: formatNumber(6973, { decimals: 2}), - restgjeld: formatNumber(67812.72, { decimals: 2 }), - expand: true, - } - ]; - - const columns = [ - { key: 'year', header: 'År' }, - { key: 'sum', header: 'Å betale' }, - { key: 'avdrag', header: 'Avdrag' }, - { key: 'renterGebyrer', header: 'Rente + gebyrer' }, - { key: 'restgjeld', header: 'Restgjeld' }, - ]; - - const subColumns = [ - { key: 'month', header: 'Måned' }, - { key: 'sum', header: 'Å betale' }, - { key: 'avdrag', header: 'Avdrag' }, - { key: 'renterGebyrer', header: 'Rente + gebyrer' }, - ] - - const subData = [ - { - id: 0, - month: 'Juni', - sum: formatNumber(1935, { decimals: 2 }), - avdrag: formatNumber(6570.50, { decimals: 2 }), - renterGebyrer: formatNumber(6973, { decimals: 2 }), - }, - { - id: 1, - month: 'Juli', - sum: formatNumber(1935, { decimals: 2 }), - avdrag: formatNumber(6570.50, { decimals: 2 }), - renterGebyrer: formatNumber(6973, { decimals: 2 }), - }, - { - id: 2, - month: 'August', - sum: formatNumber(1935, { decimals: 2 }), - avdrag: formatNumber(6570.50, { decimals: 2 }), - renterGebyrer: formatNumber(6973, { decimals: 2 }), - } - ] - // Rader uten address vil ikke kunne ekspanderes fordi funksjonen returnerer falsy - const expandedContentMapper = row => - row.expand &&
; - - return ( - - ); -} diff --git a/component-overview/examples/tables/Table-sortable.jsx b/component-overview/examples/tables/Table-sortable.jsx new file mode 100644 index 0000000000..637831e561 --- /dev/null +++ b/component-overview/examples/tables/Table-sortable.jsx @@ -0,0 +1,122 @@ +import { + Table, + TableHead, + TableBody, + TableHeaderCell, + TableRow, + TableDataCell, + TableCaption, +} from '@sb1/ffe-tables-react'; +import { formatNumber } from '@sb1/ffe-formatters'; +import { TertiaryButton } from '@sb1/ffe-buttons-react'; + +() => { + const data = [ + { + name: 'Ola Normann', + email: 'ola@normann.no', + age: 23, + fortune: 12693005.93, + button: poke, + }, + { + name: 'Sivert Svensk', + email: 'sivert@svenska.se', + age: 45, + fortune: 9005.93, + button: poke, + }, + { + name: 'Daniel Dansk', + email: 'daniel@dansk.dk', + age: 39, + fortune: 8693005.93, + button: poke, + }, + { + name: 'Lille Ole', + email: 'lilleole@gmail.com', + age: 9, + fortune: 23.12, + button: poke, + }, + ]; + + const compareStringAsc = field => (itemA, itemB) => + itemA[field].localeCompare(itemB[field]); + const compareStringDesc = field => (itemA, itemB) => + itemB[field].localeCompare(itemA[field]); + const compareNumberAsc = field => (itemA, itemB) => + itemA[field] - itemB[field]; + const compareNumberDesc = field => (itemA, itemB) => + itemA[field] - itemB[field]; + + const sortFunctions = { + name: { + ascending: compareStringAsc('name'), + descending: compareStringDesc('name'), + }, + email: { + ascending: compareStringAsc('email'), + descending: compareStringDesc('email'), + }, + age: { + ascending: compareNumberAsc('age'), + descending: compareNumberDesc('age'), + }, + fortune: { + ascending: compareNumberAsc('fortune'), + descending: compareNumberDesc('fortune'), + }, + }; + + return ( +
+ {({ activeSortKey, activeSortOrder }) => { + const sortFunc = activeSortKey + ? sortFunctions[activeSortKey][activeSortOrder] + : null; + + const sortedData = sortFunc ? data.toSorted(sortFunc) : data; + + return ( + <> + Masse spennende data + + + + Navn + + + E-post + + + Alder + + + Formue + + Poke + + + + {sortedData.map(it => ( + + {it.name} + {it.email} + {it.age} + + {formatNumber(it.fortune, { + decimals: 2, + })} + + {it.button} + + ))} + + + ); + }} +
+ ); +}; diff --git a/component-overview/examples/tables/Table.jsx b/component-overview/examples/tables/Table.jsx index 33decbb919..46e3394a2a 100644 --- a/component-overview/examples/tables/Table.jsx +++ b/component-overview/examples/tables/Table.jsx @@ -1,10 +1,15 @@ -import Table from '@sb1/ffe-tables-react'; +import { + Table, + TableHead, + TableBody, + TableHeaderCell, + TableRow, + TableDataCell, + TableCaption, + TableFoot, +} from '@sb1/ffe-tables-react'; () => { - const columns = [ - { key: 'name', header: 'Navn' }, - { key: 'age', header: 'Alder' }, - ]; const data = [ { name: 'Anders', age: 32 }, { name: 'Erik', age: 25 }, @@ -13,5 +18,34 @@ import Table from '@sb1/ffe-tables-react'; { name: 'Sandra', age: 42 }, ]; - return ; -} + return ( +
+ Utviklere + + + Navn + Alder + + + + {data.map(it => ( + + {it.name} + {it.age} + + ))} + + + + + Gjenomsnitts alder + + + {data.reduce((sum, curr) => sum + curr.age, 0) / + data.length} + + + +
+ ); +}; diff --git a/packages/ffe-tables-react/package.json b/packages/ffe-tables-react/package.json index 78ef1ea0e7..f3cfe020fc 100644 --- a/packages/ffe-tables-react/package.json +++ b/packages/ffe-tables-react/package.json @@ -26,14 +26,10 @@ "test:watch": "ffe-buildtool jest --watch" }, "dependencies": { + "@sb1/ffe-collapse-react": "^4.0.9", "@sb1/ffe-icons-react": "^10.0.5", "@sb1/ffe-tables": "^14.0.23", - "classnames": "^2.3.1", - "deep-equal": "^1.0.1", - "memoize-one": "^5.0.0", - "prop-types": "^15.7.2", - "react-lifecycles-compat": "^3.0.4", - "shallow-equals": "^1.0.0" + "classnames": "^2.3.1" }, "devDependencies": { "@sb1/ffe-buildtool": "^0.6.4", diff --git a/packages/ffe-tables-react/src/DefaultTable/DefaultTable.js b/packages/ffe-tables-react/src/DefaultTable/DefaultTable.js deleted file mode 100644 index d4d69417ec..0000000000 --- a/packages/ffe-tables-react/src/DefaultTable/DefaultTable.js +++ /dev/null @@ -1,197 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import TableHeaders from '../TableParts/TableHeaders'; -import TableFooter from '../TableParts/TableFooter'; -import TableRow from '../TableParts/TableRow'; -import TableRowExpandable from '../TableParts/TableRowExpandable'; - -class DefaultTable extends Component { - renderTableCaption() { - const { caption, srOnlyCaption } = this.props; - - if (!caption) { - return null; - } - - if (srOnlyCaption) { - return ( - - {caption} - - ); - } - - return {caption}; - } - - renderTableHeaders() { - const { columns = [], headerRender } = this.props; - - if (!columns.length) { - return null; - } - - const renderColumns = [...columns]; - - if (this.props.expandedContentMapper) { - renderColumns.push({ - key: 'expandIcon', - header: ( - - Valg for raden - - ), - alignRight: true, - }); - } - - return ( - - ); - } - - renderTableFooter() { - const { columns, footerRender } = this.props; - - if (!columns.some(column => column.footer)) { - return null; - } - return ( - - ); - } - - getData() { - const { data = [], offset = 0, limit = 0 } = this.props; - if (!offset && !limit) { - return data; - } - - return data.slice(offset, limit ? offset + limit : data.length); - } - - renderTableBody() { - const { columns, expandedContentMapper, sort, rowRender } = this.props; - - const data = this.getData(); - - if (!data.length) { - return null; - } - - const renderColumns = [...columns]; - - if (expandedContentMapper) { - renderColumns.push({ - key: 'expandIcon', - header: '', - alignRight: true, - }); - return data.map((row, index) => { - const key = row.id || row.id === 0 ? row.id.toString() : index; - const expandedContent = expandedContentMapper(row); - const rowProps = { - cells: row, - columns: renderColumns, - sort, - rowRender, - rowIndex: index, - defaultExpanded: row.defaultExpanded, - scrollToOnMount: row.scrollToOnMount, - }; - return expandedContent ? ( - - {expandedContent} - - ) : ( - - - - ); - }); - } - - return ( - - {data.map((row, index) => { - const key = - row.id || row.id === 0 ? row.id.toString() : index; - return ( - - ); - })} - - ); - } - - render() { - const { - alignLeft, - condensed, - smallHeader, - columnLayoutMobile, - breakpoint, - className, - } = this.props; - return ( - - {this.renderTableCaption()} - {this.renderTableHeaders()} - {this.renderTableFooter()} - {this.renderTableBody()} -
- ); - } -} - -DefaultTable.propTypes = { - alignLeft: PropTypes.bool, - columnLayoutMobile: PropTypes.bool, - caption: PropTypes.node, - srOnlyCaption: PropTypes.bool, - expandedContentMapper: PropTypes.func, - condensed: PropTypes.bool, - smallHeader: PropTypes.bool, - sort: PropTypes.object, - offset: PropTypes.number, - limit: PropTypes.number, - breakpoint: PropTypes.oneOf(['sm', 'none']), - data: PropTypes.arrayOf(PropTypes.object), - columns: PropTypes.arrayOf( - PropTypes.shape({ - header: PropTypes.node.isRequired, - key: PropTypes.string.isRequired, - }).isRequired, - ), - rowRender: PropTypes.func, - headerRender: PropTypes.func, - footerRender: PropTypes.func, - className: PropTypes.string, -}; - -export default DefaultTable; diff --git a/packages/ffe-tables-react/src/DefaultTable/DefaultTable.spec.js b/packages/ffe-tables-react/src/DefaultTable/DefaultTable.spec.js deleted file mode 100644 index c7716af4e5..0000000000 --- a/packages/ffe-tables-react/src/DefaultTable/DefaultTable.spec.js +++ /dev/null @@ -1,713 +0,0 @@ -import React from 'react'; -import Table from './DefaultTable'; -import { fireEvent, render, screen, within } from '@testing-library/react'; -import { expect } from '@playwright/test'; - -describe('', () => { - const columns = [ - { key: 'name', header: 'Navn' }, - { key: 'address', header: 'Adresse' }, - { key: 'age', header: 'Alder', alignRight: true }, - ]; - const columnsWithFooterContent = [ - { key: 'name', header: 'Navn', footer: 'Sum alder' }, - { key: 'address', header: 'Adresse' }, - { key: 'age', header: 'Alder', footer: 135, alignRight: true }, - ]; - const data = [ - { - name: 'Ola Normann', - address: 'Gateveien 2', - age: 23, - phone: '912 34 567', - }, - { - name: 'Sivert Svenska', - address: 'Gatuveio 7', - age: 45, - phone: '+46 123 456 789', - }, - { - name: 'Daniel Dansk', - address: 'Kartoffel Fjers', - age: 67, - phone: '+45 018 456 789', - }, - ]; - - /* const wrapperWithoutOverride = mount( -
, - ); -*/ - describe('general', () => { - it('renders the correct dom structure', () => { - render(
); - const table = screen.getByRole('table'); - expect(table.classList.contains('ffe-table')).toBe(true); - expect(table.classList.contains('ffe-table--condensed')).toBe( - false, - ); - }); - - it('can be condensed', () => { - render(
); - const table = screen.getByRole('table'); - expect(table.classList.contains('ffe-table')).toBe(true); - expect(table.classList.contains('ffe-table--condensed')).toBe(true); - }); - - it('can be left aligned', () => { - render(
); - const table = screen.getByRole('table'); - expect(table.classList.contains('ffe-table')).toBe(true); - expect(table.classList.contains('ffe-table--text-left')).toBe(true); - }); - - it('can have a two column layout on mobile', () => { - render( -
, - ); - const table = screen.getByRole('table'); - expect(table.classList.contains('ffe-table')).toBe(true); - expect(table.classList.contains('ffe-table--columns-sm')).toBe( - true, - ); - }); - - it('applies supplied classes', () => { - render( -
, - ); - const table = screen.getByRole('table'); - expect(table.classList.contains('ffe-table')).toBe(true); - expect(table.classList.contains('test-class-one')).toBe(true); - expect(table.classList.contains('test-class-two')).toBe(true); - }); - }); - - describe('table caption', () => { - it('does not render a
if caption prop is not provided', () => { - const { container } = render( - , - ); - expect(container.querySelector('caption')).toBeFalsy(); - }); - - it('renders a
if caption prop is provided', () => { - const { container } = render( - , - ); - expect(container.querySelector('caption').textContent).toBe( - 'A nice table', - ); - }); - }); - - describe('table headers', () => { - it('renders columns inside a ', () => { - render(
); - const [thead] = screen.getAllByRole('rowgroup'); - expect(thead.tagName).toBe('THEAD'); - }); - - it('renders columns as
tags', () => { - render(); - const [thead] = screen.getAllByRole('rowgroup'); - expect(within(thead).getAllByRole('columnheader')).toHaveLength(3); - }); - - it('renders header content as
content', () => { - render(); - const [first] = screen.getAllByRole('columnheader'); - expect(first.textContent).toBe(columns[0].header); - }); - }); - - describe('table footer', () => { - it('renders a ', () => { - render(
); - const [, tfoot] = screen.getAllByRole('rowgroup'); - expect(tfoot.tagName).toBe('TFOOT'); - }); - - it('renders the footer content inside ', () => { - render(
); - const [, tfoot] = screen.getAllByRole('rowgroup'); - const [firstColumn] = within(tfoot).getAllByRole('cell'); - expect(firstColumn.textContent).toBe('Sum alder'); - }); - - it('does not render unless footer is present in columns', () => { - render(
); - const [, , tfoot] = screen.getAllByRole('rowgroup'); - expect(tfoot).toBeFalsy(); - }); - }); - - describe('table footer override', () => { - let container; - let rerender; - let mockCallback; - - const columnsForFooterRender = [ - { key: 'name', header: 'Navn', footer: 'Sum alder' }, - { - key: 'address', - header: 'Adresse', - columnFooterRender: (value, dataWindow, tdProps, spanProps) => { - return ( - - ); - }, - }, - { key: 'age', header: 'Alder', footer: 135, alignRight: true }, - ]; - - beforeEach(() => { - mockCallback = jest.fn(); - }); - beforeEach(() => { - const { container: _container, rerender: _rerender } = render( -
- {value} -
{ - return ( - - { - mockCallback(); - }} - /> - { - mockCallback(); - }} - > - {trProps.children[0]} - {trProps.children[1]} - {trProps.children[2]} - - - ); - }} - />, - ); - container = _container; - rerender = _rerender; - }); - - it('renders a , triggers tr onClick method', () => { - fireEvent.click(container.querySelectorAll('tfoot tr')[0]); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - it('renders a that has two identical rows, the second row has array notation', () => { - const [tr0html, tr1html] = container.querySelectorAll('tfoot tr'); - expect(tr1html.innerHTML).toBe(tr0html.innerHTML); - }); - - it('renders a with override that should look like the default', () => { - rerender(
); - const withoutOverrideHtml = - container.querySelectorAll('tfoot')[0].innerHTML; - - rerender( -
{ - return ; - }} - />, - ); - const withDefaultOverrideHtml = - container.querySelectorAll('tfoot')[0].innerHTML; - expect(withDefaultOverrideHtml).toBe(withoutOverrideHtml); - }); - }); - - describe('table header override', () => { - let container; - let rerender; - let mockCallback; - - const columnsForFooterRender = [ - { key: 'name', header: 'Navn', footer: 'Navn' }, - { key: 'address', header: 'Adresse' }, - { key: 'age', header: 'Alder', footer: 135, alignRight: true }, - ]; - - beforeEach(() => { - mockCallback = jest.fn(); - }); - beforeEach(() => { - const { container: _container, rerender: _rerender } = render( -
{ - return ( - - { - mockCallback(); - }} - /> - { - mockCallback(); - }} - > - {trProps.children[0]} - {trProps.children[1]} - {trProps.children[2]} - - - ); - }} - />, - ); - container = _container; - rerender = _rerender; - }); - - it('renders a , triggers tr onClick method', () => { - fireEvent.click(container.querySelectorAll('thead tr')[0]); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - it('renders a that has two identical rows, the second row has array notation', () => { - const [tr0html, tr1html] = container.querySelectorAll('thead tr'); - expect(tr1html.innerHTML).toBe(tr0html.innerHTML); - }); - - it('renders a with override that should look like the default', () => { - rerender(
); - const withoutOverrideHtml = - container.querySelectorAll('thead')[0].innerHTML; - - rerender( -
{ - return ; - }} - />, - ); - const withDefaultOverrideHtml = - container.querySelectorAll('thead')[0].innerHTML; - expect(withDefaultOverrideHtml).toBe(withoutOverrideHtml); - }); - }); - - describe('expandable table rows', () => { - it('should render expandable rows when if expandable content is available', () => { - const { container } = render( -
- row.address && ( - - Adresse: {row.address} - - ) - } - />, - ); - - expect(container.querySelectorAll('tbody').length).toBe(2); - expect( - container.querySelectorAll('.ffe-table__row-expandable').length, - ).toBe(1); - }); - }); - - describe('table row render override', () => { - let container; - let rerender; - let mockCallback; - - const columnsForRenderOverride = [ - { - key: 'name', - header: 'Navn', - footer: 'Navn', - cellRender: (value, col, props) => { - return {value}; - }, - }, - { key: 'address', header: 'Adresse' }, - { key: 'age', header: 'Alder', footer: 135, alignRight: true }, - ]; - - const expandedContentMapper = row => - row.address && ( - Adresse: {row.address} - ); - - beforeEach(() => { - mockCallback = jest.fn(); - }); - beforeEach(() => { - const { container: _container, rerender: _rerender } = render( -
{ - return ( - { - mockCallback(); - }} - /> - ); - }} - />, - ); - container = _container; - rerender = _rerender; - }); - - it('renders a , triggers tr onClick method', () => { - fireEvent.click(container.querySelectorAll('tbody tr')[0]); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - it('renders a with extendedRow, triggers tr onClick method', () => { - rerender( -
{ - return ( - { - mockCallback(); - trprops.onClick(event); - }} - /> - ); - }} - />, - ); - const tbody = container.querySelectorAll('tbody')[0]; - - fireEvent.click(tbody.querySelectorAll('tr')[0]); - expect( - Array.from( - tbody.querySelectorAll( - '.ffe-table__row-expandable-content--expanded', - ), - ).length, - ).toBe(1); - expect(mockCallback.mock.calls.length).toBe(1); - - fireEvent.click(tbody.querySelectorAll('tr')[0]); - expect( - Array.from( - tbody.querySelectorAll( - '.ffe-table__row-expandable-content--expanded', - ), - ).length, - ).toBe(0); - }); - - it('renders a with override that should look like the default', () => { - rerender(
); - const withoutOverrideHtml = - container.querySelectorAll('tbody')[0].innerHTML; - - rerender( -
{ - return ; - }} - />, - ); - const withDefaultOverrideHtml = - container.querySelectorAll('tbody')[0].innerHTML; - expect(withDefaultOverrideHtml).toBe(withoutOverrideHtml); - }); - }); - - describe('table body', () => { - it('does not render a tbody if data prop is empty', () => { - const { container } = render(
); - expect(container.querySelectorAll('tbody')).toHaveLength(0); - }); - - it('renders a tbody if data is provided', () => { - const { container } = render( -
, - ); - expect(container.querySelectorAll('tbody')).toHaveLength(1); - }); - - it('renders a row for each entry in the data prop', () => { - const { container } = render( -
, - ); - expect(container.querySelectorAll('tbody tr')).toHaveLength( - data.length, - ); - }); - - it('renders a + ); +}; diff --git a/packages/ffe-tables-react/src/TableCaption.tsx b/packages/ffe-tables-react/src/TableCaption.tsx new file mode 100644 index 0000000000..2b15422ada --- /dev/null +++ b/packages/ffe-tables-react/src/TableCaption.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import classNames from 'classnames'; + +export interface TableCaptionProps + extends React.ComponentPropsWithoutRef<'caption'> {} + +export const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + TableCaptionProps +>(({ className, ...rest }, ref) => { + return ( + + ); +}; diff --git a/packages/ffe-tables-react/src/TableHead.tsx b/packages/ffe-tables-react/src/TableHead.tsx new file mode 100644 index 0000000000..7a71ab958c --- /dev/null +++ b/packages/ffe-tables-react/src/TableHead.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import classNames from 'classnames'; + +export interface TableHeadProps + extends React.ComponentPropsWithoutRef<'thead'> {} + +export const TableHead: React.FC = ({ className, ...rest }) => { + return ( + + ); +}; diff --git a/packages/ffe-tables-react/src/TableHeaderCell.tsx b/packages/ffe-tables-react/src/TableHeaderCell.tsx new file mode 100644 index 0000000000..44bc3faf86 --- /dev/null +++ b/packages/ffe-tables-react/src/TableHeaderCell.tsx @@ -0,0 +1,68 @@ +import React, { useContext, useRef } from 'react'; +import classNames from 'classnames'; +import { Icon } from '@sb1/ffe-icons-react'; +import { TableContext } from './TableContext'; + +export interface TableHeaderCellProps + extends React.ComponentPropsWithoutRef<'th'> { + sortKey?: string; +} + +const arrowUpIcon = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjAiPjxwYXRoIGQ9Ik00ODAtNTM4Ljg0NyAzMDkuMDc2LTM2Ny45MjRxLTguMzA3IDguMzA4LTE4LjM4NCA4LjUtMTAuMDc2LjE5My0xOC43NjgtOC41LTguNjkzLTguNjkyLTguNjkzLTE4LjU3NnQ4LjY5My0xOC41NzZsMTg1Ljc2OS0xODUuNzY5cTkuODQ2LTkuODQ2IDIyLjMwNy05Ljg0NiAxMi40NjEgMCAyMi4zMDcgOS44NDZsMTg1Ljc2OSAxODUuNzY5cTguMzA4IDguMzA3IDguNSAxNy44ODQuMTkzIDkuNTc2LTguNSAxOC4yNjgtOC42OTIgOC42OTMtMTguNTc2IDguNjkzdC0xOC41NzYtOC42OTNMNDgwLTUzOC44NDdaIi8+PC9zdmc+'; + +const unfoldIcon = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjAiPjxwYXRoIGQ9Im00ODAtMjMxLjM4NCA5NC43NjEtOTQuNzYxcTguMDA5LTguMDA4IDE4LjA4NS04LjIwMSAxMC4wNzctLjE5MiAxOC4zODQgOC4xMTYgOC4zMDggOC4zMDcgOC4zMDggMTguMTkxIDAgOS44ODUtOC4yMDMgMTguMTI0TDUwMi43MzEtMTgwLjk0NHEtNS4wNjMgNS4wODEtMTAuNjUgNy4xNjYtNS41ODYgMi4wODUtMTIuMDgxIDIuMDg1LTYuNDk1IDAtMTIuMDgxLTIuMDg1LTUuNTg3LTIuMDg1LTEwLjY1LTcuMTY2TDM0OC42NjUtMjg5LjkxNXEtNy44MTgtNy44NTUtNy41MTEtMTcuOTMxLjMwOC0xMC4wNzcgOC42MTYtMTguMzg0IDguMzA3LTguMzA4IDE4LjE5MS04LjMwOCA5Ljg4NSAwIDE4LjE2NCA4LjM5M0w0ODAtMjMxLjM4NFptMC00OTguNzcxLTk0Ljc2MSA5NC43NjFxLTguMDA5IDguMDA5LTE4LjA4NSA4LjIwMS0xMC4wNzcuMTkyLTE4LjM4NC04LjExNS04LjMwOC04LjMwOC04LjMwOC0xOC4xOTJ0OC4yMDMtMTguMTIzbDEwOC42MDQtMTA4Ljk3MXE1LjA2My01LjA4MSAxMC42NS03LjE2NiA1LjU4Ni0yLjA4NSAxMi4wODEtMi4wODUgNi40OTUgMCAxMi4wODEgMi4wODUgNS41ODcgMi4wODUgMTAuNjUgNy4xNjZsMTA4LjYwNCAxMDguOTcxcTcuODE4IDcuODU0IDguMDExIDE3LjQzMS4xOTIgOS41NzYtOC4xMTYgMTcuODg0LTguMzA3IDguMzA3LTE4LjE5MSA4LjMwNy05Ljg4NSAwLTE4LjI3OC04LjI3OUw0ODAtNzMwLjE1NVoiLz48L3N2Zz4='; + +export const TableHeaderCell = React.forwardRef< + HTMLTableCellElement, + TableHeaderCellProps +>(({ className, sortKey, children, onClick, ...rest }, ref) => { + const { updateSort, getSort } = useContext(TableContext); + const sortButtonRef = useRef(null); + + const ariaSort = sortKey ? getSort(sortKey) : undefined; + + return ( + + ); +}); diff --git a/packages/ffe-tables-react/src/TableParts/TableCell.js b/packages/ffe-tables-react/src/TableParts/TableCell.js deleted file mode 100644 index c749eee5ab..0000000000 --- a/packages/ffe-tables-react/src/TableParts/TableCell.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const TableCell = props => { - const { - alignRight, - alignTop, - children, - columnHeader, - contentClasses, - tdClasses, - } = props; - - let dataTh = ''; - if (columnHeader) { - if (typeof columnHeader === 'string') { - dataTh = columnHeader; - } else if (columnHeader.props && columnHeader.props.children) { - dataTh = React.Children.toArray(columnHeader.props.children)[0]; - } - } - - return ( - - ); -}; - -TableCell.propTypes = { - alignRight: PropTypes.bool, - alignTop: PropTypes.bool, - children: PropTypes.any, - columnHeader: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - contentClasses: PropTypes.string, - tdClasses: PropTypes.string, -}; - -export default TableCell; diff --git a/packages/ffe-tables-react/src/TableParts/TableFooter.js b/packages/ffe-tables-react/src/TableParts/TableFooter.js deleted file mode 100644 index 944f695367..0000000000 --- a/packages/ffe-tables-react/src/TableParts/TableFooter.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import TableCell from './TableCell'; -import classNames from 'classnames'; - -const TableFooter = ({ columns, footerRender, dataWindow }) => { - const footerContent = columns.map((column, index) => { - if (column === null) { - return ; - - const footerRendered = - typeof footerRender === 'function' - ? footerRender(trProps, columns) - : trDefault; - - return {footerRendered}; -}; - -TableFooter.propTypes = { - columns: PropTypes.arrayOf( - PropTypes.shape({ - alignRight: PropTypes.bool, - alignTop: PropTypes.bool, - footer: PropTypes.any, - header: PropTypes.node.isRequired, - hideOnDesktop: PropTypes.bool, - hideOnMobile: PropTypes.bool, - hideOnSmallTablet: PropTypes.bool, - hideOnTablet: PropTypes.bool, - }), - ), - footerRender: PropTypes.func, - dataWindow: PropTypes.array, -}; - -export default TableFooter; diff --git a/packages/ffe-tables-react/src/TableParts/TableHeaders.js b/packages/ffe-tables-react/src/TableParts/TableHeaders.js deleted file mode 100644 index 1628cb8761..0000000000 --- a/packages/ffe-tables-react/src/TableParts/TableHeaders.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const TableHeaders = ({ columns, headerRender, dataWindow }) => { - const headerContent = columns.map((column, index) => { - if (column === null) { - return - ); - }); - const trProps = { className: 'ffe-table__row', children: headerContent }; - - const trDefault = ; - - const headerRendered = - typeof headerRender === 'function' - ? headerRender(trProps, columns) - : trDefault; - - return {headerRendered}; -}; - -TableHeaders.propTypes = { - columns: PropTypes.arrayOf( - PropTypes.shape({ - alignRight: PropTypes.bool, - header: PropTypes.node.isRequired, - hideOnDesktop: PropTypes.bool, - hideOnMobile: PropTypes.bool, - hideOnSmallTablet: PropTypes.bool, - hideOnTablet: PropTypes.bool, - key: PropTypes.string.isRequired, - }), - ), - headerRender: PropTypes.func, - dataWindow: PropTypes.array, -}; - -export default TableHeaders; diff --git a/packages/ffe-tables-react/src/TableParts/TableRow.js b/packages/ffe-tables-react/src/TableParts/TableRow.js deleted file mode 100644 index 5df98a1c15..0000000000 --- a/packages/ffe-tables-react/src/TableParts/TableRow.js +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import TableCell from './TableCell'; - -const TableRow = props => { - const { - cells, - columns, - onClick, - onKeyDown, - trClasses, - expandable, - expanded, - rowRender, - rowIndex, - } = props; - - const expandableProps = expandable && { - 'aria-expanded': expanded, - onClick, - onKeyDown, - role: 'button', - tabIndex: '0', - }; - - const rowContent = columns.map((column, index) => { - return ( - - {column.cellRender - ? column.cellRender(cells[column.key], column, props, index) - : cells[column.key]} - - ); - }); - - const trprops = { - className: classNames('ffe-table__row', trClasses), - ...expandableProps, - onClick: onClick, - children: rowContent, - }; - - if (typeof rowRender === 'function') { - return rowRender(trprops, props, rowIndex); - } - - return {rowContent}; -}; - -TableRow.propTypes = { - cells: PropTypes.object.isRequired, - columns: PropTypes.arrayOf( - PropTypes.shape({ - alignRight: PropTypes.bool, - alignTop: PropTypes.bool, - header: PropTypes.any, - hideOnDesktop: PropTypes.bool, - hideOnMobile: PropTypes.bool, - hideOnSmallTablet: PropTypes.bool, - hideOnTablet: PropTypes.bool, - key: PropTypes.string, - cellRender: PropTypes.func, - }), - ).isRequired, - expandable: PropTypes.bool, - expanded: PropTypes.bool, - onClick: PropTypes.func, - rowRender: PropTypes.func, - headerRender: PropTypes.func, - footerRender: PropTypes.func, - onKeyDown: PropTypes.func, - trClasses: PropTypes.string, - rowIndex: PropTypes.number, -}; - -export default TableRow; diff --git a/packages/ffe-tables-react/src/TableParts/TableRowExpandable.js b/packages/ffe-tables-react/src/TableParts/TableRowExpandable.js deleted file mode 100644 index 1765f0b4a2..0000000000 --- a/packages/ffe-tables-react/src/TableParts/TableRowExpandable.js +++ /dev/null @@ -1,135 +0,0 @@ -import { Icon } from '@sb1/ffe-icons-react'; -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { polyfill } from 'react-lifecycles-compat'; -import shallowEqual from 'shallow-equals'; -import TableRow from './TableRow'; - -class TableRowExpandable extends Component { - constructor(props) { - super(props); - this.handleKeyPress = this.handleKeyPress.bind(this); - this.toggleExpand = this.toggleExpand.bind(this); - this.state = { - expanded: !!props.defaultExpanded, - sort: props.sort, - }; - this.rowRef = React.createRef(); - } - - componentDidMount() { - if (this.props.scrollToOnMount && this.rowRef.current) { - this.rowRef.current.scrollIntoView(); - } - } - - static getDerivedStateFromProps(nextProps, prevState) { - if ( - (!nextProps.children && prevState.expanded) || - !shallowEqual(nextProps.sort, prevState.sort) - ) { - return { - expanded: false, - sort: nextProps.sort, - }; - } - - return null; - } - - toggleExpand() { - this.setState(prevState => ({ expanded: !prevState.expanded })); - } - - handleKeyPress(event) { - if ( - (event.key === 'Enter' || event.key === ' ') && - event.target.tagName === 'TR' - ) { - this.setState(prevState => ({ expanded: !prevState.expanded })); - event.preventDefault(); - } - } - - render() { - const { cells, columns, children, rowRender, rowIndex } = this.props; - const unexpandable = !children; - - return ( - - - ), - }} - trClasses={classNames( - { 'ffe-table__row-expandable': !unexpandable }, - { - 'ffe-table__row-expandable--expanded': - this.state.expanded, - }, - )} - columns={columns} - onClick={this.toggleExpand} - onKeyDown={this.handleKeyPress} - expandable={!unexpandable} - expanded={this.state.expanded} - rowRender={rowRender} - rowIndex={rowIndex} - /> - - - - - ); - } -} - -TableRowExpandable.propTypes = { - children: PropTypes.node, - cells: PropTypes.object.isRequired, - columns: PropTypes.array.isRequired, - sort: PropTypes.object, - rowRender: PropTypes.func, - headerRender: PropTypes.func, - footerRender: PropTypes.func, - rowIndex: PropTypes.number, - defaultExpanded: PropTypes.bool, - scrollToOnMount: PropTypes.bool, -}; - -polyfill(TableRowExpandable); - -export default TableRowExpandable; diff --git a/packages/ffe-tables-react/src/TableParts/TableRowExpandable.spec.js b/packages/ffe-tables-react/src/TableParts/TableRowExpandable.spec.js deleted file mode 100644 index a015bb3363..0000000000 --- a/packages/ffe-tables-react/src/TableParts/TableRowExpandable.spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import TableRowExpandable from './TableRowExpandable'; -import { fireEvent, render, screen } from '@testing-library/react'; - -const props = { - cells: { - name: 'Ola Normann', - address: 'Gateveien 2', - age: 23, - phone: '912 34 567', - email: 'ola@normann.no', - }, - columns: [ - { key: 'name', content: 'Navn' }, - { key: 'address', content: 'Adresse' }, - { key: 'age', content: 'Alder', alignRight: true }, - { key: 'phone', content: 'Telefon' }, - { key: 'email', content: 'E-post' }, - ], -}; - -describe('', () => { - it('should be collapsed by default', () => { - const { container } = render( - -

The cake is a lie

-
, - ); - expect( - container.querySelectorAll('.ffe-table__row-expandable--expanded'), - ).toHaveLength(0); - - const content = container.querySelector( - '.ffe-table__row-expandable-content', - ); - - expect(content.getAttribute('aria-hidden')).toBe('true'); - expect( - content.classList.contains('ffe-table__row--collapsed'), - ).toBeTruthy(); - }); - - it('should be expanded if provided true defaultExpanded', () => { - const { container } = render( - -

The cake is a lie

-
, - ); - - expect( - container.querySelectorAll('.ffe-table__row--collapsed'), - ).toHaveLength(0); - - const content = container.querySelector( - '.ffe-table__row-expandable-content', - ); - - expect(content.getAttribute('aria-hidden')).toBe('false'); - expect( - content.classList.contains( - 'ffe-table__row-expandable-content--expanded', - ), - ).toBeTruthy(); - }); - - it('should scroll into view on mount if true scrollToOnMount is provided', () => { - const scrollIntoViewMock = jest.fn(); - window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; - - render( - -

The cake is a lie

-
, - ); - - expect(scrollIntoViewMock).toHaveBeenCalledTimes(1); - }); - - it('should render expanded content', () => { - render( - -

The cake is a lie

-
, - ); - - const row = screen.getByRole('button'); - fireEvent.click(row); - const expandedContent = screen.getByRole('presentation'); - expect(expandedContent.textContent).toBe('The cake is a lie'); - expect( - expandedContent.classList.contains( - 'ffe-table__row-expandable-content--expanded', - ), - ).toBeTruthy(); - expect( - expandedContent.classList.contains('ffe-table__row--collapsed'), - ).toBeFalsy(); - }); -}); diff --git a/packages/ffe-tables-react/src/TableRow.tsx b/packages/ffe-tables-react/src/TableRow.tsx new file mode 100644 index 0000000000..a878f8f2bb --- /dev/null +++ b/packages/ffe-tables-react/src/TableRow.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import classNames from 'classnames'; + +export interface TableRowProps extends React.ComponentPropsWithoutRef<'tr'> {} + +export const TableRow = React.forwardRef( + ({ className, ...rest }, ref) => { + return ( +
+ ); + }, +); diff --git a/packages/ffe-tables-react/src/TableRowExpandable.tsx b/packages/ffe-tables-react/src/TableRowExpandable.tsx new file mode 100644 index 0000000000..32cc80d43a --- /dev/null +++ b/packages/ffe-tables-react/src/TableRowExpandable.tsx @@ -0,0 +1,88 @@ +import React, { useRef, useState } from 'react'; +import classNames from 'classnames'; +import { Icon } from '@sb1/ffe-icons-react'; +import { Collapse } from '@sb1/ffe-collapse-react'; +import { txt } from './texts'; +import { v4 as uuid } from 'uuid'; + +export interface TableRowExpandableProps + extends React.ComponentPropsWithoutRef<'tr'> { + expandContent?: React.ReactNode; + locale?: 'nb' | 'nn' | 'en'; +} + +export const TableRowExpandable = React.forwardRef< + HTMLTableRowElement, + TableRowExpandableProps +>( + ( + { className, expandContent, children, locale = 'nb', onClick, ...rest }, + ref, + ) => { + const [isOpen, setIsOpen] = useState(false); + const expandoRowId = useRef(`check-${uuid()}`).current; + const [isAnimating, setIsAnimating] = useState(false); + const expandButtonRef = useRef(null); + + return ( + <> + { + if ( + !expandButtonRef.current?.contains(e.target as Node) + ) { + expandButtonRef.current?.click(); + } + onClick?.(e); + }} + ref={ref} + {...rest} + > + {children} + + + + + + + ); + }, +); diff --git a/packages/ffe-tables-react/src/index.d.ts b/packages/ffe-tables-react/src/index.d.ts deleted file mode 100644 index 245ecbfb04..0000000000 --- a/packages/ffe-tables-react/src/index.d.ts +++ /dev/null @@ -1,133 +0,0 @@ -import * as React from 'react'; - -export interface RowRenderTrProps extends React.ComponentProps<'tr'> {} - -export interface TableRowProps { - cells: Data; - columns: Column[]; - expandable?: boolean; - expanded?: boolean; - onClick?: (e: React.MouseEvent | undefined) => void; - rowRender?: ( - trprops: RowRenderTrProps, - props: TableRowProps, - rowIndex: number, - ) => JSX.Element; - headerRender?: ( - trprops: HeaderRenderTrProps, - columns: Column[], - ) => JSX.Element; - footerRender?: ( - trprops: HeaderRenderTrProps, - columns: Column[], - ) => JSX.Element; - onKeyDown?: (e: React.KeyboardEvent | undefined) => void; - trClasses?: string; - rowIndex?: number; -} - -export interface SpanProps { - className?: string; -} - -export interface ThProps { - className: string; - key: number; - 'aria-sort': 'none' | 'ascending' | 'descending' | 'other'; - scope: string; -} - -export interface TdProps { - classNames: string; - 'data-th': string; - key: number; -} - -export interface Column { - header: React.ReactNode | ''; - key: string; - footer?: React.ReactNode; - alignRight?: boolean; - alignTop?: boolean; - hideOnDesktop?: boolean; - hideOnTablet?: boolean; - hideOnSmallTablet?: boolean; - hideOnMobile?: boolean; - compare?: (a: any, b: any) => number; - notSortable?: boolean; - cellRender?: ( - value: any, - col: Column, - props: TableRowProps, - index: number, - ) => JSX.Element; - columnHeaderRender?: ( - value: React.ReactNode | '', - dataWindow: Data[], - spanProps: SpanProps, - thProps: ThProps, - columns: Column[], - index: number, - ) => JSX.Element; - columnFooterRender?: ( - value: any, - dataWindow: Data[], - tdProps: TdProps, - spanProps: SpanProps, - columns: Column[], - index: number, - ) => JSX.Element; -} - -export interface Data { - id?: string | number; - [propName: string]: any; -} - -export interface HeaderRenderTrProps { - className: string; - children: any; -} - -export type OnSortParam = { - sortBy: string; - descending: boolean; - tableData: Data[]; -}; - -export interface TableProps { - caption?: React.ReactNode; - srOnlyCaption?: boolean; - expandedContentMapper?: (data: Data) => JSX.Element | false; - sortable?: boolean; - sortBy?: string; - onSort?: (config: OnSortParam) => void; - descending?: boolean; - offset?: number; - limit?: number; - condensed?: boolean; - smallHeader?: boolean; - alignLeft?: boolean; - columnLayoutMobile?: boolean; - breakpoint?: 'sm' | 'none'; - rowRender?: ( - trprops: RowRenderTrProps, - props: TableRowProps, - rowIndex: number, - ) => JSX.Element; - headerRender?: ( - trprops: HeaderRenderTrProps, - columns: Column[], - ) => JSX.Element; - footerRender?: ( - trprops: HeaderRenderTrProps, - columns: Column[], - ) => JSX.Element; - data?: Data[]; - columns?: Column[]; - className?: string; -} - -declare class Table extends React.Component {} - -export default Table; diff --git a/packages/ffe-tables-react/src/index.js b/packages/ffe-tables-react/src/index.js deleted file mode 100644 index de4c7d5e8f..0000000000 --- a/packages/ffe-tables-react/src/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Table from './Table'; - -export default Table; diff --git a/packages/ffe-tables-react/src/index.ts b/packages/ffe-tables-react/src/index.ts new file mode 100644 index 0000000000..a0175d127d --- /dev/null +++ b/packages/ffe-tables-react/src/index.ts @@ -0,0 +1,12 @@ +export { Table, TableProps } from './Table'; +export { TableBody, TableBodyProps } from './TableBody'; +export { TableDataCell, TableDataCellProps } from './TableDataCell'; +export { TableFoot, TableFootProps } from './TableFoot'; +export { TableHead, TableHeadProps } from './TableHead'; +export { TableHeaderCell, TableHeaderCellProps } from './TableHeaderCell'; +export { TableRow, TableRowProps } from './TableRow'; +export { TableCaption, TableCaptionProps } from './TableCaption'; +export { + TableRowExpandable, + TableRowExpandableProps, +} from './TableRowExpandable'; diff --git a/packages/ffe-tables-react/src/texts.ts b/packages/ffe-tables-react/src/texts.ts new file mode 100644 index 0000000000..47c716870d --- /dev/null +++ b/packages/ffe-tables-react/src/texts.ts @@ -0,0 +1,14 @@ +const nb = { + showMore: 'Vis mer', + showLess: 'Vis mindre', +} as const; +const nn = { + showMore: 'Vis meir', + showLess: 'Vis mindre', +} as const; +const en = { + showMore: 'Show more', + showLess: 'Show less', +} as const; + +export const txt = { nb, nn, en }; diff --git a/packages/ffe-tables-react/src/types.ts b/packages/ffe-tables-react/src/types.ts new file mode 100644 index 0000000000..7d72be5c75 --- /dev/null +++ b/packages/ffe-tables-react/src/types.ts @@ -0,0 +1,3 @@ +export const ariaSortOrders = ['none', 'ascending', 'descending'] as const; + +export type SortOrder = (typeof ariaSortOrders)[number]; diff --git a/packages/ffe-tables/less/expandable-row.less b/packages/ffe-tables/less/expandable-row.less deleted file mode 100644 index c2bf1723c9..0000000000 --- a/packages/ffe-tables/less/expandable-row.less +++ /dev/null @@ -1,94 +0,0 @@ -.ffe-table { - &__row-expandable { - cursor: pointer; - position: relative; - - @media (hover: hover) and (pointer: fine) { - &:hover { - background-color: var(--ffe-v-table-expandable-row-hover); - } - } - - &:focus { - outline: 2px solid var(--ffe-v-table-expandable-focus-outline); - } - - &:focus:not(:focus-visible) { - outline: none; - } - - &--expanded { - background-color: var(--ffe-v-table-expanded-row-bg); - } - - &--expanded:not(:last-child) { - border-color: var(--ffe-v-table-expanded-row-bg); - } - - .ffe-table__content { - @media (max-width: @breakpoint-sm) { - margin-bottom: 0; - } - } - } - - &__row-expandable-content { - opacity: 0; - - .ffe-table__cell-expandable-content { - padding: 0; - } - - &:not(.ffe-table__row-expandable-content--expanded) { - border: 0; - } - - &--expanded { - transition: background-color @ffe-transition-duration 0.1s; - animation: slide-row 0.2s cubic-bezier(0.5, 0.015, 0.485, 1.445); - background-color: var(--ffe-v-table-expanded-content-bg); - opacity: 1; - - .ffe-table__cell-expandable-content { - padding: @ffe-spacing-xs; - } - } - } - - &__expand-icon { - color: var(--ffe-v-table-expand-icon-fill); - transition: transform @ffe-transition-duration @ffe-ease-in-out-back; - position: absolute; - top: 10px; - right: 5px; - - &.ffe-icons { - color: var(--ffe-v-table-expand-icon-fill); - } - - @media (min-width: @breakpoint-md) { - position: static; - vertical-align: middle; - } - - &--expanded { - transform: rotate(180deg); - } - } -} - -@keyframes slide-row { - 0% { - transform: translateY(-2em); - opacity: 0; - } - - 50% { - opacity: 0.25; - } - - 100% { - transform: translateY(0); - opacity: 1; - } -} diff --git a/packages/ffe-tables/less/sortable-table.less b/packages/ffe-tables/less/sortable-table.less deleted file mode 100644 index 04fda6e805..0000000000 --- a/packages/ffe-tables/less/sortable-table.less +++ /dev/null @@ -1,59 +0,0 @@ -.ffe-sortable-table { - &__header { - color: var(--ffe-v-table-heading-color); - white-space: nowrap; - display: inline-block; - border-bottom: solid 2px transparent; - border-radius: 4px 4px 0 0; - padding-top: 2px; - - @media (hover: hover) and (pointer: fine) { - &:hover { - cursor: pointer; - color: var(--ffe-v-table-sortable-header-hover); - border-color: inherit; - user-select: none; - - .native & { - @media (prefers-color-scheme: dark) { - border-color: var(--ffe-v-table-sortable-header-hover); - } - } - - .ffe-sortable-table__sort-arrow.ffe-icons { - color: var(--ffe-v-table-sortable-header-hover); - } - } - } - - &--active { - border-bottom: solid 2px - var(--ffe-v-table-sortable-header-active-border); - } - - &:focus { - outline: none; - border-radius: 4px; - box-shadow: 0 0 0 2px - var(--ffe-v-table-sortable-header-focus-outline); - } - - &:focus:not(:focus-visible) { - box-shadow: none; - border-radius: 4px 4px 0 0; - } - } - - &__sort-arrow { - vertical-align: middle; - color: var(--ffe-v-table-sortable-arrow-fill); - transition: transform @ffe-transition-duration @ffe-ease-in-out-back; - - &.ffe-icons { - color: var(--ffe-v-table-sortable-arrow-fill); - } - &--descending { - transform: rotate(180deg); - } - } -} diff --git a/packages/ffe-tables/less/table-breakpoint-none.less b/packages/ffe-tables/less/table-breakpoint-none.less deleted file mode 100644 index 10fcf08679..0000000000 --- a/packages/ffe-tables/less/table-breakpoint-none.less +++ /dev/null @@ -1,82 +0,0 @@ -.ffe-table--breakpoint-none { - &.ffe-table--condensed { - .ffe-table__heading, - .ffe-table__cell { - padding: @ffe-spacing-xs; - } - } - - &.ffe-table--columns-sm { - .ffe-table__content { - display: block; - max-width: none; - margin-bottom: 0; - } - } - - [data-th]::before { - display: none; - } - - .ffe-table__head { - display: table-header-group; - } - - .ffe-table__row { - display: table-row; - } - - .ffe-table__heading { - display: table-cell; - padding: @ffe-spacing-xs; - } - - .ffe-table__cell { - display: table-cell; - padding: @ffe-spacing-sm @ffe-spacing-xs; - } - - .ffe-table__content { - display: block; - max-width: none; - margin-bottom: 0; - } - - .ffe-table__row-expandable { - .ffe-table__content { - margin-bottom: 0; - } - } - - .ffe-table__expand-icon { - position: static; - vertical-align: middle; - } - - .ffe-table__heading, - .ffe-table__cell { - &.ffe-table--hide-xlg { - @media (min-width: @breakpoint-lg) { - display: none; - } - } - - &.ffe-table--hide-lg { - @media (min-width: @breakpoint-md) and (max-width: (@breakpoint-lg - 1px)) { - display: none; - } - } - - &.ffe-table--hide-md { - @media (min-width: @breakpoint-sm) and (max-width: (@breakpoint-md - 1px)) { - display: none; - } - } - - &.ffe-table--hide-sm { - @media (max-width: (@breakpoint-sm - 1px)) { - display: none; - } - } - } -} diff --git a/packages/ffe-tables/less/table-breakpoint-sm.less b/packages/ffe-tables/less/table-breakpoint-sm.less deleted file mode 100644 index cbf4004c3c..0000000000 --- a/packages/ffe-tables/less/table-breakpoint-sm.less +++ /dev/null @@ -1,73 +0,0 @@ -.ffe-table--breakpoint-sm { - @media (min-width: @breakpoint-sm) { - &.ffe-table--condensed { - .ffe-table__heading, - .ffe-table__cell { - padding: @ffe-spacing-xs; - } - } - - &.ffe-table--columns-sm { - .ffe-table__content { - display: block; - max-width: none; - margin-bottom: 0; - } - } - - [data-th]::before { - display: none; - } - - .ffe-table__head { - display: table-header-group; - } - - .ffe-table__row { - display: table-row; - } - - .ffe-table__heading { - display: table-cell; - padding: @ffe-spacing-xs; - } - - .ffe-table__cell { - display: table-cell; - padding: @ffe-spacing-sm @ffe-spacing-xs; - } - - .ffe-table__content { - display: block; - max-width: none; - margin-bottom: 0; - } - } - - .ffe-table__heading, - .ffe-table__cell { - &.ffe-table--hide-xlg { - @media (min-width: @breakpoint-lg) { - display: none; - } - } - - &.ffe-table--hide-lg { - @media (min-width: @breakpoint-md) and (max-width: (@breakpoint-lg - 1px)) { - display: none; - } - } - - &.ffe-table--hide-md { - @media (min-width: @breakpoint-sm) and (max-width: (@breakpoint-md - 1px)) { - display: none; - } - } - - &.ffe-table--hide-sm { - @media (max-width: (@breakpoint-sm - 1px)) { - display: none; - } - } - } -} diff --git a/packages/ffe-tables/less/table.less b/packages/ffe-tables/less/table.less deleted file mode 100644 index 8817ba90e5..0000000000 --- a/packages/ffe-tables/less/table.less +++ /dev/null @@ -1,189 +0,0 @@ -.ffe-table { - font-family: 'SpareBank1-regular', arial, sans-serif; - margin: var(--ffe-spacing-sm) 0; - min-width: 100%; - text-align: left; - border-collapse: collapse; - - &--text-left { - .ffe-table__heading, - .ffe-table__content, - .ffe-table__cell { - // Set !important to make --text-left work with --condensed which sets padding to 10px - padding-left: 0 !important; - } - } - - &__caption { - &:extend(.ffe-h4 all); - } - - &__head { - display: none; - - @media (min-width: @breakpoint-md) { - display: table-header-group; - } - } - - &__footer, - &__heading { - font-family: var(--ffe-g-font-strong); - font-variant-numeric: tabular-nums; - font-weight: normal; - } - - &__footer { - .ffe-table__row { - border-bottom: none; - } - } - - &__row { - border-bottom: 1px solid var(--ffe-v-table-row-bordercolor); - display: block; - padding: var(--ffe-spacing-xs) 0; - - @media (min-width: @breakpoint-md) { - display: table-row; - } - } - - &__heading, - &__cell { - overflow-wrap: normal; - } - - &__heading { - color: var(--ffe-v-table-heading-color); - text-align: left; - - @media (min-width: @breakpoint-md) { - display: table-cell; - padding: var(--ffe-spacing-xs); - } - } - - &__cell { - display: block; - padding: 0 var(--ffe-spacing-2xs); - - &--top { - vertical-align: top; - } - - @media (min-width: @breakpoint-md) { - display: table-cell; - padding: var(--ffe-spacing-sm) var(--ffe-spacing-xs); - } - } - - &__row, - &__cell { - &--collapsed { - line-height: 0; - padding: 0; - font-size: 0; - } - } - - &--condensed { - font-size: 0.875rem; - - .ffe-table__heading, - .ffe-table__cell { - @media (min-width: @breakpoint-md) { - padding: var(--ffe-spacing-xs); - } - } - } - - &__content { - display: block; - max-width: none; - vertical-align: top; - margin-bottom: var(--ffe-spacing-sm); - color: var(--ffe-v-table-content-color); - - &--text-right { - text-align: right; - } - - @media (min-width: @breakpoint-md) { - display: block; - max-width: none; - margin-bottom: 0; - } - } - - &__heading &__content { - color: var(--ffe-v-table-heading-color); - } - - [data-th]::before { - content: attr(data-th); - display: block; - color: var(--ffe-v-table-heading-color); - .ffe-strong-text(); - - @media (min-width: @breakpoint-md) { - display: none; - } - } - - &--small-header [data-th]::before { - width: 65px; - } - - &--columns-sm { - .ffe-table__content { - display: inline-block; - max-width: ~'calc(100% - 105px)'; - margin-bottom: 0; - - @media (min-width: @breakpoint-md) { - display: block; - max-width: none; - margin-bottom: 0; - } - } - - [data-th]::before { - width: 100px; - display: inline-block; - } - - @media (min-width: @breakpoint-md) { - [data-th]::before { - display: none; - } - } - } - - .ffe-table__heading, - .ffe-table__cell { - &.ffe-table--hide-xlg { - @media (min-width: @breakpoint-lg) { - display: none; - } - } - - &.ffe-table--hide-lg { - @media (min-width: @breakpoint-md) and (max-width: (@breakpoint-lg - 1px)) { - display: none; - } - } - - &.ffe-table--hide-md { - @media (min-width: @breakpoint-sm) and (max-width: (@breakpoint-md - 1px)) { - display: none; - } - } - - &.ffe-table--hide-sm { - @media (max-width: (@breakpoint-sm - 1px)) { - display: none; - } - } - } -} diff --git a/packages/ffe-tables/less/tables.less b/packages/ffe-tables/less/tables.less index b8796be4b5..41aef3078c 100644 --- a/packages/ffe-tables/less/tables.less +++ b/packages/ffe-tables/less/tables.less @@ -1,6 +1,59 @@ @import 'theme'; -@import 'table'; -@import 'sortable-table'; -@import 'expandable-row'; -@import 'table-breakpoint-none'; -@import 'table-breakpoint-sm'; + +.ffe-table { + width: 100%; + border-collapse: collapse; + display: table; +} + +.ffe-table__caption { + margin-bottom: var(--ffe-spacing-md); + color: var(--ffe-g-heading-color); + font-size: var(--ffe-fontsize-h4); +} + +.ffe-table__head .ffe-table__th { + color: var(--ffe-v-table-heading-color); +} + +.ffe-table__th { + text-align: left; +} + +.ffe-table__td, +.ffe-table__th, +.ffe-table__expand-content { + padding: var(--ffe-spacing-sm); +} + +.ffe-table__body .ffe-table__tr { + border-bottom: 1px solid var(--ffe-v-table-row-bordercolor); + &:first-of-type { + border-top: 1px solid var(--ffe-v-table-row-bordercolor); + } +} + +.ffe-table__expand-button { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + display: flex; + align-items: center; + .ffe-icons { + color: var(--ffe-v-table-expand-icon-fill); + display: block; + transition: all var(--ffe-transition-duration) var(--ffe-ease); + } + + &--open .ffe-icons { + transform: rotate(180deg); + } +} + +.ffe-table__expand-content { + background: var(--ffe-v-table-expanded-content-bg); +} diff --git a/packages/ffe-tables/less/theme.less b/packages/ffe-tables/less/theme.less index 0b34461503..c059f13de4 100644 --- a/packages/ffe-tables/less/theme.less +++ b/packages/ffe-tables/less/theme.less @@ -1,34 +1,44 @@ :root, :host { - --ffe-v-table-row-bordercolor: var(--ffe-farge-varmgraa); --ffe-v-table-heading-color: var(--ffe-farge-fjell); - --ffe-v-table-content-color: var(--ffe-farge-svart); - --ffe-v-table-sortable-header-hover: var(--ffe-farge-natt); + --ffe-v-table-row-bordercolor: var(--ffe-farge-varmgraa); + --ffe-v-table-expand-icon-fill: var(--ffe-farge-vann); + --ffe-v-table-expanded-content-bg: var(--ffe-farge-syrin-30); + + /* --ffe-v-table-row-bordercolor: var(--ffe-farge-varmgraa); + --ffe-v-table-heading-color: var(--ffe-farge-fjell);*/ + /* --ffe-v-table-content-color: var(--ffe-farge-svart);*/ + /* --ffe-v-table-sortable-header-hover: var(--ffe-farge-natt); --ffe-v-table-sortable-header-active-border: var(--ffe-farge-fjell); - --ffe-v-table-sortable-header-focus-outline: var(--ffe-farge-vann); - --ffe-v-table-sortable-arrow-fill: var(--ffe-farge-fjell); - --ffe-v-table-expandable-row-hover: var(--ffe-farge-syrin-70); - --ffe-v-table-expandable-focus-outline: var(--ffe-farge-vann); - --ffe-v-table-expanded-row-bg: var(--ffe-farge-syrin-70); + --ffe-v-table-sortable-header-focus-outline: var(--ffe-farge-vann);*/ + /* --ffe-v-table-sortable-arrow-fill: var(--ffe-farge-fjell); + --ffe-v-table-expandable-row-hover: var(--ffe-farge-syrin-70);*/ + /* --ffe-v-table-expandable-focus-outline: var(--ffe-farge-vann);*/ + /* --ffe-v-table-expanded-row-bg: var(--ffe-farge-syrin-70); --ffe-v-table-expanded-content-bg: var(--ffe-farge-syrin-30); - --ffe-v-table-expand-icon-fill: var(--ffe-farge-vann); + --ffe-v-table-expand-icon-fill: var(--ffe-farge-vann);*/ @media (prefers-color-scheme: dark) { .native { - --ffe-v-table-row-bordercolor: var(--ffe-farge-graa); --ffe-v-table-heading-color: var(--ffe-farge-vann-70); - --ffe-v-table-content-color: var(--ffe-farge-lysgraa); - --ffe-v-table-sortable-header-hover: var(--ffe-farge-vann-30); + --ffe-v-table-row-bordercolor: var(--ffe-farge-graa); + --ffe-v-table-expand-icon-fill: var(--ffe-farge-vann-70); + --ffe-v-table-expanded-content-bg: var(--ffe-farge-svart); + + /* --ffe-v-table-row-bordercolor: var(--ffe-farge-graa); + --ffe-v-table-heading-color: var(--ffe-farge-vann-70);*/ + /* --ffe-v-table-content-color: var(--ffe-farge-lysgraa);*/ + /* --ffe-v-table-sortable-header-hover: var(--ffe-farge-vann-30); --ffe-v-table-sortable-header-active-border: var( --ffe-farge-vann-70 - ); - --ffe-v-table-sortable-header-focus-outline: var(--ffe-farge-hvit); - --ffe-v-table-sortable-arrow-fill: var(--ffe-farge-vann-70); - --ffe-v-table-expandable-row-hover: var(--ffe-farge-natt); - --ffe-v-table-expandable-focus-outline: var(--ffe-farge-hvit); + );*/ + /* --ffe-v-table-sortable-header-focus-outline: var(--ffe-farge-hvit); + --ffe-v-table-sortable-arrow-fill: var(--ffe-farge-vann-70);*/ + /* --ffe-v-table-expandable-row-hover: var(--ffe-farge-natt);*/ + /* --ffe-v-table-expandable-focus-outline: var(--ffe-farge-hvit); --ffe-v-table-expanded-row-bg: var(--ffe-farge-natt); --ffe-v-table-expanded-content-bg: var(--ffe-farge-svart); - --ffe-v-table-expand-icon-fill: var(--ffe-farge-vann-70); + --ffe-v-table-expand-icon-fill: var(--ffe-farge-vann-70);*/ } } }
cell for each entry in each row', () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('td')).toHaveLength( - data.length * columns.length, - ); - }); - - it('renders a content-div inside each td', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll('td .ffe-table__content').length, - ).toBe(data.length * columns.length); - }); - - it('renders content of each cell', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll('td .ffe-table__content')[0] - .textContent, - ).toBe(data[0].name); - }); - - it('adds column heading to data-th property of each cell', () => { - const { container } = render( -
, - ); - - expect(container.querySelectorAll('[data-th="Navn"]')).toHaveLength( - columns.length, - ); - }); - - it('render screen reader only caption', () => { - const { container } = render( -
, - ); - const captions = container.querySelectorAll('caption'); - - expect(captions).toHaveLength(1); - expect( - captions[0].classList.contains('ffe-screenreader-only'), - ).toBe(true); - expect(captions[0].textContent).toBe('Read me'); - }); - - it('do not render caption when srOnlyCaption is true but missing caption text', () => { - const { container } = render( -
, - ); - expect(container.querySelectorAll('caption')).toHaveLength(0); - }); - - it('sets correct class for top-alignment on table cells', () => { - const columnsWithTopAlignment = columns.map(column => ({ - ...column, - alignTop: true, - })); - - const { container } = render( -
, - ); - - expect( - container.querySelectorAll('.ffe-table__cell--top').length, - ).toBe(9); - }); - - it('sets correct class for hiding column on desktop', () => { - const columnsWithTopAlignment = columns.map((column, index) => - index === 0 ? { ...column, hideOnDesktop: true } : column, - ); - - const { container } = render( -
, - ); - - expect( - container.querySelectorAll('.ffe-table--hide-xlg').length, - ).toBe(4); - }); - }); - - describe('offset and limit properties', () => { - const muchData = 'ABCDEFGHIJKLMNO'.split('').map((c, i) => ({ - name: c, - address: `Sesame Street ${i}`, - age: i, - })); - - describe('setting limit', () => { - it('reduces number of table rows', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - ), - ).toHaveLength(3); - }); - - it('does not skip data', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - )[0].textContent, - ).toBe('A'); - }); - - it('limits data', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - )[2].textContent, - ).toBe('C'); - }); - }); - - describe('setting offset', () => { - it('reduces number of table rows', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - ), - ).toHaveLength(5); - }); - - it('skips data', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - )[0].textContent, - ).toBe('K'); - }); - - it('does not limits data', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - )[4].textContent, - ).toBe('O'); - }); - }); - - describe('setting both', () => { - it('reduces number of table rows', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - ), - ).toHaveLength(4); - }); - - it('skips data', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - )[0].textContent, - ).toBe('F'); - }); - - it('limits data', () => { - const { container } = render( -
, - ); - expect( - container.querySelectorAll( - '.ffe-table__cell:first-of-type .ffe-table__content', - )[3].textContent, - ).toBe('I'); - }); - }); - }); -}); diff --git a/packages/ffe-tables-react/src/SortableTable/SortableTable.js b/packages/ffe-tables-react/src/SortableTable/SortableTable.js deleted file mode 100644 index 42f617c872..0000000000 --- a/packages/ffe-tables-react/src/SortableTable/SortableTable.js +++ /dev/null @@ -1,197 +0,0 @@ -import { Icon } from '@sb1/ffe-icons-react'; -import classNames from 'classnames'; -import equal from 'deep-equal'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Table from '../Table'; -import sortData from './sort-data'; -import memoize from 'memoize-one'; - -class SortableTable extends Component { - constructor(props) { - super(props); - const { sortBy, descending = false } = props; - this.state = { - sortBy, - descending, - }; - } - - sort = (columns, data, sortBy, descending) => { - const { onSort = () => {} } = this.props; - const tableData = sortData(columns, data, sortBy, descending); - onSort({ - sortBy, - descending, - tableData, - }); - return tableData; - }; - - sortTableData = memoize((columns, data, sortBy, descending) => - sortBy ? this.sort(columns, data, sortBy, descending) : data, - ); - - sortStateHasChanged(nextState) { - return ( - nextState.sortBy !== this.state.sortBy || - nextState.descending !== this.state.descending - ); - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - !equal(nextProps, this.props) || this.sortStateHasChanged(nextState) - ); - } - - tableHeaderClicked(columnKey) { - this.setState(prevState => { - const descending = - columnKey === prevState.sortBy ? !prevState.descending : false; - - return { - sortBy: columnKey, - descending, - }; - }); - } - - handleKeyPress(columnKey, event) { - if (event.key === 'Enter' || event.key === ' ') { - this.tableHeaderClicked(columnKey); - event.preventDefault(); - } - } - - getAriaSort(column) { - if (this.state.sortBy !== column.key) { - return 'none'; - } - return this.state.descending ? 'descending' : 'ascending'; - } - - decorateSortableTableHeader(column) { - if (column.header === '') { - return ; - } - if (column.notSortable) { - return column.header; - } - - return ( - this.handleKeyPress(column.key, event)} - className={classNames('ffe-sortable-table__header', { - 'ffe-sortable-table__header--active': - this.state.sortBy === column.key, - })} - role="button" - onClick={this.tableHeaderClicked.bind(this, column.key)} - > - {column.header} - - - ); - } - - render() { - const sortableColumns = this.props.columns.map(column => { - return { - ...column, - ariaSort: this.getAriaSort(column), - header: this.decorateSortableTableHeader(column), - }; - }); - - let { caption } = this.props; - const { - expandedContentMapper, - condensed, - columnLayoutMobile, - smallHeader, - alignLeft, - breakpoint, - limit, - offset, - srOnlyCaption, - className, - rowRender, - headerRender, - footerRender, - columns, - data, - } = this.props; - const { sortBy, descending } = this.state; - - if (srOnlyCaption && sortBy) { - caption = caption.concat( - ' ', - sortBy, - ' ', - descending ? 'descending' : 'ascending', - ); - } - - const tableData = this.sortTableData(columns, data, sortBy, descending); - - return ( -
- ); - } -} - -SortableTable.propTypes = { - caption: PropTypes.node, - srOnlyCaption: PropTypes.bool, - expandedContentMapper: PropTypes.func, - offset: PropTypes.number, - limit: PropTypes.number, - columnLayoutMobile: PropTypes.bool, - condensed: PropTypes.bool, - smallHeader: PropTypes.bool, - alignLeft: PropTypes.bool, - breakpoint: PropTypes.oneOf(['sm', 'none']), - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - data: PropTypes.arrayOf(PropTypes.object).isRequired, - sortBy: PropTypes.string, - descending: PropTypes.bool, - onSort: PropTypes.func, - rowRender: PropTypes.func, - headerRender: PropTypes.func, - footerRender: PropTypes.func, - className: PropTypes.string, -}; - -export default SortableTable; diff --git a/packages/ffe-tables-react/src/SortableTable/SortableTable.spec.js b/packages/ffe-tables-react/src/SortableTable/SortableTable.spec.js deleted file mode 100644 index d9c8afffe2..0000000000 --- a/packages/ffe-tables-react/src/SortableTable/SortableTable.spec.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import SortableTable from './SortableTable'; -import { render, screen } from '@testing-library/react'; - -describe('', () => { - const columns = [ - { header: 'Name', key: 'name' }, - { header: 'Age', key: 'age' }, - { header: 'Employee-id', key: 'id' }, - { header: '', key: 'button' }, - ]; - - const data = [ - { - name: 'Zombie Mountain', - age: 36, - id: 2, - button: , - }, - { - name: 'Daenerys Targaryen', - age: 16, - id: 4, - button: , - }, - { name: 'Ned Stark', age: 48, id: 3, button: }, - { name: 'Jon Snow', age: 20, id: 1, button: }, - ]; - - it('should show right amount of th tags', () => { - render(); - expect(screen.getAllByRole('columnheader')).toHaveLength( - columns.length, - ); - }); - - it('should render a row for each data array entry', () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('tbody tr')).toHaveLength( - data.length, - ); - }); - - it('should set data-th property on each cell rendered', () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('[data-th]')).toHaveLength( - data.length * columns.length, - ); - }); - - it('should add `aria-sort` property', () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('[aria-sort]')).toHaveLength( - columns.length, - ); - }); - - it('should not show sorting arrow if header is blank string', () => { - const { container } = render( - , - ); - expect( - container.querySelectorAll('.ffe-sortable-table__sort-arrow') - .length, - ).toBe(columns.length - 1); - }); - - it('should show buttons in table when passing in buttons in data', () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('button').length).toBe(data.length); - }); - - describe('condensed', () => { - it('should by default not be condensed', () => { - const { container } = render( - , - ); - expect( - container.querySelectorAll('.ffe-table--condensed'), - ).toHaveLength(0); - }); - - it('can be condensed', () => { - const { container } = render( - , - ); - expect( - container.querySelectorAll('.ffe-table--condensed'), - ).toHaveLength(1); - }); - }); -}); diff --git a/packages/ffe-tables-react/src/SortableTable/sort-data.js b/packages/ffe-tables-react/src/SortableTable/sort-data.js deleted file mode 100644 index c9ddd885fa..0000000000 --- a/packages/ffe-tables-react/src/SortableTable/sort-data.js +++ /dev/null @@ -1,29 +0,0 @@ -export default (columns, data, sortBy, descending) => { - const sortedData = [...data]; - - let compare = (data1, data2) => { - if (!isNaN(data1[sortBy])) { - return data1[sortBy] - data2[sortBy]; - } - if (!data1[sortBy]) { - return data2[sortBy] ? 1 : 0; - } - return data1[sortBy].localeCompare(data2[sortBy]); - }; - - const column = columns.find(col => col.key === sortBy); - const customCompare = - column && typeof column.compare === 'function' ? column.compare : null; - - if (customCompare) { - compare = (data1, data2) => customCompare(data1[sortBy], data2[sortBy]); - } - - sortedData.sort(compare); - - if (descending) { - sortedData.reverse(); - } - - return sortedData; -}; diff --git a/packages/ffe-tables-react/src/SortableTable/sort-data.spec.js b/packages/ffe-tables-react/src/SortableTable/sort-data.spec.js deleted file mode 100644 index 52caeadc74..0000000000 --- a/packages/ffe-tables-react/src/SortableTable/sort-data.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import sortData from './sort-data'; - -const columns = [ - { key: 'name', header: 'Name' }, - { key: 'age', header: 'Age' }, - { key: 'id', header: 'Employee-id' }, - { - key: 'rand', - header: 'A random number with thousand separator needs a custom compare', - compare: (a, b) => a.replace(/\s/g, '') - b.replace(/\s/g, ''), - }, -]; - -const data = [ - { name: 'Zombie Mountain', age: 36, id: 2, rand: '69 873' }, - { name: 'Daenerys Targaryen', age: 16, id: 4, rand: '9 156' }, - { name: 'Ned Stark', age: 48, id: 3, rand: '3 863 914' }, - { name: 'Jon Snow', age: 20, id: 1, rand: '145 678' }, -]; - -describe('sortData', () => { - it('should sort data in descending order', () => { - const ascendingTableData = sortData(columns, data, 'age', true); - for (let i = 1; i > ascendingTableData.length; i++) { - expect(ascendingTableData[i].id).toBeLessThan( - ascendingTableData[i - 1].id, - ); - } - }); - - it('should sort data in ascending order', () => { - const ascendingTableData = sortData(columns, data, 'age', false); - for (let i = 1; i > ascendingTableData.length; i++) { - expect(ascendingTableData[i].id).toBeGreaterThan( - ascendingTableData[i - 1].id, - ); - } - }); - - it('should sort data based on `sortBy` parameter', () => { - const tableData = sortData(columns, data, 'age', true); - expect(tableData[0].age).toBe(48); - expect(tableData[1].age).toBe(36); - expect(tableData[2].age).toBe(20); - expect(tableData[3].age).toBe(16); - }); - - it('should sort data even though an attribute is not set', () => { - data[3] = { name: 'Jon Snow', id: 1, rand: '145 678' }; // no age key - const tableData = sortData(columns, data, 'age', false); - expect(tableData[0].age).toBe(16); - expect(tableData[1].age).toBe(36); - expect(tableData[2].age).toBe(48); - expect(tableData[3].age).toBe(undefined); - }); - - it('should sort data with a custom compare function when provided', () => { - const tableData = sortData(columns, data, 'rand', false); - expect(tableData[0].rand).toBe('9 156'); - expect(tableData[1].rand).toBe('69 873'); - expect(tableData[2].rand).toBe('145 678'); - expect(tableData[3].rand).toBe('3 863 914'); - }); -}); diff --git a/packages/ffe-tables-react/src/Table.js b/packages/ffe-tables-react/src/Table.js deleted file mode 100644 index 8b2b6d2c99..0000000000 --- a/packages/ffe-tables-react/src/Table.js +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { - node, - bool, - func, - number, - oneOf, - oneOfType, - arrayOf, - shape, - string, -} from 'prop-types'; -import DefaultTable from './DefaultTable/DefaultTable'; -import SortableTable from './SortableTable/SortableTable'; - -const Table = props => { - const { sortable, ...rest } = props; - if (sortable) { - return ; - } - return ; -}; - -Table.propTypes = { - caption: node, - /** Make the caption only visible for screen readers */ - srOnlyCaption: bool, - /** A mapping function that returns a renderable node that will be rendered for each expandable row */ - expandedContentMapper: func, - sortable: bool, - /** A string reference to a column key to set an initial sort of the table */ - sortBy: string, - /** Used in conjunction with sortBy */ - descending: bool, - /** When paginated, lets you set the number of rows to offset from the beginning */ - offset: number, - /** Limits the number of rows to show on one page. Decides whether or not to paginate your table */ - limit: number, - /** Makes your table condensed - for pages that show a lot of data */ - condensed: bool, - /** Set to true if you want the row headings to be narrower on smalls screens */ - smallHeader: bool, - /** Removes left cell padding from the first column */ - alignLeft: bool, - /** Show your table as a regular column based table on small devices */ - columnLayoutMobile: bool, - /** Decide if your table rows should break into a vertical view on small or no devices */ - breakpoint: oneOf(['sm', 'none']), - /** - * Override rendering of rows with a prop - * rowRender={(props, rowcontent, trprops, index)=>{return {rowcontent}}} - */ - rowRender: func, - /** - * Override rendering of headers with a prop - * headerRender={(props, rowcontent, trprops) => {return {rowcontent}}} - */ - headerRender: func, - /** - * Override rendering of footer with a prop - * footerRender={(props, rowcontent, trprops) => {return {rowcontent}}} - */ - footerRender: func, - /** - * An array of objects with keys equal to the columns array's key value, and value equal to whatever you - * want in your cell. These are basically an array of rows - **/ - data: arrayOf( - shape({ - /** A unique ID for each row of data. Required for sorting */ - id: oneOfType([string, number]), - }), - ), - /** An array of objects with a key describing your column, and a header describing that column */ - columns: arrayOf( - shape({ - /** The column header. Required, but may be an empty string. */ - header: node.isRequired, - /** The column key used in the `data` array's objects */ - key: string.isRequired, - /** Content shown in the table footer */ - footer: node, - alignRight: bool, - alignTop: bool, - hideOnDesktop: bool, - hideOnTablet: bool, - hideOnSmallTablet: bool, - hideOnMobile: bool, - /** Custom comparator function for sorting this column */ - compare: func, - /** Disable column from sorting. Not needed if heading is an empty string */ - notSortable: bool, - cellRender: func, - columnHeaderRender: func, - columnFooterRender: func, - }).isRequired, - ), - /** Class names set in addition to the ffe tables classes. */ - className: string, -}; - -export default Table; diff --git a/packages/ffe-tables-react/src/Table.tsx b/packages/ffe-tables-react/src/Table.tsx new file mode 100644 index 0000000000..6af583a3ab --- /dev/null +++ b/packages/ffe-tables-react/src/Table.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import classNames from 'classnames'; +import { + TableProvider, + TableContext, + TableContextInterFace, +} from './TableContext'; + +export interface TableProps + extends Omit, 'children'> { + children: + | React.ReactNode + | (({ + activeSortOrder, + activeSortKey, + }: { + activeSortOrder?: TableContextInterFace['activeSortOrder']; + activeSortKey?: TableContextInterFace['activeSortKey']; + }) => React.ReactNode); +} + +export const Table = React.forwardRef( + ({ className, children, ...rest }, ref) => { + return ( + + + {({ activeSortOrder, activeSortKey }) => ( +
+ {typeof children === 'function' + ? children({ activeSortOrder, activeSortKey }) + : children} +
+ )} + + + ); + }, +); diff --git a/packages/ffe-tables-react/src/TableBody.tsx b/packages/ffe-tables-react/src/TableBody.tsx new file mode 100644 index 0000000000..c5cbeddc78 --- /dev/null +++ b/packages/ffe-tables-react/src/TableBody.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import classNames from 'classnames'; + +export interface TableBodyProps + extends React.ComponentPropsWithoutRef<'tbody'> {} + +export const TableBody: React.FC = ({ className, ...rest }) => { + return ( +
+ ); +}); diff --git a/packages/ffe-tables-react/src/TableContext.tsx b/packages/ffe-tables-react/src/TableContext.tsx new file mode 100644 index 0000000000..0caeb2b134 --- /dev/null +++ b/packages/ffe-tables-react/src/TableContext.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { SortOrder, ariaSortOrders } from './types'; + +export interface TableContextInterFace { + activeSortOrder?: SortOrder; + activeSortKey?: string; + updateSort: (sortKey: string) => void; + getSort: (sortKey: string) => SortOrder; +} + +export const TableContext = React.createContext({ + updateSort: () => null, + getSort: () => 'none', +}); + +const getNextSort = (sort: SortOrder = 'none'): SortOrder => { + return sort + ? ariaSortOrders[ + (ariaSortOrders.indexOf(sort) + 1) % ariaSortOrders.length + ] + : 'none'; +}; + +export const TableProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [activeSortOrder, setActiveSortOrder] = useState('none'); + const [activeSortKey, setActiveSortKey] = useState(); + + const updateSort = (sortKey: string) => { + if (sortKey === activeSortKey) { + setActiveSortOrder(getNextSort(activeSortOrder)); + } else { + setActiveSortKey(sortKey); + setActiveSortOrder(getNextSort('none')); + } + }; + + const getSort = (sortKey: string): SortOrder => { + if (sortKey === activeSortKey) { + return activeSortOrder; + } + return 'none'; + }; + + return ( + + {children} + + ); +}; diff --git a/packages/ffe-tables-react/src/TableDataCell.tsx b/packages/ffe-tables-react/src/TableDataCell.tsx new file mode 100644 index 0000000000..31b6846162 --- /dev/null +++ b/packages/ffe-tables-react/src/TableDataCell.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import classNames from 'classnames'; + +export interface TableDataCellProps + extends React.ComponentPropsWithoutRef<'td'> {} + +export const TableDataCell = React.forwardRef< + HTMLTableCellElement, + TableDataCellProps +>(({ className, ...rest }, ref) => { + return ( +
+ ); +}); diff --git a/packages/ffe-tables-react/src/TableFoot.tsx b/packages/ffe-tables-react/src/TableFoot.tsx new file mode 100644 index 0000000000..75b8366e9f --- /dev/null +++ b/packages/ffe-tables-react/src/TableFoot.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import classNames from 'classnames'; + +export interface TableFootProps + extends React.ComponentPropsWithoutRef<'tfoot'> {} + +export const TableFoot: React.FC = ({ className, ...rest }) => { + return ( +
{ + if (!sortButtonRef.current?.contains(e.target as Node)) { + sortButtonRef.current?.click(); + } + onClick?.(e); + }} + ref={ref} + {...rest} + > +
+ {sortKey ? ( + + ) : ( + children + )} +
+
- - {children} - - ; - } - - const className = classNames( - { 'ffe-table--hide-sm': column.hideOnMobile }, - { 'ffe-table--hide-md': column.hideOnSmallTablet }, - { 'ffe-table--hide-lg': column.hideOnTablet }, - { 'ffe-table--hide-xlg': column.hideOnDesktop }, - ); - - let dataTh = ''; - const columnHeader = column.header; - if (columnHeader) { - if (typeof columnHeader === 'string') { - dataTh = columnHeader; - } else if (columnHeader.props && columnHeader.props.children) { - dataTh = React.Children.toArray(columnHeader.props.children)[0]; - } - } - - const tdProps = { - className: classNames( - 'ffe-table__cell', - { 'ffe-table__cell--top': column.alignTop }, - className, - ), - 'data-th': dataTh, - key: index, - }; - - const spanProps = { - className: classNames('ffe-table__content', { - 'ffe-table__content--text-right': column.alignRight, - }), - }; - - if (typeof column.columnFooterRender === 'function') { - return column.columnFooterRender( - column.footer, - dataWindow, - tdProps, - spanProps, - columns, - index, - ); - } - - return ( - - {column.footer} - - ); - }); - - const trProps = { className: 'ffe-table__row', children: footerContent }; - - const trDefault =
; - } - - const thProps = { - key: index, - className: classNames( - 'ffe-table__heading', - { 'ffe-table--hide-sm': column.hideOnMobile }, - { 'ffe-table--hide-md': column.hideOnSmallTablet }, - { 'ffe-table--hide-lg': column.hideOnTablet }, - { 'ffe-table--hide-xlg': column.hideOnDesktop }, - ), - 'aria-sort': column.ariaSort, - scope: 'col', - }; - - const spanProps = { - className: classNames('ffe-table__content', { - 'ffe-table__content--text-right': column.alignRight, - }), - }; - - const thContentDefault = {column.header}; - - if (typeof column.columnHeaderRender === 'function') { - return column.columnHeaderRender( - column.header, - dataWindow, - spanProps, - thProps, - columns, - index, - ); - } - - return ( - - {thContentDefault} -
-
{this.state.expanded && children}
-
+ +
+ setIsAnimating(false)} + > +
+ {expandContent} +
+
+