Skip to content

Commit

Permalink
feat: allow multiple DynamicTable per page MAASENG-3091 (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
petermakowski authored Apr 25, 2024
1 parent 30e9dfd commit 0631b2a
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 23 deletions.
11 changes: 10 additions & 1 deletion src/lib/components/DynamicTable/DynamicTable.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@
}

thead {
scrollbar-gutter: stable;
position: sticky;
top: 0;
z-index: 1;
background-color: white;
}

&.is-full-height {
thead {
scrollbar-gutter: stable;
}
}

thead tr,
Expand Down
69 changes: 69 additions & 0 deletions src/lib/components/DynamicTable/DynamicTable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default meta;
export const Example: StoryObj<typeof DynamicTable> = {
args: {
className: "machines-table",
variant: "full-height",
children: (
<>
<thead>
Expand Down Expand Up @@ -54,3 +55,71 @@ export const Example: StoryObj<typeof DynamicTable> = {
),
},
};

export const TwoTablesExample: StoryObj<typeof DynamicTable> = {
render: (args) => <div>{args.children}</div>,
args: {
children: (
<>
<DynamicTable className="first-table" variant="regular">
<thead>
<tr>
<th>FQDN</th>
<th>IP address</th>
<th>Zone</th>
<th>Owner</th>
<th>Actions</th>
</tr>
</thead>
<DynamicTable.Body>
{data.slice(0, 10).map((item) => (
<tr key={item.fqdn}>
<td>{item.fqdn}</td>
<td>{item.ipAddress}</td>
<td>{item.zone}</td>
<td>{item.owner}</td>
<td>
<Button
appearance="base"
style={{ marginBottom: 0, padding: 0 }}
>
<Icon name="delete" />
</Button>
</td>
</tr>
))}
</DynamicTable.Body>
</DynamicTable>
<DynamicTable className="second-table" variant="regular">
<thead>
<tr>
<th>FQDN</th>
<th>IP address</th>
<th>Zone</th>
<th>Owner</th>
<th>Actions</th>
</tr>
</thead>
<DynamicTable.Body>
{data.slice(10, 20).map((item) => (
<tr key={item.fqdn}>
<td>{item.fqdn}</td>
<td>{item.ipAddress}</td>
<td>{item.zone}</td>
<td>{item.owner}</td>
<td>
<Button
appearance="base"
style={{ marginBottom: 0, padding: 0 }}
>
<Icon name="delete" />
</Button>
</td>
</tr>
))}
</DynamicTable.Body>
</DynamicTable>
</>
),
},
};
25 changes: 22 additions & 3 deletions src/lib/components/DynamicTable/DynamicTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ beforeAll(() => {
} as DOMRect);
});

it("sets a fixed table body height based on top offset on large screens", async () => {
it("sets a fixed table body height based on top offset on large screens in full-height variant", async () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.xSmall);

await act(async () => {
fireEvent(window, new Event("resize"));
});

const { container } = render(
<DynamicTable>
<DynamicTable variant="full-height">
<DynamicTable.Body className="test-class">
<tr>
<td>Test content</td>
Expand All @@ -48,6 +48,7 @@ it("sets a fixed table body height based on top offset on large screens", async

// does not alter the height on small screens
expect(tbody).toHaveStyle("height: undefined");
expect(container.querySelector("table")).toHaveClass("is-full-height");

vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.large);

Expand All @@ -60,9 +61,27 @@ it("sets a fixed table body height based on top offset on large screens", async
);
});

it("does not apply dynamic height in regular variant", async () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.large);
const { container } = render(
<DynamicTable variant="regular">
<DynamicTable.Body>
<tr>
<td>Test content</td>
</tr>
</DynamicTable.Body>
</DynamicTable>,
);
await act(async () => {
fireEvent(window, new Event("resize"));
});
const tbody = container.querySelector("tbody");
await vi.waitFor(() => expect(tbody).toHaveStyle("height: undefined"));
});

it("displays loading state", () => {
const { container } = render(
<DynamicTable>
<DynamicTable variant="regular">
<DynamicTable.Loading />
</DynamicTable>,
);
Expand Down
68 changes: 50 additions & 18 deletions src/lib/components/DynamicTable/DynamicTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
useLayoutEffect,
useRef,
useCallback,
createContext,
useContext,
useMemo,
} from "react";

import type { RowData, Table } from "@tanstack/react-table";
Expand All @@ -14,24 +17,42 @@ import { BREAKPOINTS } from "@/constants";
import { Placeholder } from "@/lib/elements";
import "./DynamicTable.scss";

export type DynamicTableProps = PropsWithChildren<{ className?: string }>;
const DynamicTableContext = createContext<{
variant: "full-height" | "regular";
}>({ variant: "regular" });

export type DynamicTableProps = PropsWithChildren<{
className?: string;
variant: "full-height" | "regular";
}>;

/**
* A table based on tanstack/react-table with a fixed header, where the table body can be scrolled vertically independent of the page itself.
* A table based on tanstack/react-table.
* In a full-height variant has a fixed header and the table body can be scrolled vertically independent of the page itself. The table body will take up the remaining height of the viewport.
* In a regular variant, the table body will scroll with the page and the header is sticky.
*
* @param className A class name to apply to the <table> element
* @param variant The variant of the table ("full-height" or "regular").
* @param children The markup of the table itself, composed of <thead> and DynamicTable.Body
* @returns
*/
export const DynamicTable = ({
className,
children,
variant,
...props
}: DynamicTableProps) => {
return (
<table {...props} className={classNames("p-table-dynamic", className)}>
{children}
</table>
<DynamicTableContext.Provider value={{ variant }}>
<table
{...props}
className={classNames("p-table-dynamic", className, {
"is-full-height": variant === "full-height",
})}
>
{children}
</table>
</DynamicTableContext.Provider>
);
};

Expand Down Expand Up @@ -77,15 +98,21 @@ const DynamicTableLoading = <TData extends RowData>({
</>
);
};
/**
* sets a fixed height for the table body
* allowing it to be scrolled independently of the page
*/

interface DynamicTableBodyProps extends AriaAttributes {
className?: string;
children: React.ReactNode;
height?: string;
style?: React.CSSProperties;
}

const DynamicTableBody = ({
className,
children,
style,
...props
}: PropsWithChildren<{ className?: string } & AriaAttributes>) => {
}: DynamicTableBodyProps) => {
const { variant } = useContext(DynamicTableContext);
const tableBodyRef: RefObject<HTMLTableSectionElement> = useRef(null);
const [offset, setOffset] = useState<number | null>(null);

Expand All @@ -107,23 +134,28 @@ const DynamicTableBody = ({
return () => window.removeEventListener("resize", handleResize);
}, [handleResize]);

const dynamicStyle = useMemo(() => {
if (variant === "full-height" && offset) {
return {
height: `calc(100vh - ${offset}px)`,
minHeight: `calc(100vh - ${offset}px)`,
...style,
};
}
return style;
}, [variant, offset, style]);

return (
<tbody
className={className}
ref={tableBodyRef}
style={
offset
? {
height: `calc(100vh - ${offset}px)`,
minHeight: `calc(100vh - ${offset}px)`,
}
: undefined
}
style={dynamicStyle}
{...props}
>
{children}
</tbody>
);
};

DynamicTable.Body = DynamicTableBody;
DynamicTable.Loading = DynamicTableLoading;
2 changes: 1 addition & 1 deletion src/lib/components/TableCaption/TableCaption.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const Example: StoryObj<typeof TableCaption> = {
),
},
render: (args) => (
<DynamicTable>
<DynamicTable variant="regular">
<thead>
<tr>
<th>FQDN</th>
Expand Down

0 comments on commit 0631b2a

Please sign in to comment.