Peter Makowski has proposed merging 
~petermakowski/maas-site-manager:header-height-MAASENG-1609 into 
maas-site-manager:main.

Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/442849
-- 
Your team MAAS Committers is requested to review the proposed merge of 
~petermakowski/maas-site-manager:header-height-MAASENG-1609 into 
maas-site-manager:main.
diff --git a/frontend/src/App.scss b/frontend/src/App.scss
index cad8a9a..2c1dbd9 100644
--- a/frontend/src/App.scss
+++ b/frontend/src/App.scss
@@ -73,3 +73,4 @@
 @import "@/components/base/TablePagination/TablePagination";
 @import "@/components/base/PaginationBar/PaginationBar";
 @import "@/components/base/TooltipButton/TooltipButton";
+@import "@/components/DynamicTable/DynamicTable";
diff --git a/frontend/src/_utils.scss b/frontend/src/_utils.scss
index d82c0ab..cf65341 100644
--- a/frontend/src/_utils.scss
+++ b/frontend/src/_utils.scss
@@ -75,3 +75,4 @@
   clip: rect(1px, 1px, 1px, 1px) !important;
   white-space: nowrap !important;
 }
+
diff --git a/frontend/src/components/DynamicTable/DynamicTable.test.tsx b/frontend/src/components/DynamicTable/DynamicTable.test.tsx
new file mode 100644
index 0000000..5ff7851
--- /dev/null
+++ b/frontend/src/components/DynamicTable/DynamicTable.test.tsx
@@ -0,0 +1,37 @@
+import { render, fireEvent, waitFor } from "@testing-library/react";
+
+import DynamicTable from "./DynamicTable";
+
+import BREAKPOINTS from "@/base/breakpoints";
+
+const offset = 100;
+
+beforeAll(() => {
+  // simulate top offset as JSDOM doesn't support getBoundingClientRect
+  // - equivalent of another element of height 100px being displayed above the table
+  vi.spyOn(window.HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
+    bottom: 0,
+    height: 0,
+    left: 0,
+    right: 0,
+    top: offset,
+    width: 0,
+  } as DOMRect);
+});
+
+it("sets a fixed table body height based on top offset on large screens", async () => {
+  vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.xSmall);
+  await fireEvent(window, new Event("resize"));
+
+  const { container } = render(<DynamicTable.Body className="test-class">Test content</DynamicTable.Body>);
+  // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+  const tbody = container.querySelector("tbody");
+  fireEvent(window, new Event("resize"));
+
+  // does not alter the height on small screens
+  expect(tbody).toHaveStyle("height: undefined");
+
+  vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.large);
+  await fireEvent(window, new Event("resize"));
+  await waitFor(() => expect(tbody).toHaveStyle(`height: calc(100vh - ${offset + 1}px)`));
+});
diff --git a/frontend/src/components/DynamicTable/DynamicTable.tsx b/frontend/src/components/DynamicTable/DynamicTable.tsx
new file mode 100644
index 0000000..b214fbf
--- /dev/null
+++ b/frontend/src/components/DynamicTable/DynamicTable.tsx
@@ -0,0 +1,50 @@
+import type { PropsWithChildren, RefObject } from "react";
+import { useState, useEffect, useLayoutEffect } from "react";
+
+import classNames from "classnames";
+
+import BREAKPOINTS from "@/base/breakpoints";
+
+const DynamicTable = ({ className, children }: PropsWithChildren<{ className?: string }>) => {
+  return <table className={classNames("p-table--dynamic", className)}>{children}</table>;
+};
+
+/**
+ * sets a fixed height for the table body
+ * allowing it to be scrolled independently of the page
+ */
+const DynamicTableBody = ({ className, children }: PropsWithChildren<{ className?: string }>) => {
+  const tableBodyRef: RefObject<HTMLTableSectionElement> = useRef(null);
+  const [offset, setOffset] = useState<number | null>(null);
+
+  const handleResize = useCallback(() => {
+    if (window.innerWidth > BREAKPOINTS.small) {
+      const top = tableBodyRef.current?.getBoundingClientRect?.().top;
+      if (top) setOffset(top + 1);
+    } else {
+      setOffset(null);
+    }
+  }, []);
+
+  useLayoutEffect(() => {
+    handleResize();
+  }, [handleResize]);
+
+  useEffect(() => {
+    window.addEventListener("resize", handleResize);
+    return () => window.removeEventListener("resize", handleResize);
+  }, [handleResize]);
+
+  return (
+    <tbody
+      className={className}
+      ref={tableBodyRef}
+      style={offset ? { height: `calc(100vh - ${offset}px)`, minHeight: `calc(100vh - ${offset}px)` } : undefined}
+    >
+      {children}
+    </tbody>
+  );
+};
+DynamicTable.Body = DynamicTableBody;
+
+export default DynamicTable;
diff --git a/frontend/src/components/DynamicTable/_DynamicTable.scss b/frontend/src/components/DynamicTable/_DynamicTable.scss
new file mode 100644
index 0000000..9b60ac2
--- /dev/null
+++ b/frontend/src/components/DynamicTable/_DynamicTable.scss
@@ -0,0 +1,23 @@
+.p-table--dynamic {
+  margin-bottom: 0;
+
+
+  thead, tbody {
+    display: block;
+    overflow-x: hidden;
+    overflow-y: auto;
+    scrollbar-gutter: stable;
+}
+
+  tbody {
+    height: auto;
+    min-height: auto;
+  }
+
+  thead tr,
+  tbody tr {
+    display: table;
+    table-layout: fixed;
+    width: 100%;
+  }
+}
diff --git a/frontend/src/components/DynamicTable/index.ts b/frontend/src/components/DynamicTable/index.ts
new file mode 100644
index 0000000..51b2226
--- /dev/null
+++ b/frontend/src/components/DynamicTable/index.ts
@@ -0,0 +1 @@
+export { default } from "./DynamicTable";
diff --git a/frontend/src/components/RequestsTable/RequestsTable.tsx b/frontend/src/components/RequestsTable/RequestsTable.tsx
index 34e109f..c9ade99 100644
--- a/frontend/src/components/RequestsTable/RequestsTable.tsx
+++ b/frontend/src/components/RequestsTable/RequestsTable.tsx
@@ -6,6 +6,7 @@ import type { Column, ColumnDef } from "@tanstack/react-table";
 import type { EnrollmentRequest } from "@/api/types";
 import docsUrls from "@/base/docsUrls";
 import DateTime from "@/components/DateTime";
+import DynamicTable from "@/components/DynamicTable/DynamicTable";
 import ExternalLink from "@/components/ExternalLink";
 import SelectAllCheckbox from "@/components/SelectAllCheckbox";
 import TableCaption from "@/components/TableCaption";
@@ -91,57 +92,55 @@ const RequestsTable = ({
   });
 
   return (
-    <>
-      <table aria-label="enrollment requests" className="sites-table">
-        <thead>
-          {table.getHeaderGroups().map((headerGroup) => (
-            <tr key={headerGroup.id}>
-              {headerGroup.headers.map((header) => {
-                return (
-                  <th className={`${header.column.id}`} colSpan={header.colSpan} key={header.id}>
-                    {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
-                  </th>
-                );
-              })}
-            </tr>
-          ))}
-        </thead>
-        {error ? (
-          <TableCaption>
-            <TableCaption.Error error={error} />
-          </TableCaption>
-        ) : isLoading ? (
-          <TableCaption>
-            <TableCaption.Loading />
-          </TableCaption>
-        ) : table.getRowModel().rows.length < 1 ? (
-          <TableCaption>
-            <TableCaption.Title>No outstanding requests</TableCaption.Title>
-            <TableCaption.Description>
-              You have to request an enrolment in the site-manager-agent.
-              <br />
-              <ExternalLink to={docsUrls.enrollmentRequest}>Read more about it in the documentation.</ExternalLink>
-            </TableCaption.Description>
-          </TableCaption>
-        ) : (
-          <tbody>
-            {table.getRowModel().rows.map((row) => {
+    <DynamicTable aria-label="enrollment requests" className="sites-table">
+      <thead>
+        {table.getHeaderGroups().map((headerGroup) => (
+          <tr key={headerGroup.id}>
+            {headerGroup.headers.map((header) => {
               return (
-                <tr key={row.id}>
-                  {row.getVisibleCells().map((cell) => {
-                    return (
-                      <td className={`${cell.column.id}`} key={cell.id}>
-                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
-                      </td>
-                    );
-                  })}
-                </tr>
+                <th className={`${header.column.id}`} colSpan={header.colSpan} key={header.id}>
+                  {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
+                </th>
               );
             })}
-          </tbody>
-        )}
-      </table>
-    </>
+          </tr>
+        ))}
+      </thead>
+      {error ? (
+        <TableCaption>
+          <TableCaption.Error error={error} />
+        </TableCaption>
+      ) : isLoading ? (
+        <TableCaption>
+          <TableCaption.Loading />
+        </TableCaption>
+      ) : table.getRowModel().rows.length < 1 ? (
+        <TableCaption>
+          <TableCaption.Title>No outstanding requests</TableCaption.Title>
+          <TableCaption.Description>
+            You have to request an enrolment in the site-manager-agent.
+            <br />
+            <ExternalLink to={docsUrls.enrollmentRequest}>Read more about it in the documentation.</ExternalLink>
+          </TableCaption.Description>
+        </TableCaption>
+      ) : (
+        <DynamicTable.Body>
+          {table.getRowModel().rows.map((row) => {
+            return (
+              <tr key={row.id}>
+                {row.getVisibleCells().map((cell) => {
+                  return (
+                    <td className={`${cell.column.id}`} key={cell.id}>
+                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                    </td>
+                  );
+                })}
+              </tr>
+            );
+          })}
+        </DynamicTable.Body>
+      )}
+    </DynamicTable>
   );
 };
 
diff --git a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
index f368905..c211493 100644
--- a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
+++ b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
@@ -11,6 +11,7 @@ import ConnectionInfo from "./ConnectionInfo";
 import SitesTableControls from "./SitesTableControls/SitesTableControls";
 
 import type { SitesQueryResult } from "@/api/types";
+import DynamicTable from "@/components/DynamicTable/DynamicTable";
 import ExternalLink from "@/components/ExternalLink";
 import NoRegions from "@/components/NoRegions";
 import SelectAllCheckbox from "@/components/SelectAllCheckbox";
@@ -231,7 +232,7 @@ const SitesTable = ({
         setSearchText={setSearchText}
       />
       <PaginationBar {...paginationProps} />
-      <table aria-label="sites" className="sites-table">
+      <DynamicTable aria-label="sites" className="sites-table" id="sites-table">
         <thead>
           {table.getHeaderGroups().map((headerGroup) => (
             <tr key={headerGroup.id}>
@@ -256,7 +257,7 @@ const SitesTable = ({
         ) : table.getRowModel().rows.length < 1 ? (
           <NoRegions />
         ) : (
-          <tbody>
+          <DynamicTable.Body>
             {table.getRowModel().rows.map((row) => {
               return (
                 <tr
@@ -273,9 +274,9 @@ const SitesTable = ({
                 </tr>
               );
             })}
-          </tbody>
+          </DynamicTable.Body>
         )}
-      </table>
+      </DynamicTable>
     </>
   );
 };
diff --git a/frontend/src/components/SitesList/SitesTable/_SitesTable.scss b/frontend/src/components/SitesList/SitesTable/_SitesTable.scss
index 03a559d..dacb224 100644
--- a/frontend/src/components/SitesList/SitesTable/_SitesTable.scss
+++ b/frontend/src/components/SitesList/SitesTable/_SitesTable.scss
@@ -1,7 +1,8 @@
 $connection-status-icon-width: 1.5rem;
 
 .sites-table {
-  thead th:first-child {
+  thead th:first-child,
+  tbody td:first-child {
     width: 3rem;
   }
 
diff --git a/frontend/src/components/TokensList/_TokensList.scss b/frontend/src/components/TokensList/_TokensList.scss
index 3d0d8d5..a3593d1 100644
--- a/frontend/src/components/TokensList/_TokensList.scss
+++ b/frontend/src/components/TokensList/_TokensList.scss
@@ -6,17 +6,11 @@ $instructions-height-medium: 10.9375rem;
   display: grid;
 
   @media only screen and (min-width: $breakpoint-small) {
-    height: $header-height-medium;
-    position: sticky;
-    top: -#{$spv--medium};
     background-color: white;
     z-index: 1;
     padding-top: $spv--medium;
   }
 
-  @media only screen and (min-width: $breakpoint-large) {
-    height: $header-height-large;
-  }
 
   .tokens-list-certificate {
     display: grid;
diff --git a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
index 42cbf90..a250c3b 100644
--- a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
+++ b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
@@ -1,10 +1,13 @@
+import type { PropsWithChildren } from "react";
 import { useCallback, useMemo, useState } from "react";
 
 import type { ColumnDef, Column, Row, Getter } from "@tanstack/react-table";
 import { flexRender, useReactTable, getCoreRowModel } from "@tanstack/react-table";
+import classNames from "classnames";
 import pick from "lodash/fp/pick";
 
 import type { Token } from "@/api/types";
+import DynamicTable from "@/components/DynamicTable";
 import SelectAllCheckbox from "@/components/SelectAllCheckbox";
 import TableCaption from "@/components/TableCaption";
 import CopyButton from "@/components/base/CopyButton";
@@ -119,13 +122,14 @@ const TokensTable = ({ data, error, isLoading }: Pick<useTokensQueryResult, "dat
     enableMultiRowSelection: true,
     onRowSelectionChange: setRowSelection,
   });
+
   return (
-    <table aria-label="tokens" className="tokens-table">
+    <DynamicTable aria-label="tokens" className="tokens-table u-no-margin--bottom">
       <thead>
         {tokenTable.getHeaderGroups().map((headerGroup) => (
           <tr key={headerGroup.id}>
             {headerGroup.headers.map((header) => (
-              <th colSpan={header.colSpan} key={header.id}>
+              <th className={`tokens-table__col-${header.column.id}`} colSpan={header.colSpan} key={header.id}>
                 {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
               </th>
             ))}
@@ -148,17 +152,21 @@ const TokensTable = ({ data, error, isLoading }: Pick<useTokensQueryResult, "dat
           </TableCaption.Description>
         </TableCaption>
       ) : (
-        <tbody>
+        <DynamicTable.Body>
           {tokenTable.getRowModel().rows.map((row) => (
             <tr key={row.id}>
               {row.getVisibleCells().map((cell) => {
-                return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>;
+                return (
+                  <td className={`tokens-table__col-${cell.column.id}`} key={cell.id}>
+                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                  </td>
+                );
               })}
             </tr>
           ))}
-        </tbody>
+        </DynamicTable.Body>
       )}
-    </table>
+    </DynamicTable>
   );
 };
 
diff --git a/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss b/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss
index 7835c4c..2c8587c 100644
--- a/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss
+++ b/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss
@@ -5,49 +5,45 @@
   }
   
   thead th {
-    position: sticky;
     top: -#{$spv--medium};
-    background-color: white;
-    z-index: 1;
     padding-top: $spv--large;
-
-    @media only screen and (min-width: $breakpoint-small){
-      top: calc($header-height-medium - $spv--medium);
-    }
-
-    @media only screen and (min-width: $breakpoint-large) {
-      top: calc($header-height-large - $spv--medium);
-    }
   }
 
-  thead th:first-child {
+  thead th:first-child,
+  tbody td:first-child {
     width: 3rem;
   }
 
-  thead th:last-child {
+  thead th:last-child,
+  tbody td:last-child {
     width: 15rem;
   }
 
-  thead th:nth-child(3) {
+  thead th:nth-child(3),
+  tbody td:nth-child(3) {
     width: 15rem;
   }
 
   @media screen and (min-width: $breakpoint-small) and (max-width: $breakpoint-large) {
-    thead th:last-child {
+    thead th:last-child,
+    tbody td:last-child {
       width: 10rem;
     }
 
-    thead th:nth-child(3) {
+    thead th:nth-child(3),
+    tbody td:nth-child(3) {
       width: 10rem;
     }
   }
 
   @media screen and (max-width: $breakpoint-small) {
-    thead th:last-child {
+    thead th:last-child,
+    tbody td:last-child {
       width: auto;
     }
 
-    thead th:nth-child(3) {
+    thead th:nth-child(3),
+    tbody td:nth-child(3) {
       width: auto;
     }
   }
-- 
Mailing list: https://launchpad.net/~sts-sponsors
Post to     : sts-sponsors@lists.launchpad.net
Unsubscribe : https://launchpad.net/~sts-sponsors
More help   : https://help.launchpad.net/ListHelp

Reply via email to