From feeca3eb42b674ded11e0e6cbf6a048952dce9a9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Nov 2024 16:37:53 +0100 Subject: [PATCH] split components --- src/components/table/FillerRows.tsx | 4 +- src/components/table/SortIndicator.tsx | 28 +++ src/components/table/T.tsx | 13 ++ src/components/table/Table.tsx | 283 +------------------------ src/components/table/Tbody.tsx | 20 ++ src/components/table/Td.tsx | 87 ++++++++ src/components/table/Th.tsx | 76 +++++++ src/components/table/Thead.tsx | 23 ++ src/components/table/Tr.tsx | 39 ++++ 9 files changed, 298 insertions(+), 275 deletions(-) create mode 100644 src/components/table/SortIndicator.tsx create mode 100644 src/components/table/T.tsx create mode 100644 src/components/table/Tbody.tsx create mode 100644 src/components/table/Td.tsx create mode 100644 src/components/table/Th.tsx create mode 100644 src/components/table/Thead.tsx create mode 100644 src/components/table/Tr.tsx diff --git a/src/components/table/FillerRows.tsx b/src/components/table/FillerRows.tsx index 613c2278..5d33127b 100644 --- a/src/components/table/FillerRows.tsx +++ b/src/components/table/FillerRows.tsx @@ -1,7 +1,9 @@ import type { Row } from '@tanstack/react-table' import type { VirtualItem } from '@tanstack/react-virtual' -import { type TableFillLevel, Td, Tr } from './Table' +import { type TableFillLevel } from './Table' +import { Td } from './Td' +import { Tr } from './Tr' function FillerRow({ columns, diff --git a/src/components/table/SortIndicator.tsx b/src/components/table/SortIndicator.tsx new file mode 100644 index 00000000..62abc78e --- /dev/null +++ b/src/components/table/SortIndicator.tsx @@ -0,0 +1,28 @@ +import type { SortDirection } from '@tanstack/react-table' + +import ArrowRightIcon from '../icons/ArrowRightIcon' + +export function SortIndicator({ + direction = false, +}: { + direction: false | SortDirection +}) { + switch (direction) { + case 'asc': + return ( + + ) + case 'desc': + return ( + + ) + case false: + return null + } +} diff --git a/src/components/table/T.tsx b/src/components/table/T.tsx new file mode 100644 index 00000000..0219630e --- /dev/null +++ b/src/components/table/T.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components' + +export const T = styled.table<{ $gridTemplateColumns: string }>( + ({ theme, $gridTemplateColumns }) => ({ + gridTemplateColumns: $gridTemplateColumns, + borderSpacing: 0, + display: 'grid', + borderCollapse: 'collapse', + minWidth: '100%', + width: '100%', + ...theme.partials.text.body2LooseLineHeight, + }) +) diff --git a/src/components/table/Table.tsx b/src/components/table/Table.tsx index 041ebcf1..c6c4c67b 100644 --- a/src/components/table/Table.tsx +++ b/src/components/table/Table.tsx @@ -1,7 +1,5 @@ import { Div, type DivProps } from 'honorable' import { - type CSSProperties, - type ComponentProps, Fragment, type MouseEvent, type Ref, @@ -16,7 +14,6 @@ import type { ColumnDef, FilterFn, Row, - SortDirection, TableOptions, } from '@tanstack/react-table' import { @@ -30,28 +27,25 @@ import { import { rankItem } from '@tanstack/match-sorter-utils' import type { VirtualItem } from '@tanstack/react-virtual' import { useVirtualizer } from '@tanstack/react-virtual' -import styled, { useTheme } from 'styled-components' +import { useTheme } from 'styled-components' import { isEmpty, isNil } from 'lodash-es' import { type FillLevel, InfoOutlineIcon, Tooltip } from '../../index' import Button from '../Button' import CaretUpIcon from '../icons/CaretUpIcon' -import ArrowRightIcon from '../icons/ArrowRightIcon' -import { FillLevelProvider } from '../contexts/FillLevelContext' import EmptyState, { type EmptyStateProps } from '../EmptyState' import { Spinner } from '../Spinner' -import { - tableCellColor, - tableCellHoverColor, - tableFillLevelToBg, - tableFillLevelToBorder, - tableFillLevelToBorderColor, - tableFillLevelToHighlightedCellBg, - tableHeaderColor, -} from './colors' +import { tableFillLevelToBg, tableFillLevelToBorderColor } from './colors' import { FillerRows } from './FillerRows' import { useIsScrolling, useOnVirtualSliceChange } from './hooks' +import { SortIndicator } from './SortIndicator' +import { T } from './T' +import { Tbody } from './Tbody' +import { Td, TdExpand, TdLoading } from './Td' +import { Th } from './Th' +import { Thead } from './Thead' +import { Tr } from './Tr' export type TableProps = DivProps & { data: any[] @@ -104,240 +98,6 @@ function getGridTemplateCols(columnDefs: ColumnDef[] = []): string { .join(' ') } -const T = styled.table<{ $gridTemplateColumns: string }>( - ({ theme, $gridTemplateColumns }) => ({ - gridTemplateColumns: $gridTemplateColumns, - borderSpacing: 0, - display: 'grid', - borderCollapse: 'collapse', - minWidth: '100%', - width: '100%', - ...theme.partials.text.body2LooseLineHeight, - }) -) - -const TheadUnstyled = forwardRef< - HTMLTableSectionElement, - ComponentProps<'thead'> ->((props, ref) => ( - - - -)) - -const Thead = styled(TheadUnstyled)(() => ({ - display: 'contents', - position: 'sticky', - top: 0, - zIndex: 3, -})) - -const TbodyUnstyled = forwardRef< - HTMLTableSectionElement, - ComponentProps<'tbody'> ->((props, ref) => ( - - - -)) - -const Tbody = styled(TbodyUnstyled)(() => ({ - display: 'contents', -})) - -export const Tr = styled.tr<{ - $fillLevel: TableFillLevel - $highlighted?: boolean - $selected?: boolean - $selectable?: boolean - $clickable?: boolean - $raised?: boolean -}>( - ({ - theme, - $clickable: clickable = false, - $raised: raised = false, - $selectable: selectable = false, - $selected: selected = false, - $highlighted: highlighted = false, - $fillLevel: fillLevel, - }) => ({ - display: 'contents', - backgroundColor: - theme.colors[ - tableCellColor(fillLevel, highlighted, raised, selectable, selected) - ], - - ...(clickable && { - cursor: 'pointer', - - // highlight when hovered, but don't highlight if a child button is hovered - '&:not(:has(button:hover)):hover': { - backgroundColor: - theme.colors[tableCellHoverColor(fillLevel, selectable, selected)], - }, - }), - }) -) - -const Th = styled.th<{ - $fillLevel: TableFillLevel - $stickyColumn: boolean - $highlight?: boolean - $cursor?: CSSProperties['cursor'] - $hideHeader?: boolean -}>( - ({ - theme, - $fillLevel: fillLevel, - $stickyColumn: stickyColumn, - $highlight: highlight, - $cursor: cursor, - $hideHeader: hideHeader, - }) => ({ - padding: 0, - position: 'sticky', - top: 0, - zIndex: 4, - '.thOuterWrap': { - alignItems: 'center', - display: hideHeader ? 'none' : 'flex', - position: 'relative', - backgroundColor: theme.colors[tableHeaderColor(fillLevel, highlight)], - zIndex: 4, - borderBottom: theme.borders[tableFillLevelToBorder[fillLevel]], - color: theme.colors.text, - height: 48, - minHeight: 48, - whiteSpace: 'nowrap', - padding: '0 12px', - textAlign: 'left', - ...(cursor ? { cursor } : {}), - '.thSortIndicatorWrap': { - display: 'flex', - gap: theme.spacing.xsmall, - }, - }, - '&:last-child': { - /* Hackery to hide unpredictable visible gap between columns */ - zIndex: 3, - '&::before': { - content: '""', - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - width: 10000, - backgroundColor: theme.colors[tableHeaderColor(fillLevel, false)], - borderBottom: hideHeader - ? 'none' - : theme.borders[tableFillLevelToBorder[fillLevel]], - }, - }, - '&:first-child': { - ...(stickyColumn - ? { - backgroundColor: 'inherit', - position: 'sticky', - left: 0, - zIndex: 5, - '.thOuterWrap': { - boxShadow: theme.boxShadows.slight, - zIndex: 5, - }, - } - : {}), - }, - }) -) - -// TODO: Set vertical align to top for tall cells (~3 lines of text or more). See ENG-683. -export const Td = styled.td<{ - $fillLevel: TableFillLevel - $firstRow?: boolean - $loose?: boolean - $padCells?: boolean - $stickyColumn: boolean - $highlight?: boolean - $truncateColumn: boolean - $center?: boolean -}>( - ({ - theme, - $fillLevel: fillLevel, - $firstRow: firstRow, - $loose: loose, - $padCells: padCells, - $stickyColumn: stickyColumn, - $highlight: highlight, - $truncateColumn: truncateColumn = false, - $center: center, - }) => ({ - ...theme.partials.text.body2LooseLineHeight, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: center ? 'center' : 'flex-start', - height: 'auto', - minHeight: 52, - - backgroundColor: highlight - ? theme.colors[tableFillLevelToHighlightedCellBg[fillLevel]] - : 'inherit', - borderTop: firstRow ? '' : theme.borders[tableFillLevelToBorder[fillLevel]], - color: theme.colors['text-light'], - - padding: padCells ? (loose ? '16px 12px' : '8px 12px') : 0, - '&:first-child': stickyColumn - ? { - boxShadow: theme.boxShadows.slight, - position: 'sticky', - left: 0, - zIndex: 1, - } - : {}, - ...(truncateColumn - ? { - '*': { - width: '100%', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - } - : {}), - }) -) - -const TdExpand = styled.td(({ theme }) => ({ - '&:last-child': { - gridColumn: '2 / -1', - }, - backgroundColor: 'inherit', - color: theme.colors['text-light'], - height: 'auto', - minHeight: 52, - padding: '16px 12px', -})) - -const TdLoading = styled(Td)(({ theme }) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gridColumn: '1 / -1', - textAlign: 'center', - gap: theme.spacing.xsmall, - color: theme.colors['text-xlight'], - minHeight: theme.spacing.large * 2 + theme.spacing.xlarge, -})) - function isRow(row: Row | VirtualItem): row is Row { return typeof (row as Row).getVisibleCells === 'function' } @@ -362,31 +122,6 @@ const defaultGlobalFilterFn: FilterFn = ( return itemRank.passed } -const sortDirToIcon = { - asc: ( - - ), - desc: ( - - ), -} - -function SortIndicator({ - direction = false, -}: { - direction: false | SortDirection -}) { - if (!direction) return null - - return sortDirToIcon[direction] -} - function TableRef( { data, diff --git a/src/components/table/Tbody.tsx b/src/components/table/Tbody.tsx new file mode 100644 index 00000000..68080808 --- /dev/null +++ b/src/components/table/Tbody.tsx @@ -0,0 +1,20 @@ +import { type ComponentProps, forwardRef } from 'react' +import styled from 'styled-components' + +import { FillLevelProvider } from '../contexts/FillLevelContext' + +const TbodyUnstyled = forwardRef< + HTMLTableSectionElement, + ComponentProps<'tbody'> +>((props, ref) => ( + + + +)) + +export const Tbody = styled(TbodyUnstyled)(() => ({ + display: 'contents', +})) diff --git a/src/components/table/Td.tsx b/src/components/table/Td.tsx new file mode 100644 index 00000000..d791cfb5 --- /dev/null +++ b/src/components/table/Td.tsx @@ -0,0 +1,87 @@ +import styled from 'styled-components' + +import { + tableFillLevelToBorder, + tableFillLevelToHighlightedCellBg, +} from './colors' +import { type TableFillLevel } from './Table' + +export const Td = styled.td<{ + $fillLevel: TableFillLevel + $firstRow?: boolean + $loose?: boolean + $padCells?: boolean + $stickyColumn: boolean + $highlight?: boolean + $truncateColumn: boolean + $center?: boolean +}>( + ({ + theme, + $fillLevel: fillLevel, + $firstRow: firstRow, + $loose: loose, + $padCells: padCells, + $stickyColumn: stickyColumn, + $highlight: highlight, + $truncateColumn: truncateColumn = false, + $center: center, + }) => ({ + ...theme.partials.text.body2LooseLineHeight, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: center ? 'center' : 'flex-start', + height: 'auto', + minHeight: 52, + + backgroundColor: highlight + ? theme.colors[tableFillLevelToHighlightedCellBg[fillLevel]] + : 'inherit', + borderTop: firstRow ? '' : theme.borders[tableFillLevelToBorder[fillLevel]], + color: theme.colors['text-light'], + + padding: padCells ? (loose ? '16px 12px' : '8px 12px') : 0, + '&:first-child': stickyColumn + ? { + boxShadow: theme.boxShadows.slight, + position: 'sticky', + left: 0, + zIndex: 1, + } + : {}, + ...(truncateColumn + ? { + '*': { + width: '100%', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + } + : {}), + }) +) + +export const TdExpand = styled.td(({ theme }) => ({ + '&:last-child': { + gridColumn: '2 / -1', + }, + backgroundColor: 'inherit', + color: theme.colors['text-light'], + height: 'auto', + minHeight: 52, + padding: '16px 12px', +})) + +export const TdLoading = styled(Td)(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gridColumn: '1 / -1', + textAlign: 'center', + gap: theme.spacing.xsmall, + color: theme.colors['text-xlight'], + minHeight: theme.spacing.large * 2 + theme.spacing.xlarge, +})) diff --git a/src/components/table/Th.tsx b/src/components/table/Th.tsx new file mode 100644 index 00000000..a8b12af7 --- /dev/null +++ b/src/components/table/Th.tsx @@ -0,0 +1,76 @@ +import type { CSSProperties } from 'react' +import styled from 'styled-components' + +import { tableFillLevelToBorder, tableHeaderColor } from './colors' +import { type TableFillLevel } from './Table' + +export const Th = styled.th<{ + $fillLevel: TableFillLevel + $stickyColumn: boolean + $highlight?: boolean + $cursor?: CSSProperties['cursor'] + $hideHeader?: boolean +}>( + ({ + theme, + $fillLevel: fillLevel, + $stickyColumn: stickyColumn, + $highlight: highlight, + $cursor: cursor, + $hideHeader: hideHeader, + }) => ({ + padding: 0, + position: 'sticky', + top: 0, + zIndex: 4, + '.thOuterWrap': { + alignItems: 'center', + display: hideHeader ? 'none' : 'flex', + position: 'relative', + backgroundColor: theme.colors[tableHeaderColor(fillLevel, highlight)], + zIndex: 4, + borderBottom: theme.borders[tableFillLevelToBorder[fillLevel]], + color: theme.colors.text, + height: 48, + minHeight: 48, + whiteSpace: 'nowrap', + padding: '0 12px', + textAlign: 'left', + ...(cursor ? { cursor } : {}), + '.thSortIndicatorWrap': { + display: 'flex', + gap: theme.spacing.xsmall, + }, + }, + '&:last-child': { + /* Hackery to hide unpredictable visible gap between columns */ + zIndex: 3, + '&::before': { + content: '""', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + width: 10000, + backgroundColor: theme.colors[tableHeaderColor(fillLevel, false)], + borderBottom: hideHeader + ? 'none' + : theme.borders[tableFillLevelToBorder[fillLevel]], + }, + }, + '&:first-child': { + ...(stickyColumn + ? { + backgroundColor: 'inherit', + position: 'sticky', + left: 0, + zIndex: 5, + '.thOuterWrap': { + boxShadow: theme.boxShadows.slight, + zIndex: 5, + }, + } + : {}), + }, + }) +) diff --git a/src/components/table/Thead.tsx b/src/components/table/Thead.tsx new file mode 100644 index 00000000..94b41bf5 --- /dev/null +++ b/src/components/table/Thead.tsx @@ -0,0 +1,23 @@ +import { type ComponentProps, forwardRef } from 'react' +import styled from 'styled-components' + +import { FillLevelProvider } from '../contexts/FillLevelContext' + +const TheadUnstyled = forwardRef< + HTMLTableSectionElement, + ComponentProps<'thead'> +>((props, ref) => ( + + + +)) + +export const Thead = styled(TheadUnstyled)(() => ({ + display: 'contents', + position: 'sticky', + top: 0, + zIndex: 3, +})) diff --git a/src/components/table/Tr.tsx b/src/components/table/Tr.tsx new file mode 100644 index 00000000..b93d9718 --- /dev/null +++ b/src/components/table/Tr.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components' + +import { tableCellColor, tableCellHoverColor } from './colors' +import { type TableFillLevel } from './Table' + +export const Tr = styled.tr<{ + $fillLevel: TableFillLevel + $highlighted?: boolean + $selected?: boolean + $selectable?: boolean + $clickable?: boolean + $raised?: boolean +}>( + ({ + theme, + $clickable: clickable = false, + $raised: raised = false, + $selectable: selectable = false, + $selected: selected = false, + $highlighted: highlighted = false, + $fillLevel: fillLevel, + }) => ({ + display: 'contents', + backgroundColor: + theme.colors[ + tableCellColor(fillLevel, highlighted, raised, selectable, selected) + ], + + ...(clickable && { + cursor: 'pointer', + + // highlight when hovered, but don't highlight if a child button is hovered + '&:not(:has(button:hover)):hover': { + backgroundColor: + theme.colors[tableCellHoverColor(fillLevel, selectable, selected)], + }, + }), + }) +)