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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | 37x 660x 660x 660x 910x 660x 4075x 3128x 37x 37x | import GiveTableHeader from "@shared/Table/GiveTableHeader";
import { TableRow, TableHead, SxProps } from "@mui/material";
import { TableColumnType, TableData, TableOrder } from "./Table.types";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import { styled, useAppTheme } from "@theme/v2/Provider";
import GiveAnimationWrapper from "@shared/BannerAnimations/GiveAnimationWrapper";
import { PageAnimationsType } from "@shared/BannerAnimations/animations";
import MarkAllCheckBox from "./MarkAllCheckBox";
import { memo } from "react";
type TableHeaderComponentProps = {
columns: TableColumnType[];
sortKey: keyof TableData | null;
order: TableOrder;
handleClick: (
column: TableColumnType,
) => (event: React.MouseEvent<HTMLElement>) => void;
getWidth: (index: number, col: TableColumnType) => string | undefined;
isMarkAllCheckBoxDisabled?: boolean;
/**
* Distance (px) from the top of the scroll container at which the header
* pins. 0 for standard tables; set to the compact banner height for tables
* that reveal a compact bar on scroll so the header sits below it.
*/
stickyTop?: number;
/**
* TRA013: whether this header pins (`position: sticky`) or sits in normal flow.
*
* - Plain tables + the labels row INSIDE the descending compact header pass
* the default (`true` → pinned / positioned normally).
* - The in-table column header of a compact-on-scroll table passes `false` so
* it scrolls off the top like ordinary content as the banner leaves — the
* descending `CompactScrollHeader` then takes over as the pinned header.
*/
sticky?: boolean;
/**
* The page's banner animation, so the column-header row continues the compact
* bar's animated background (fading to white) instead of cutting off at a
* solid white block. Only set for the labels row inside the compact header;
* omit for plain tables and the scroll-off in-table header (flat white).
*/
animationPage?: PageAnimationsType;
/**
* Draw no background at all — thead and cells fully transparent, keeping
* only the labels and their bottom rule.
*
* For a NON-pinned header on a page whose content sits on an animated
* backdrop (the Event Detail tabs): the default per-cell white paints an
* opaque band cut into the backdrop, while every other head element there is
* transparent over it. Only meaningful together with `sticky={false}` — a
* pinned transparent header would let the rows scrolling beneath show
* through the labels.
*/
transparent?: boolean;
};
const TableHeaderComponent = ({
columns,
sortKey,
order,
handleClick,
getWidth,
stickyTop = 0,
sticky = true,
animationPage,
transparent = false,
}: TableHeaderComponentProps) => {
const { isMobileView } = useCustomThemeV2();
const { palette } = useAppTheme();
// Blend the animation behind the header only where a compact bar sits directly
// above it (stickyTop > 0 with an animation page) — i.e. the labels row inside
// the descending `CompactScrollHeader`. Plain tables / the scroll-off in-table
// header keep a flat white header.
const showAnimation = !!animationPage && !!stickyTop;
const firstVisibleColumnIndex = columns.findIndex((col) => !col?.hideColumn);
return (
<TableHead
style={{
// Pin the column labels while the list scrolls underneath — unless the
// caller opts out (`sticky={false}`) so the in-table header scrolls off
// the top with the banner (TRA013). react-virtuoso applies sticky to its
// own header wrapper, but our custom `THead` renders the header fragment
// inline, so the positioning has to live on this thead.
position: sticky ? "sticky" : "static",
top: sticky ? stickyTop : undefined,
zIndex: 3,
// Opaque base hides the rows scrolling underneath the pinned header;
// a transparent (non-pinned) header lets the page's backdrop through
// instead.
backgroundColor: transparent ? "transparent" : palette.surface?.primary,
}}
>
<TableRow
sx={{
position: "relative",
zIndex: 1,
verticalAlign: "bottom",
"& th": {
borderBottom: `1px solid ${palette.border?.primary}`,
},
}}
>
{columns.map((col, index) => {
if (col?.hideColumn) return null;
return (
<GiveTableHeader
key={index}
state={sortKey === col.key ? order : "none"}
onClick={col.sortable ? handleClick(col) : undefined}
width={getWidth(index, col)}
sxProps={
{
...(!col.sortable && { pointerEvents: "none" }),
...(col.hasCheckbox &&
isMobileView && {
paddingLeft: "0",
}),
// Transparent cells let the animation — or, on a transparent
// header, the page's backdrop — show through; overrides the
// white background GiveTableHeader sets.
...((showAnimation || transparent) && {
background: "transparent",
}),
...(col.sx || {}),
// `col.sx` is typed as the full `TableRowProps["sx"]` union
// (object | array | fn); spreading it widens this literal so
// it no longer matches the plain-object `SxProps` GiveTable-
// Header expects. At runtime it is always a style object.
} as SxProps
}
sxContentProps={{
justifySelf: justifyHeader[col?.headerPosition || "left"],
...(showAnimation && { zIndex: 1 }),
}}
checkBoxElement={col?.hasCheckbox && <MarkAllCheckBox />}
backgroundElement={
showAnimation && index === firstVisibleColumnIndex ? (
<HeaderAnimationBg
page={animationPage}
aria-hidden
videoProps={{ "data-testid": "header-column-animation" }}
// Extend the single animation up behind the summary row that
// sits directly above this labels row (inside the compact
// header) so one continuous video backs both — instead of a
// second video in the summary that plays out of phase and
// reads as two "cut off" animations at the seam.
style={{ top: -stickyTop }}
/>
) : undefined
}
>
{col.title}
</GiveTableHeader>
);
})}
</TableRow>
</TableHead>
);
};
export default memo(TableHeaderComponent);
// The ONE page video for the compact scroll header, mounted inside the first
// visible header cell but positioned against the row. `top` is set inline to
// `-stickyTop` so it also fills the summary row directly above (which sits in
// front of it), making a single continuous animation across both instead of two
// out-of-phase videos meeting at a visible seam. Keeping it in the existing row
// prevents a spanning animation row from changing fixed-table column widths.
const HeaderAnimationBg = styled(GiveAnimationWrapper)(({ theme }) => ({
position: "absolute",
// `top` comes from the instance (`-stickyTop`); `bottom: 0` anchors it to the
// header's baseline so the element spans summary row + labels row.
bottom: 0,
// Extend past the table's side padding to the scroll-container edges so the
// animation reaches full width like the summary above, avoiding the white
// gaps at the header sides. 64px matches GiveTable's large-view padding.
left: "-64px",
right: "-64px",
minHeight: 0,
zIndex: 0,
pointerEvents: "none",
// The animation spans the header row and covers the header cells' own bottom
// border, so draw the grey separator under the column labels here — on the
// animation element itself — where it stays visible on scroll. The white wash
// (::after) sits inside this border, so the line reads as a clean grey rule.
borderBottom: `1px solid ${theme.palette.border?.primary}`,
// One fade over the full height: faint at the top (summary) so the video reads
// clearly, ~0.8 around the summary/labels boundary (≈60%), fully white by the
// data rows.
"&::after": {
content: '""',
position: "absolute",
inset: 0,
background:
"linear-gradient(180deg, rgba(255,255,255,0.68) 0%, rgba(255,255,255,0.8) 60%, #FFFFFF 100%)",
pointerEvents: "none",
},
}));
const justifyHeader = {
right: "flex-end",
center: "center",
left: "flex-start",
};
|