All files / src/componentsV2/Table CompactScrollHeader.tsx

100% Statements 9/9
100% Branches 6/6
100% Functions 5/5
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152                                                                                      35x                     563x                                                                                     35x 2850x 563x                                                                 35x                       35x              
import { ReactNode, memo } from "react";
import { Box } from "@mui/material";
import { styled } from "@theme/v2/Provider";
import { PageAnimationsType } from "@shared/BannerAnimations/animations";
import { TableColumnType, TableData, TableOrder } from "./Table.types";
import TableHeaderComponent from "./TableHeaderComponent";
import { COMPACT_HEAD_HEIGHT } from "./compactHead";
 
type Props = {
  columns: TableColumnType[];
  sortKey: keyof TableData | null;
  order: TableOrder;
  handleClick: (
    column: TableColumnType,
  ) => (event: React.MouseEvent<HTMLElement>) => void;
  getWidth: (index: number, col: TableColumnType) => string | undefined;
  /** The compact summary content (e.g. GiveMerchantBanner in compact mode). */
  compactHead: ReactNode;
  /** Page banner animation drawn as one continuous background behind both rows. */
  animationPage?: PageAnimationsType;
  /** Whether the collapsed header is revealed (drives the slide-down). */
  isVisible: boolean;
  /** Horizontal padding, matched to GiveTable's so labels align to the columns. */
  padding?: string;
};
 
/**
 * TRA013 — the descending collapsed header for compact-on-scroll tables (wide
 * view). While the banner + the in-table column header scroll off the top, this
 * self-contained header **slides down from the top of the viewport** and pins,
 * as one unit: a summary row on top of a column-labels row, over a single
 * continuous animated background.
 *
 * It is a separate, absolutely-positioned layer (not the sticky `<thead>`) for
 * two reasons: CSS `position: sticky` cannot be moved with `transform` without
 * breaking the pin, and the in-table header has to be free to scroll away. It
 * sits at the top of `Table`'s relative `Container`, so the data rows in the
 * scroller beneath scroll under it like any pinned header.
 *
 * Alignment: the labels are a real `<table>` with the SAME `getWidth` columns +
 * fixed layout, and the padding is matched to GiveTable's, so this table's
 * columns line up with the main table's columns.
 */
const CompactScrollHeader = ({
  columns,
  sortKey,
  order,
  handleClick,
  getWidth,
  compactHead,
  animationPage,
  isVisible,
  padding,
}: Props) => {
  return (
    <Root
      padding={padding}
      data-testid="compact-scroll-header"
      className={isVisible ? "compact-scroll-header--visible" : undefined}
      aria-hidden={!isVisible}
    >
      {/* The inner content is only MOUNTED once revealed — while parked this
          layer is merely CSS-hidden, so mounting it would put a SECOND copy of
          the banner stats + column labels in the document (breaking text
          queries and screen readers) while the full banner and the in-table
          header are still on screen. The Root stays mounted so its descend
          transition still plays. Mirrors Table's inline CompactHeadBar. */}
      {isVisible && (
        <>
          <SummaryRow>{compactHead}</SummaryRow>
          <LabelsTable>
            {/*
              The labels row. `sticky={false}` — the Root already pins this header,
              so the labels sit in normal flow directly below the summary.
              `stickyTop` is the summary height so the single animation (drawn in
              the labels row) extends UP behind the summary, backing both rows
              continuously.
            */}
            <TableHeaderComponent
              columns={columns}
              sortKey={sortKey}
              order={order}
              handleClick={handleClick}
              getWidth={getWidth}
              stickyTop={COMPACT_HEAD_HEIGHT}
              animationPage={animationPage}
              sticky={false}
            />
          </LabelsTable>
        </>
      )}
    </Root>
  );
};
 
export default memo(CompactScrollHeader);
 
const Root = styled(Box, {
  shouldForwardProp: (prop) => prop !== "padding",
})<{ padding?: string }>(({ padding }) => ({
  position: "absolute",
  top: 0,
  left: 0,
  right: 0,
  zIndex: 101,
  display: "flex",
  flexDirection: "column",
  overflow: "hidden",
  // The rows inside mount on reveal, so keep the parked (empty) layer at the
  // summary height — `translateY(-100%)` then still travels a real distance
  // and the descend animates instead of degrading to a fade (same trick as
  // Table's inline CompactHeadBar).
  minHeight: `${COMPACT_HEAD_HEIGHT}px`,
  // Match GiveTable's large-view content padding so the labels table below lines
  // up with the main table's columns.
  padding: padding ?? "0 64px",
  // Parked above the viewport and hidden until revealed; `opacity` keeps the
  // parked bar from peeking above the page while `transform` does the descend.
  transform: "translateY(-100%)",
  opacity: 0,
  pointerEvents: "none",
  transition: "transform 0.28s ease, opacity 0.28s ease",
  "&.compact-scroll-header--visible": {
    transform: "translateY(0)",
    opacity: 1,
    pointerEvents: "auto",
  },
}));
 
// The summary row sits directly above the labels row and in front of the shared
// animation that extends up behind it. Fixed to the summary height so the
// animation's `-stickyTop` extension lines up exactly with this row.
const SummaryRow = styled(Box)(() => ({
  position: "relative",
  zIndex: 2,
  minHeight: `${COMPACT_HEAD_HEIGHT}px`,
  display: "flex",
  alignItems: "center",
  width: "100%",
}));
 
// A real fixed-layout table so the column labels align with the main table.
// `width: 100%` + the same `getWidth` percentages match the main table's columns
// within the shared (padded) content width.
const LabelsTable = styled("table")(() => ({
  position: "relative",
  zIndex: 1,
  width: "100%",
  tableLayout: "fixed",
  borderCollapse: "collapse",
}));