Skip to content

Commit

Permalink
feat(Table): add sticky column capabilities (#2172)
Browse files Browse the repository at this point in the history
  • Loading branch information
YossiSaadi authored Jun 27, 2024
1 parent da91e70 commit a79a28e
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 94 deletions.
5 changes: 3 additions & 2 deletions packages/core/src/components/Table/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ComponentDefaultTestId } from "../../../tests/constants";
import { RowHeights, RowSizes } from "./TableConsts";
import styles from "./Table.module.scss";
import { TableProvider } from "../context/TableContext/TableContext";
import TableRoot from "./TableRoot";

export type TableLoadingStateType = "long-text" | "medium-text" | "circle" | "rectangle";

Expand Down Expand Up @@ -76,9 +77,9 @@ const Table: VibeComponent<ITableProps, HTMLDivElement> & {

return (
<TableProvider value={{ columns, dataState, emptyState, errorState, size }}>
<div ref={ref} id={id} className={classNames} data-testid={testId} role="table" style={calculatedStyle}>
<TableRoot ref={ref} id={id} className={classNames} style={calculatedStyle} data-testid={testId}>
{children}
</div>
</TableRoot>
</TableProvider>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.virtualized {
overflow: hidden;
}

.hasScroll {
--sticky-cell-box-shadow: 3px 0 4px rgba(0, 0, 0, 0.1);
}
32 changes: 32 additions & 0 deletions packages/core/src/components/Table/Table/TableRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { forwardRef } from "react";
import { ITableProps } from "./Table";
import cx from "classnames";
import { useTable } from "../context/TableContext/TableContext";
import styles from "./TableRoot.module.scss";

type TableRootProps = Pick<ITableProps, "id" | "className" | "data-testid" | "style" | "children">;

const TableRoot = forwardRef(
(
{ id, className, "data-testid": dataTestId, style, children }: TableRootProps,
ref: React.ForwardedRef<HTMLDivElement>
) => {
const { isVirtualized, scrollLeft, onTableRootScroll } = useTable();

return (
<div
ref={ref}
id={id}
className={cx(className, { [styles.virtualized]: isVirtualized, [styles.hasScroll]: scrollLeft > 0 })}
data-testid={dataTestId}
role="table"
style={style}
onScroll={onTableRootScroll}
>
{children}
</div>
);
}
);

export default TableRoot;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import Avatar from "../../../Avatar/Avatar";
import { Calendar, Doc, Status } from "../../../Icon/Icons";
import { LabelColor } from "../../../Label/LabelConstants";
import { TableVirtualizedRow } from "../../TableVirtualizedBody/TableVirtualizedBody";
import { ITableColumn } from "../Table";

export const doAndDontIconsRuleColumns = [
{
Expand Down Expand Up @@ -305,13 +307,13 @@ export const scrollTableColumns = [
}
];

export const virtualizedScrollTableData = [...new Array(5000)].map((_, index) => ({
id: index,
export const virtualizedScrollTableData: TableVirtualizedRow[] = [...new Array(5000)].map((_, index) => ({
id: index.toString(),
num: index,
text: `This is line number ${index}`
}));

export const virtualizedScrollTableColumns = [
export const virtualizedScrollTableColumns: ITableColumn[] = [
{
id: "num",
title: "#",
Expand Down
24 changes: 18 additions & 6 deletions packages/core/src/components/Table/Table/__tests__/Table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@ import TableHeader from "../../TableHeader/TableHeader";
import TableCellSkeleton from "../../TableCellSkeleton/TableCellSkeleton";
import * as TableContextModule from "../../context/TableContext/TableContext";
import { RowSizes } from "../TableConsts";
import { ITableContext } from "../../context/TableContext/TableContext.types";

function mockUseTable() {
jest.spyOn(TableContextModule, "useTable").mockImplementation(() => ({
columns: [],
emptyState: <div />,
errorState: <div />,
size: RowSizes.MEDIUM
}));
jest.spyOn(TableContextModule, "useTable").mockImplementation(
() =>
({
columns: [],
emptyState: <div />,
errorState: <div />,
size: RowSizes.MEDIUM,
scrollLeft: 0,
onTableRootScroll: jest.fn(),
headRef: { current: null },
onHeadScroll: jest.fn(),
virtualizedListRef: { current: null },
onVirtualizedListScroll: jest.fn(),
isVirtualized: false,
markTableAsVirtualized: jest.fn()
} satisfies ITableContext)
);
}

jest.mock("../../context/TableContext/TableContext", () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@
overflow: hidden;
display: flex;
align-items: center;
}

&.sticky {
z-index: 1;
position: sticky;
left: 0;
box-shadow: var(--sticky-cell-box-shadow);
}
}
5 changes: 3 additions & 2 deletions packages/core/src/components/Table/TableCell/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { ComponentDefaultTestId } from "../../../tests/constants";

export interface ITableCellProps extends VibeComponentProps {
children?: React.ReactNode;
sticky?: boolean;
}

const TableCell: VibeComponent<ITableCellProps, HTMLDivElement> = forwardRef(
({ id, className, "data-testid": dataTestId, children }, ref) => {
({ sticky, id, className, "data-testid": dataTestId, children }, ref) => {
const isSingleChild = React.Children.count(children) === 1;
const typeOfFirstChild = typeof React.Children.toArray(children)[0];
const isFirstChildString = typeOfFirstChild === "string" || typeOfFirstChild === "number";
Expand All @@ -20,7 +21,7 @@ const TableCell: VibeComponent<ITableCellProps, HTMLDivElement> = forwardRef(
<div
ref={ref}
id={id}
className={cx(styles.tableCell, className)}
className={cx(styles.tableCell, { [styles.sticky]: sticky }, className)}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.TABLE_CELL, id)}
role="cell"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@
grid-template-columns: var(--table-grid-template-columns);
position: sticky;
top: 0;
z-index: 1;
z-index: 2;
background-color: inherit;
min-width: 100%;
width: fit-content;

> * {
> [role="columnheader"] {
background-color: inherit;
}

&.virtualized {
overflow: auto;
width: auto;
scrollbar-width: none;

&::-webkit-scrollbar {
display: none;
}
}
}
10 changes: 8 additions & 2 deletions packages/core/src/components/Table/TableHeader/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ import { ITableHeaderCellProps } from "../TableHeaderCell/TableHeaderCell";
import cx from "classnames";
import { getTestId } from "../../../tests/test-ids-utils";
import { ComponentDefaultTestId } from "../../../tests/constants";
import { useTable } from "../context/TableContext/TableContext";
import useMergeRef from "../../../hooks/useMergeRef";

export interface ITableHeaderProps extends VibeComponentProps {
children?: React.ReactElement<ITableHeaderCellProps> | React.ReactElement<ITableHeaderCellProps>[];
}

const TableHeader: VibeComponent<ITableHeaderProps, HTMLDivElement> = forwardRef(
({ id, className, "data-testid": dataTestId, children }, ref) => {
const { headRef, onHeadScroll, isVirtualized } = useTable();
const mergedRef = useMergeRef(headRef, ref);

return (
<div
ref={ref}
ref={mergedRef}
id={id}
className={cx(styles.tableHeader, className)}
className={cx(styles.tableHeader, { [styles.virtualized]: isVirtualized }, className)}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.TABLE_HEADER, id)}
role="rowgroup"
onScroll={onHeadScroll}
>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
background-color: inherit;
@include focus-style();

&.sticky {
z-index: 1;
position: sticky;
left: 0;
box-shadow: var(--sticky-cell-box-shadow);
}

&:hover,
&.sortActive {
background-color: var(--primary-background-hover-color);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ITableHeaderCellProps extends VibeComponentProps {
sortState?: "asc" | "desc" | "none";
onSortClicked?: (direction: "asc" | "desc" | "none") => void;
sortButtonAriaLabel?: string;
sticky?: boolean;
}

const TableHeaderCell: VibeComponent<ITableHeaderCellProps, HTMLDivElement> = forwardRef(
Expand All @@ -34,7 +35,8 @@ const TableHeaderCell: VibeComponent<ITableHeaderCellProps, HTMLDivElement> = fo
infoContent,
icon,
sortState = "none",
sortButtonAriaLabel = "Sort"
sortButtonAriaLabel = "Sort",
sticky
},
ref
) => {
Expand All @@ -47,7 +49,11 @@ const TableHeaderCell: VibeComponent<ITableHeaderCellProps, HTMLDivElement> = fo
<div
ref={ref}
id={id}
className={cx(styles.tableHeaderCell, { [styles.sortActive]: isSortActive }, className)}
className={cx(
styles.tableHeaderCell,
{ [styles.sortActive]: isSortActive, [styles.sticky]: sticky },
className
)}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.TABLE_HEADER_CELL, id)}
role="columnheader"
onMouseOver={() => setIsHovered(true)}
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/components/Table/TableRow/TableRow.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@
height: var(--table-row-size);
display: grid;
grid-template-columns: var(--table-grid-template-columns);
min-width: 100%;
width: fit-content;

&[aria-selected="true"] > * {
&[aria-selected="true"] > [role="cell"] {
background-color: var(--primary-selected-color);
}

> [role="cell"] {
background-color: var(--primary-background-color);
}

&:hover {
> * {
> [role="cell"] {
background-color: var(--primary-background-hover-color);
}

&[aria-selected="true"] > * {
&[aria-selected="true"] > [role="cell"] {
background-color: var(--primary-selected-hover-color);
}
}
Expand Down
9 changes: 1 addition & 8 deletions packages/core/src/components/Table/TableRow/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { forwardRef, useEffect, useRef } from "react";
import React, { forwardRef, useRef } from "react";
import { VibeComponent, VibeComponentProps } from "../../../types";
import { ITableCellProps } from "../TableCell/TableCell";
import useMergeRef from "../../../hooks/useMergeRef";
import { getTestId } from "../../../tests/test-ids-utils";
import { ComponentDefaultTestId } from "../../../tests/constants";
import cx from "classnames";
import styles from "./TableRow.module.scss";
import { useTable } from "../context/TableContext/TableContext";

export interface ITableRowProps extends VibeComponentProps {
/**
Expand All @@ -19,15 +18,9 @@ export interface ITableRowProps extends VibeComponentProps {

const TableRow: VibeComponent<ITableRowProps, HTMLDivElement> = forwardRef(
({ highlighted, children, style, id, className, "data-testid": dataTestId }, ref) => {
const { rowWidth, setRowWidth } = useTable();
const componentRef = useRef(null);
const mergedRef = useMergeRef(componentRef, ref);

useEffect(() => {
if (rowWidth > 0 || !componentRef.current?.scrollWidth) return;
setRowWidth(componentRef.current.scrollWidth);
}, [rowWidth, setRowWidth]);

return (
<div
id={id}
Expand Down
Loading

0 comments on commit a79a28e

Please sign in to comment.