diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index ef99665..2a4cf1a 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -1,11 +1,11 @@ import { Box, Button, Card, Checkbox, CircularProgress, Collapse, darken, Divider, Grid2, IconButton, InputAdornment, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material"; +import { alpha } from "@mui/system/colorManipulator"; import { makeStyles } from "@mui/styles"; import { ArrowDownward, ArrowUpward, ChevronRight, Close, Search, ViewColumn } from "@mui/icons-material" import { useEffect, useRef, useState } from "react"; import React from "react"; import { useMobile } from "hooks"; import classNames from "classnames"; -import { getThemeType } from "theme"; const useStyles = makeStyles((theme: Theme) => { //const isMobile = useMobile() @@ -15,11 +15,10 @@ const useStyles = makeStyles((theme: Theme) => { border: "none" }, tableContainer: { - // margin: theme.spacing(1), - // [theme.breakpoints.up("sm")]: { - // margin: theme.spacing(2) - // }, - // width: "auto" + width: "100%", + maxWidth: "100%", + overflow: "hidden", + minWidth: 0, }, title: { padding: theme.spacing(1), @@ -59,10 +58,14 @@ const useStyles = makeStyles((theme: Theme) => { }, stickyHeader: { position: "sticky", - backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)", + backgroundColor: theme.palette.background.paper, + // Match Paper's elevation overlay so header matches search bar area + backgroundImage: theme.palette.mode === "dark" + ? `linear-gradient(${alpha("#fff", 0.05)}, ${alpha("#fff", 0.05)})` + : undefined, top: 0, zIndex: 300 //giving a really high z-index to make sure nothing is in front of it - } + }, })}, ); @@ -75,6 +78,53 @@ export interface Column { disableSort?: boolean; } +function ResizableHeaderTitle(props: { + column: Column; + index: number; + columnsLength: number; + resizeable: boolean; + onColumnClick: (column: Column) => void; + showOrderIcon: (column: Column) => React.ReactNode; +}) { + const { column, index, columnsLength, resizeable, onColumnClick, showOrderIcon } = props; + const titleRef = useRef(null); + const [isOverflowing, setIsOverflowing] = useState(false); + + useEffect(() => { + const el = titleRef.current; + if (!el) return; + const checkOverflow = () => setIsOverflowing(el.scrollWidth > el.clientWidth); + checkOverflow(); + const ro = new ResizeObserver(checkOverflow); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + return ( + !column.disableSort && onColumnClick(column)} + sx={{ + cursor: column.disableSort ? "default" : "pointer", + minWidth: 0, + overflow: "hidden", + ...(isOverflowing && resizeable && index < columnsLength - 1 + ? { + maskImage: "linear-gradient(to right, black 75%, transparent 100%)", + WebkitMaskImage: "linear-gradient(to right, black 75%, transparent 100%)", + } + : {}), + }} + > + + {column.title} {showOrderIcon(column)} + + + ); +} + interface Props { rows: T[], columns?: Column[] | (() => Column[]), @@ -450,14 +500,12 @@ export default function ResponsiveTable(props: Props) { } const resizeClick = (event: React.MouseEvent, index: number) => { - - let newWidths: number[] = [] - event.currentTarget.parentElement?.parentElement?.parentElement?.childNodes.forEach(node => { - if (node instanceof HTMLElement) { - newWidths.push(node.clientWidth) - } - }) - + const row = event.currentTarget.closest("tr") + if (!row) return + const cells = Array.from(row.querySelectorAll("th, td")) + const offset = (rowSelect ? 1 : 0) + (renderGutter ? 1 : 0) + const newWidths = cells.slice(offset).map((cell) => cell.clientWidth) + rowWidthsRef.current = newWidths dragIndexRef.current = index; dragStartRef.current = event.clientX; @@ -483,11 +531,15 @@ export default function ResponsiveTable(props: Props) { if (!dragStart) return let newWidths: number[] = [...rowWidths] - newWidths[index] = newWidths[index] - (dragStart - e.clientX) + const delta = dragStart - e.clientX + const newWidthForIndex = newWidths[index] - delta + const clampedWidth = Math.max(MIN_COLUMN_WIDTH, newWidthForIndex) + const actualDelta = newWidths[index] - clampedWidth + newWidths[index] = clampedWidth newWidths.every((width, j) => { if (j <= index || width === 0) return true else if (newWidths[j] !== undefined) { - newWidths[j] = newWidths[j] + (dragStart - e.clientX) + newWidths[j] = newWidths[j] + actualDelta return false } }) @@ -495,45 +547,49 @@ export default function ResponsiveTable(props: Props) { } + const MIN_COLUMN_WIDTH = 24; + return ( - - - - - + {(title || setSearchText) && ( + + + + + {renderTitle()} + {renderSubtitle()} + + + + - - {renderTitle()} - {renderSubtitle()} - + {actions && actions} - - - {actions && actions} - - - {endTitleElement} - - - {setSearchText && - {searchBar()} - } - - - { columns && !hideKeys && - - - - } - - + {endTitleElement} + + + {setSearchText && ( + + {searchBar()} + + )} + + + { columns && !hideKeys && + + + + } - - - + + + + )} + +
+ {customElement && @@ -552,18 +608,23 @@ export default function ResponsiveTable(props: Props) { ) return ( - - !column.disableSort && columnClick(column)} - sx={{ cursor: column.disableSort ? "default" : "pointer" }} - > - {column.title} {showOrderIcon(column)} - + + {index < columns.length-1 && resizeable && resizeClick(event, index)} > | @@ -672,6 +733,7 @@ export default function ResponsiveTable(props: Props) { }
+ {!hidePagination &&