Skip to content

Commit

Permalink
feat: sticky header implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
kostasdano committed Apr 29, 2024
1 parent ba1a9b6 commit 7e20426
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 26 deletions.
6 changes: 4 additions & 2 deletions src/components/Table/Table.style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SerializedStyles } from '@emotion/react';
import type { CSSObject, SerializedStyles } from '@emotion/react';
import { css } from '@emotion/react';
import type { Theme } from 'theme';

Expand All @@ -13,7 +13,7 @@ export const tableContainer =
`;
};

export const tableStyles = (): SerializedStyles => {
export const tableStyles = ({ sx }: { sx?: CSSObject }): SerializedStyles => {
return css`
width: 100%;
border-collapse: collapse;
Expand All @@ -26,5 +26,7 @@ export const tableStyles = (): SerializedStyles => {
tbody > tr:last-child > td {
border-bottom: none;
}
${sx};
`;
};
28 changes: 21 additions & 7 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { flexRender } from '@tanstack/react-table';
import React from 'react';
import React, { useRef } from 'react';
import isEqual from 'react-fast-compare';

import type { TableProps } from '.';
Expand All @@ -13,11 +13,24 @@ const Table = <TData,>({
rowSize = 'sm',
columnsConfig,
sorting,
hasStickyHeader = false,
sx,
}: TableProps<TData>) => {
const { columnVisibility, setColumnVisibility } = columnsConfig ?? {};

const hasColumnVisibilityConfig = Boolean(columnVisibility && setColumnVisibility);

/** If true, the scrollbar of tbody is visible */
const [hasScrollbar, setHasScrollbar] = React.useState(false);

const tBodyRef = useRef<HTMLTableSectionElement>();

React.useEffect(() => {
if (tBodyRef?.current) {
setHasScrollbar(tBodyRef?.current?.scrollHeight > tBodyRef?.current?.clientHeight);
}
}, [tBodyRef.current]);

const table = useTable<TData>({
data,
columns,
Expand All @@ -34,10 +47,10 @@ const Table = <TData,>({
return (
<div css={tableContainer()}>
{hasColumnVisibilityConfig && <TTitle columnsConfig={columnsConfig} columns={columns} />}
<table css={tableStyles()}>
<THead>
<table css={tableStyles({ sx: sx?.table })}>
<THead hasStickyHeader={hasStickyHeader} hasScrollbar={hasScrollbar} sx={sx?.thead}>
{table.getHeaderGroups().map((headerGroup) => (
<TR key={headerGroup.id}>
<TR key={headerGroup.id} sx={sx?.tr}>
{headerGroup.headers.map((header) => (
<TH
key={header.id}
Expand All @@ -49,6 +62,7 @@ const Table = <TData,>({
onSort: header.column.toggleSorting,
isMultiSortable: header.column.getCanMultiSort(),
})}
sx={sx?.th}
>
{header.isPlaceholder
? null
Expand All @@ -58,13 +72,13 @@ const Table = <TData,>({
</TR>
))}
</THead>
<TBody>
<TBody hasStickyHeader={hasStickyHeader} ref={tBodyRef} sx={sx?.tbody}>
{table.getRowModel().rows.map((row) => {
return (
<TR key={row.id}>
<TR key={row.id} sx={sx?.tr}>
{row.getVisibleCells().map((cell) => {
return (
<TD key={cell.id} rowSize={rowSize}>
<TD key={cell.id} rowSize={rowSize} width={cell.column.getSize()} sx={sx?.td}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TD>
);
Expand Down
22 changes: 22 additions & 0 deletions src/components/Table/components/TBody/TBody.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { CSSObject, SerializedStyles } from '@emotion/react';
import { css } from '@emotion/react';

import type { TBodyProps } from './TBody';

export const tBodyContainer =
({ hasStickyHeader, sx }: Pick<TBodyProps, 'hasStickyHeader'> & { sx?: CSSObject }) =>
(): SerializedStyles => {
return css`
${hasStickyHeader &&
`
display: block;
overflow-y: auto;
tr {
display: flex;
}
`}
${sx};
`;
};
22 changes: 20 additions & 2 deletions src/components/Table/components/TBody/TBody.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import type { CSSObject } from '@emotion/react';
import React from 'react';
import isEqual from 'react-fast-compare';

const TBody: React.FCC = ({ children }) => {
return <tbody>{children}</tbody>;
import { tBodyContainer } from './TBody.style';
import type { TableProps } from 'components/Table/types';

export type TBodyProps = Pick<TableProps<any>, 'hasStickyHeader'> & {
children?: React.ReactNode;
/** Style overrides */
sx?: CSSObject;
};

const TBody = React.forwardRef<HTMLTableSectionElement, TBodyProps>(
({ hasStickyHeader, sx, children }, ref) => {
return (
<tbody ref={ref} css={tBodyContainer({ hasStickyHeader, sx })}>
{children}
</tbody>
);
}
);

TBody.displayName = 'TBody';

export default React.memo(TBody, isEqual);
19 changes: 17 additions & 2 deletions src/components/Table/components/TD/TD.style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SerializedStyles, Theme } from '@emotion/react';
import type { CSSObject, SerializedStyles, Theme } from '@emotion/react';
import { css } from '@emotion/react';

import type { RowSize, TableProps } from 'components/Table';
Expand All @@ -17,14 +17,29 @@ export const getMinHeight = (rowSize: RowSize) => (theme: Theme) => {
};

export const tdContainer =
({ rowSize }: Pick<TableProps<any>, 'rowSize'>) =>
({
rowSize,
width,
sx,
}: Pick<TableProps<any>, 'rowSize'> & { width?: number; isLastCell?: boolean; sx?: CSSObject }) =>
(theme: Theme): SerializedStyles => {
const getWidth = () => {
if (width) {
return `${width}%`;
}

return '100%';
};

return css`
width: ${getWidth()};
height: ${getMinHeight(rowSize)(theme)};
padding: 8px 16px;
border-bottom: 1px solid ${theme.tokens.colors.get('borderColor.decorative.default')};
box-sizing: border-box;
align-content: center;
${sx};
`;
};

Expand Down
9 changes: 7 additions & 2 deletions src/components/Table/components/TD/TD.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSObject } from '@emotion/react';
import type { RowSize } from 'index';
import React from 'react';
import isEqual from 'react-fast-compare';
Expand All @@ -9,11 +10,15 @@ type Props = {
colSpan?: number;
/** Size of Row */
rowSize?: RowSize;
/** The width of the cell */
width?: number;
/** Style overrides */
sx?: CSSObject;
};

const TD: React.FCC<Props> = ({ colSpan, rowSize = 'sm', children, ...rest }) => {
const TD: React.FCC<Props> = ({ colSpan, rowSize = 'sm', width, sx, children, ...rest }) => {
return (
<td css={tdContainer({ rowSize })} colSpan={colSpan} {...rest}>
<td css={tdContainer({ rowSize, width, sx })} colSpan={colSpan} {...rest}>
<div css={tdContent()}>{children}</div>
</td>
);
Expand Down
10 changes: 5 additions & 5 deletions src/components/Table/components/TH/TH.style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SerializedStyles, Theme } from '@emotion/react';
import type { CSSObject, SerializedStyles, Theme } from '@emotion/react';
import { css } from '@emotion/react';

import { getMinHeight } from '../TD/TD.style';
Expand All @@ -8,18 +8,18 @@ import { generateStylesFromTokens } from 'components/Typography/utils';
/** @TODO replace all css with tokens */

export const thContainer =
({ rowSize, width }: Pick<TableProps<any>, 'rowSize'> & { width?: number }) =>
({ rowSize, width, sx }: Pick<TableProps<any>, 'rowSize'> & { width?: number; sx?: CSSObject }) =>
(theme: Theme): SerializedStyles => {
return css`
width: ${width ? `${width}%` : undefined};
width: ${width ? `${width}%` : '100%'};
height: ${getMinHeight(rowSize)(theme)};
align-content: center;
text-align: left;
box-sizing: border-box;
padding: 8px 16px;
border-bottom: 1px solid ${theme.tokens.colors.get('borderColor.decorative.default')};
border-right: 1px solid ${theme.tokens.colors.get('borderColor.decorative.default')};
color: ${theme.tokens.colors.get('textColor.default.secondary')};
${generateStylesFromTokens(theme.tokens.typography.get('normal.body02'))};
${sx};
`;
};
6 changes: 5 additions & 1 deletion src/components/Table/components/TH/TH.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSObject } from '@emotion/react';
import React from 'react';
import isEqual from 'react-fast-compare';
import type { DivProps } from 'utils/common';
Expand All @@ -17,6 +18,8 @@ type Props = {
onSort?: (desc?: boolean, isMulti?: boolean) => void;
/** Whether multi-sorting is enabled */
isMultiSortable?: boolean;
/** Style overrides */
sx?: CSSObject;
};

const TH: React.FCC<Props & Pick<DivProps, 'onClick'>> = ({
Expand All @@ -25,12 +28,13 @@ const TH: React.FCC<Props & Pick<DivProps, 'onClick'>> = ({
children,
onSort,
isMultiSortable,
sx,
...rest
}) => {
const isSortable = Boolean(onSort);

return (
<th css={thContainer({ rowSize, width })} {...rest}>
<th css={thContainer({ rowSize, width, sx })} {...rest}>
<div css={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div>{children}</div>
{isSortable && <THOptions onSort={onSort} isMultiSortable={isMultiSortable} />}
Expand Down
37 changes: 37 additions & 0 deletions src/components/Table/components/THead/THead.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { css, type SerializedStyles } from '@emotion/react';
import type { Theme } from 'theme';

import type { THeadProps } from './THead';

export const tHeadContainer =
({
hasStickyHeader,
hasScrollbar,
sx,
}: Pick<THeadProps, 'hasStickyHeader' | 'sx'> & { hasScrollbar?: boolean }) =>
(theme: Theme): SerializedStyles => {
return css`
box-shadow: 0 1px 0 0 ${theme.tokens.colors.get('borderColor.decorative.default')};
${hasStickyHeader &&
`
display: block;
width: calc(100%);
padding-right: ${hasScrollbar ? '8px' : '0px'};
box-sizing: border-box;
tr {
display: flex;
}
`}
${hasScrollbar &&
`
box-shadow: ${theme.tokens.boxShadow.get('2')}, 0 1px 0 0 ${theme.tokens.colors.get(
'borderColor.decorative.default'
)} ;
`}
${sx};
`;
};
15 changes: 13 additions & 2 deletions src/components/Table/components/THead/THead.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import type { CSSObject } from '@emotion/react';
import React from 'react';
import isEqual from 'react-fast-compare';

const THead: React.FCC = ({ children }) => {
return <thead>{children}</thead>;
import { tHeadContainer } from './THead.style';
import type { TableProps } from 'components/Table/types';

export type THeadProps = Pick<TableProps<any>, 'hasStickyHeader'> & {
/** Whether the tbody has a scrollbar. When true, a padding-right is added to the thead in order for the element to align with the tbody correctly */
hasScrollbar?: boolean;
/** Style overrides */
sx?: CSSObject;
};

const THead: React.FCC<THeadProps> = ({ hasStickyHeader, hasScrollbar, sx, children }) => {
return <thead css={tHeadContainer({ hasStickyHeader, hasScrollbar, sx })}>{children}</thead>;
};

export default React.memo(THead, isEqual);
11 changes: 11 additions & 0 deletions src/components/Table/components/TR/TR.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { css, type SerializedStyles } from '@emotion/react';

import type { TRProps } from './TR';

export const trContainer =
({ sx }: Pick<TRProps, 'sx'>) =>
(): SerializedStyles => {
return css`
${sx};
`;
};
12 changes: 10 additions & 2 deletions src/components/Table/components/TR/TR.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import type { CSSObject } from '@emotion/react';
import React from 'react';
import isEqual from 'react-fast-compare';

const TR: React.FCC = ({ children }) => {
return <tr>{children}</tr>;
import { trContainer } from './TR.style';

export type TRProps = {
/** Style overrides */
sx?: CSSObject;
};

const TR: React.FCC<TRProps> = ({ sx, children }) => {
return <tr css={trContainer({ sx })}>{children}</tr>;
};

export default React.memo(TR, isEqual);
2 changes: 1 addition & 1 deletion src/components/Table/hooks/useTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const getColumns = (columns: any[]) => {
columnHelper.accessor(column.id as any, {
header: column.header,
cell: (info) => info.getValue(),
size: column.width,
size: column.width ?? 'auto',
enableSorting: column.isSortable ?? false,
})
);
Expand Down
12 changes: 12 additions & 0 deletions src/components/Table/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSObject } from '@emotion/react';
import type { SortingState, OnChangeFn } from '@tanstack/react-table';

export type TableProps<TData> = {
Expand All @@ -12,6 +13,17 @@ export type TableProps<TData> = {
columnsConfig?: ColumnsConfig;
/** Sorting Configuration */
sorting?: SortingConfig;
/** Whether the table has a sticky header and scrollable tbody */
hasStickyHeader?: boolean;
/** Style overrides for Table component and subcomponents */
sx?: {
table?: CSSObject;
thead?: CSSObject;
tbody?: CSSObject;
th?: CSSObject;
tr?: CSSObject;
td?: CSSObject;
};
};

export type UseTableProps<TData> = Pick<TableProps<TData>, 'columns' | 'data' | 'sorting'> &
Expand Down

0 comments on commit 7e20426

Please sign in to comment.