diff --git a/src/components/table/FillerRows.tsx b/src/components/table/FillerRows.tsx
new file mode 100644
index 00000000..5d33127b
--- /dev/null
+++ b/src/components/table/FillerRows.tsx
@@ -0,0 +1,92 @@
+import type { Row } from '@tanstack/react-table'
+import type { VirtualItem } from '@tanstack/react-virtual'
+
+import { type TableFillLevel } from './Table'
+import { Td } from './Td'
+import { Tr } from './Tr'
+
+function FillerRow({
+ columns,
+ height,
+ index,
+ stickyColumn,
+ selectable,
+ fillLevel,
+ ...props
+}: {
+ columns: unknown[]
+ height: number
+ index: number
+ stickyColumn: boolean
+ selectable?: boolean
+ fillLevel: TableFillLevel
+}) {
+ return (
+
+ |
+
+ )
+}
+
+export function FillerRows({
+ rows,
+ height,
+ position,
+ fillLevel,
+ ...props
+}: {
+ rows: Row[] | VirtualItem[]
+ columns: unknown[]
+ height: number
+ position: 'top' | 'bottom'
+ stickyColumn: boolean
+ clickable?: boolean
+ selectable?: boolean
+ fillLevel: TableFillLevel
+}) {
+ return (
+ <>
+
+
+ >
+ )
+}
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.tsx b/src/components/table/Table.tsx
similarity index 57%
rename from src/components/Table.tsx
rename to src/components/table/Table.tsx
index ff4dcffa..c6c4c67b 100644
--- a/src/components/Table.tsx
+++ b/src/components/table/Table.tsx
@@ -1,10 +1,7 @@
import { Div, type DivProps } from 'honorable'
import {
- type CSSProperties,
- type ComponentProps,
Fragment,
type MouseEvent,
- type MutableRefObject,
type Ref,
forwardRef,
useCallback,
@@ -17,7 +14,6 @@ import type {
ColumnDef,
FilterFn,
Row,
- SortDirection,
TableOptions,
} from '@tanstack/react-table'
import {
@@ -31,24 +27,32 @@ 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 usePrevious from '../hooks/usePrevious'
-import { 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 { type FillLevel, InfoOutlineIcon, Tooltip } from '../../index'
+import Button from '../Button'
+import CaretUpIcon from '../icons/CaretUpIcon'
+import EmptyState, { type EmptyStateProps } from '../EmptyState'
+import { Spinner } from '../Spinner'
+
+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[]
columns: any[]
hideHeader?: boolean
padCells?: boolean
+ fillLevel?: TableFillLevel
rowBg?: 'base' | 'raised' | 'stripes'
highlightedRowId?: string
getRowCanExpand?: any
@@ -68,13 +72,12 @@ export type TableProps = DivProps & {
hasNextPage?: boolean
fetchNextPage?: () => void
isFetchingNextPage?: boolean
- onVirtualSliceChange?: (slice: {
- start: VirtualItem | undefined
- end: VirtualItem | undefined
- }) => void
+ onVirtualSliceChange?: (slice: VirtualSlice) => void
}
-type VirtualSlice = {
+export type TableFillLevel = Exclude
+
+export type VirtualSlice = {
start: VirtualItem | undefined
end: VirtualItem | undefined
}
@@ -95,241 +98,6 @@ function getGridTemplateCols(columnDefs: ColumnDef[] = []): string {
.join(' ')
}
-const T = styled.table<{ $gridTemplateColumns: string }>(
- ({ theme, $gridTemplateColumns }) => ({
- gridTemplateColumns: $gridTemplateColumns,
- backgroundColor: theme.colors['fill-one'],
- 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)(({ theme }) => ({
- display: 'contents',
- position: 'sticky',
- top: 0,
- zIndex: 3,
- backgroundColor: theme.colors['fill-two'],
-}))
-
-const TbodyUnstyled = forwardRef<
- HTMLTableSectionElement,
- ComponentProps<'tbody'>
->((props, ref) => (
-
-
-
-))
-
-const Tbody = styled(TbodyUnstyled)(({ theme }) => ({
- display: 'contents',
- backgroundColor: theme.colors['fill-one'],
-}))
-
-const Tr = styled.tr<{
- $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,
- }) => ({
- display: 'contents',
- backgroundColor: highlighted
- ? theme.colors['fill-two']
- : selected
- ? theme.colors['fill-zero-hover']
- : raised || (selectable && !selected)
- ? theme.colors['fill-zero-selected']
- : theme.colors['fill-zero'],
-
- ...(clickable && {
- cursor: 'pointer',
-
- // highlight when hovered, but don't highlight if a child button is hovered
- '&:not(:has(button:hover)):hover': {
- backgroundColor: selectable
- ? selected
- ? theme.colors['fill-zero-hover']
- : theme.colors['fill-zero-selected']
- : theme.colors['fill-zero-hover'],
- },
- }),
- })
-)
-
-const Th = styled.th<{
- $stickyColumn: boolean
- $highlight?: boolean
- $cursor?: CSSProperties['cursor']
- $hideHeader?: boolean
-}>(
- ({
- theme,
- $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: highlight
- ? theme.colors['fill-two']
- : theme.colors['fill-one'],
- zIndex: 4,
- borderBottom: theme.borders.default,
- 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['fill-two'],
- borderBottom: hideHeader ? 'none' : theme.borders.default,
- },
- },
- '&: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.
-const Td = styled.td<{
- $firstRow?: boolean
- $loose?: boolean
- $padCells?: boolean
- $stickyColumn: boolean
- $highlight?: boolean
- $truncateColumn: boolean
- $center?: boolean
-}>(
- ({
- theme,
- $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['fill-two'] : 'inherit',
- borderTop: firstRow ? '' : theme.borders.default,
- 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'
}
@@ -354,175 +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 FillerRow({
- columns,
- height,
- index,
- stickyColumn,
- selectable,
- ...props
-}: {
- columns: unknown[]
- height: number
- index: number
- stickyColumn: boolean
- selectable?: boolean
-}) {
- return (
-
- |
-
- )
-}
-
-function FillerRows({
- rows,
- height,
- position,
- ...props
-}: {
- rows: Row[] | VirtualItem[]
- columns: unknown[]
- height: number
- position: 'top' | 'bottom'
- stickyColumn: boolean
- clickable?: boolean
- selectable?: boolean
-}) {
- return (
- <>
-
-
- >
- )
-}
-
-function useIsScrolling(
- ref: MutableRefObject,
- {
- onIsScrollingChange: onScrollingChange,
- restDelay = 350,
- }: { onIsScrollingChange: (isScrolling: boolean) => void; restDelay?: number }
-) {
- const [isScrolling, setIsScrolling] = useState(false)
- const timeout = useRef(null)
-
- useEffect(() => {
- onScrollingChange?.(isScrolling)
- }, [isScrolling, onScrollingChange])
-
- useEffect(() => {
- if (ref.current) {
- const el = ref.current
-
- const scrollHandler = () => {
- setIsScrolling(true)
- window.clearTimeout(timeout.current)
- timeout.current = window.setTimeout(() => {
- setIsScrolling(false)
- }, restDelay)
- }
-
- el.addEventListener('scroll', scrollHandler, { passive: true })
-
- return () => {
- el.removeEventListener('scroll', scrollHandler)
- }
- }
- }, [ref, restDelay])
-}
-
-function useOnVirtualSliceChange({
- virtualRows,
- virtualizeRows,
- onVirtualSliceChange,
-}: {
- virtualRows: VirtualItem[]
- virtualizeRows: boolean
- onVirtualSliceChange: (slice: VirtualSlice) => void
-}) {
- const sliceStartRow = virtualRows[0]
- const sliceEndRow: VirtualItem = virtualRows[virtualRows.length - 1]
- const prevSliceStartRow = usePrevious(virtualRows[0])
- const prevSliceEndRow = usePrevious(virtualRows[virtualRows.length - 1])
-
- useEffect(() => {
- if (
- virtualizeRows &&
- (prevSliceEndRow !== sliceEndRow || prevSliceStartRow !== sliceStartRow)
- ) {
- onVirtualSliceChange?.({ start: sliceStartRow, end: sliceEndRow })
- }
- }, [
- sliceStartRow,
- sliceEndRow,
- virtualizeRows,
- onVirtualSliceChange,
- prevSliceEndRow,
- prevSliceStartRow,
- ])
-}
-
function TableRef(
{
data,
@@ -532,6 +131,7 @@ function TableRef(
renderExpanded,
loose = false,
padCells = true,
+ fillLevel = 0,
rowBg = 'stripes',
stickyColumn = false,
scrollTopMargin = 500,
@@ -693,8 +293,10 @@ function TableRef(
ref={forwardRef}
>
{headerGroups.map((headerGroup) => (
-
+
{headerGroup.headers.map((header) => (
|
)}
{rows.map((maybeRow) => {
@@ -783,6 +390,7 @@ function TableRef(
onRowClick?.(e, row)}
+ $fillLevel={fillLevel}
$raised={raised}
$highlighted={row?.id === highlightedRowId}
$selectable={row?.getCanSelect() ?? false}
@@ -797,6 +405,7 @@ function TableRef(
{isNil(row) && isLoaderRow ? (
(
{row?.getIsExpanded() && (
- |
+
{renderExpanded({ row })}
@@ -848,6 +461,7 @@ function TableRef(
height={paddingBottom}
position="bottom"
stickyColumn={stickyColumn}
+ fillLevel={fillLevel}
/>
)}
@@ -855,7 +469,6 @@ function TableRef(
{isEmpty(rows) && (
)}
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..928503b4
--- /dev/null
+++ b/src/components/table/Td.tsx
@@ -0,0 +1,90 @@
+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 || highlight
+ ? ''
+ : 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..b9dc1d49
--- /dev/null
+++ b/src/components/table/Th.tsx
@@ -0,0 +1,78 @@
+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: highlight
+ ? undefined
+ : 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)],
+ },
+ }),
+ })
+)
diff --git a/src/components/table/colors.ts b/src/components/table/colors.ts
new file mode 100644
index 00000000..879bbbd6
--- /dev/null
+++ b/src/components/table/colors.ts
@@ -0,0 +1,89 @@
+import { type TableFillLevel } from './Table'
+
+export const tableFillLevelToBorder = {
+ 0: 'fill-two',
+ 1: 'fill-three',
+ 2: 'fill-three',
+} as const satisfies Record
+
+export const tableFillLevelToBorderColor = {
+ 0: 'border-fill-two',
+ 1: 'border-fill-three',
+ 2: 'border-fill-three',
+} as const satisfies Record
+
+export const tableFillLevelToBg = {
+ 0: 'fill-zero',
+ 1: 'fill-one',
+ 2: 'fill-two',
+} as const satisfies Record
+
+const tableFillLevelToHeaderBg = {
+ 0: 'fill-one',
+ 1: 'fill-two',
+ 2: 'fill-three',
+} as const satisfies Record
+
+const tableFillLevelToCellBg = {
+ 0: 'fill-zero',
+ 1: 'fill-one',
+ 2: 'fill-two',
+} as const satisfies Record
+
+const tableFillLevelToRaisedCellBg = {
+ 0: 'fill-zero-selected',
+ 1: 'fill-one-selected',
+ 2: 'fill-two-selected',
+} as const satisfies Record
+
+const tableFillLevelToSelectedCellBg = {
+ 0: 'fill-zero-hover',
+ 1: 'fill-one-hover',
+ 2: 'fill-two-hover',
+} as const satisfies Record
+
+export const tableFillLevelToHighlightedCellBg = {
+ 0: 'fill-two',
+ 1: 'fill-three',
+ 2: 'fill-three',
+} as const satisfies Record
+
+const tableFillLevelToHoverCellBg = {
+ 0: 'fill-zero-hover',
+ 1: 'fill-one-hover',
+ 2: 'fill-two-hover',
+} as const satisfies Record
+
+export const tableHeaderColor = (
+ fillLevel: TableFillLevel,
+ highlighted: boolean
+) =>
+ highlighted
+ ? tableFillLevelToHighlightedCellBg[fillLevel]
+ : tableFillLevelToHeaderBg[fillLevel]
+
+export const tableCellColor = (
+ fillLevel: TableFillLevel,
+ highlighted: boolean,
+ raised: boolean,
+ selectable: boolean,
+ selected: boolean
+) =>
+ highlighted
+ ? tableFillLevelToHighlightedCellBg[fillLevel]
+ : selected
+ ? tableFillLevelToSelectedCellBg[fillLevel]
+ : raised || (selectable && !selected)
+ ? tableFillLevelToRaisedCellBg[fillLevel]
+ : tableFillLevelToCellBg[fillLevel]
+
+export const tableCellHoverColor = (
+ fillLevel: TableFillLevel,
+ selectable: boolean,
+ selected: boolean
+) =>
+ selectable
+ ? selected
+ ? tableFillLevelToSelectedCellBg[fillLevel]
+ : tableFillLevelToRaisedCellBg[fillLevel]
+ : tableFillLevelToHoverCellBg[fillLevel]
diff --git a/src/components/table/hooks.ts b/src/components/table/hooks.ts
new file mode 100644
index 00000000..d6480215
--- /dev/null
+++ b/src/components/table/hooks.ts
@@ -0,0 +1,72 @@
+import type { VirtualItem } from '@tanstack/react-virtual'
+import { type MutableRefObject, useEffect, useRef, useState } from 'react'
+
+import usePrevious from '../../hooks/usePrevious'
+
+import { type VirtualSlice } from './Table'
+
+export function useIsScrolling(
+ ref: MutableRefObject,
+ {
+ onIsScrollingChange: onScrollingChange,
+ restDelay = 350,
+ }: { onIsScrollingChange: (isScrolling: boolean) => void; restDelay?: number }
+) {
+ const [isScrolling, setIsScrolling] = useState(false)
+ const timeout = useRef(null)
+
+ useEffect(() => {
+ onScrollingChange?.(isScrolling)
+ }, [isScrolling, onScrollingChange])
+
+ useEffect(() => {
+ if (ref.current) {
+ const el = ref.current
+
+ const scrollHandler = () => {
+ setIsScrolling(true)
+ window.clearTimeout(timeout.current)
+ timeout.current = window.setTimeout(() => {
+ setIsScrolling(false)
+ }, restDelay)
+ }
+
+ el.addEventListener('scroll', scrollHandler, { passive: true })
+
+ return () => {
+ el.removeEventListener('scroll', scrollHandler)
+ }
+ }
+ }, [ref, restDelay])
+}
+
+export function useOnVirtualSliceChange({
+ virtualRows,
+ virtualizeRows,
+ onVirtualSliceChange,
+}: {
+ virtualRows: VirtualItem[]
+ virtualizeRows: boolean
+ onVirtualSliceChange: (slice: VirtualSlice) => void
+}) {
+ const sliceStartRow = virtualRows[0]
+ const sliceEndRow: VirtualItem = virtualRows[virtualRows.length - 1]
+ const prevSliceStartRow = usePrevious(virtualRows[0])
+ const prevSliceEndRow = usePrevious(virtualRows[virtualRows.length - 1])
+
+ useEffect(() => {
+ if (
+ virtualizeRows &&
+ (prevSliceEndRow !== sliceEndRow || prevSliceStartRow !== sliceStartRow)
+ ) {
+ onVirtualSliceChange?.({ start: sliceStartRow, end: sliceEndRow })
+ }
+ }, [
+ sliceStartRow,
+ sliceEndRow,
+ virtualizeRows,
+ onVirtualSliceChange,
+ prevSliceEndRow,
+ prevSliceStartRow,
+ ])
+}
diff --git a/src/index.ts b/src/index.ts
index 331c5e23..81ffb56b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -63,7 +63,7 @@ export { default as Tab } from './components/Tab'
export type { TabListStateProps, TabBaseProps } from './components/TabList'
export { TabList } from './components/TabList'
export { default as TabPanel } from './components/TabPanel'
-export { default as Table } from './components/Table'
+export { default as Table } from './components/table/Table'
export { default as TipCarousel } from './components/TipCarousel'
export {
type ValidationResponse,
diff --git a/src/stories/Table.stories.tsx b/src/stories/Table.stories.tsx
index 8332fb28..d1438494 100644
--- a/src/stories/Table.stories.tsx
+++ b/src/stories/Table.stories.tsx
@@ -359,14 +359,25 @@ const extremeLengthData = Array(200)
export const Default = Template.bind({})
Default.args = {
+ fillLevel: 0,
width: '900px',
height: '400px',
data: repeatedData,
columns,
}
+export const Empty = Template.bind({})
+Empty.args = {
+ fillLevel: 0,
+ width: '900px',
+ height: '400px',
+ data: [],
+ columns,
+}
+
export const Highlighted = Template.bind({})
Highlighted.args = {
+ fillLevel: 0,
width: '900px',
height: '400px',
data: repeatedData,
@@ -396,6 +407,7 @@ Highlighted.args = {
export const VirtualizedRows = Template.bind({})
VirtualizedRows.args = {
+ fillLevel: 0,
virtualizeRows: true,
width: '900px',
height: '400px',
@@ -405,6 +417,7 @@ VirtualizedRows.args = {
export const PagedData = PagedTemplate.bind({})
PagedData.args = {
+ fillLevel: 0,
pageSize: 30,
width: '900px',
height: '400px',
@@ -415,6 +428,7 @@ PagedData.args = {
export const Loose = Template.bind({})
Loose.args = {
+ fillLevel: 0,
width: '900px',
height: '400px',
data: repeatedData,
@@ -425,6 +439,7 @@ Loose.args = {
export const Clickable = Template.bind({})
Clickable.args = {
+ fillLevel: 0,
width: '900px',
height: '400px',
data: repeatedData,
@@ -435,6 +450,7 @@ Clickable.args = {
export const StickyColumn = Template.bind({})
StickyColumn.args = {
+ fillLevel: 0,
width: '400px',
height: '400px',
data: repeatedData,
@@ -445,6 +461,7 @@ StickyColumn.args = {
export const Expandable = Template.bind({})
Expandable.args = {
+ fillLevel: 0,
width: '900px',
height: '400px',
data: repeatedData,
@@ -457,6 +474,7 @@ Expandable.args = {
export const FilterableAndSortable = FilterableTemplate.bind({})
FilterableAndSortable.args = {
+ fillLevel: 0,
virtualizeRows: true,
emptyStateProps: {
message: 'No results match your query',
@@ -470,6 +488,7 @@ FilterableAndSortable.args = {
export const Selectable = SelectableTemplate.bind({})
Selectable.args = {
+ fillLevel: 0,
width: '900px',
height: '400px',
data: repeatedData,