use existing ColumnHeader to enable sort, column menu

This commit is contained in:
Sidney Alcantara
2022-10-31 17:04:18 +11:00
parent 2355ff7dfc
commit 1f581af858
6 changed files with 108 additions and 108 deletions

View File

@@ -1,36 +1,33 @@
import { useRef } from "react";
import { forwardRef, useRef } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useDrag, useDrop } from "react-dnd";
import {
styled,
alpha,
Tooltip,
TooltipProps,
tooltipClasses,
Fade,
Grid,
GridProps,
IconButton,
Typography,
} from "@mui/material";
import DropdownIcon from "@mui/icons-material/MoreHoriz";
import LockIcon from "@mui/icons-material/LockOutlined";
import ColumnHeaderSort from "./ColumnHeaderSort";
import ColumnHeaderSort, { SORT_STATES } from "./ColumnHeaderSort";
import {
projectScope,
userRolesAtom,
altPressAtom,
} from "@src/atoms/projectScope";
import { projectScope, altPressAtom } from "@src/atoms/projectScope";
import {
tableScope,
updateColumnAtom,
columnMenuAtom,
tableSortsAtom,
} from "@src/atoms/tableScope";
import { getFieldProp } from "@src/components/fields";
import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column";
import { ColumnConfig } from "@src/types/table";
import { FieldType } from "@src/constants/fields";
import { spreadSx } from "@src/utils/ui";
export { COLUMN_HEADER_HEIGHT };
@@ -47,36 +44,20 @@ const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
},
}));
export interface IDraggableHeaderRendererProps {
export interface IColumnHeaderProps extends Partial<GridProps> {
column: ColumnConfig;
width: number;
focusInsideCell: boolean;
children: React.ReactNode;
}
export default function DraggableHeaderRenderer({
column,
}: IDraggableHeaderRendererProps) {
const [userRoles] = useAtom(userRolesAtom, projectScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
export const ColumnHeader = forwardRef(function ColumnHeader(
{ column, width, focusInsideCell, children, ...props }: IColumnHeaderProps,
ref: React.Ref<HTMLDivElement>
) {
const openColumnMenu = useSetAtom(columnMenuAtom, tableScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const [{ isDragging }, dragRef] = useDrag({
type: "COLUMN_DRAG",
item: { key: column.key },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [{ isOver }, dropRef] = useDrop({
accept: "COLUMN_DRAG",
drop: ({ key }: { key: string }) => {
updateColumn({ key, config: {}, index: column.index });
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const [tableSorts] = useAtom(tableSortsAtom, tableScope);
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -85,14 +66,26 @@ export default function DraggableHeaderRenderer({
openColumnMenu({ column, anchorEl: buttonRef.current });
};
const _sortKey = getFieldProp("sortKey", (column as any).type);
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
const currentSort: typeof SORT_STATES[number] =
tableSorts[0]?.key !== sortKey
? "none"
: tableSorts[0]?.direction || "none";
return (
<Grid
key={column.key}
role="columnheader"
id={`column-header-${column.key}`}
ref={(ref) => {
dragRef(ref);
dropRef(ref);
}}
ref={ref}
{...props}
aria-sort={
currentSort === "none"
? "none"
: currentSort === "asc"
? "ascending"
: "descending"
}
container
alignItems="center"
wrap="nowrap"
@@ -100,7 +93,8 @@ export default function DraggableHeaderRenderer({
sx={[
{
height: "100%",
"& svg, & button": { display: "block" },
"& svg, & button": { display: "block", zIndex: 1 },
border: (theme) => `1px solid ${theme.palette.divider}`,
color: "text.secondary",
transition: (theme) =>
@@ -109,29 +103,18 @@ export default function DraggableHeaderRenderer({
}),
"&:hover": { color: "text.primary" },
cursor: "move",
position: "relative",
py: 0,
pr: 0.5,
pl: 1,
width: "100%",
},
isDragging
? { opacity: 0.5 }
: isOver
? {
backgroundColor: (theme) =>
alpha(
theme.palette.primary.main,
theme.palette.action.focusOpacity
),
color: "primary.main",
}
: {},
...spreadSx(props.sx),
]}
className="column-header"
>
{(column.width as number) > 140 && (
{width > 140 && (
<Tooltip
title={
<>
@@ -149,6 +132,7 @@ export default function DraggableHeaderRenderer({
onClick={() => {
navigator.clipboard.writeText(column.key);
}}
style={{ position: "relative", zIndex: 2 }}
>
{column.editable === false ? (
<LockIcon />
@@ -190,6 +174,8 @@ export default function DraggableHeaderRenderer({
fontWeight: "fontWeightMedium",
lineHeight: `${COLUMN_HEADER_HEIGHT}px`,
textOverflow: "clip",
position: "relative",
zIndex: 1,
}}
component="div"
color="inherit"
@@ -205,15 +191,22 @@ export default function DraggableHeaderRenderer({
</LightTooltip>
</Grid>
<Grid item>
<ColumnHeaderSort column={column as any} />
</Grid>
{column.type !== FieldType.id && (
<Grid item>
<ColumnHeaderSort
sortKey={sortKey}
currentSort={currentSort}
tabIndex={focusInsideCell ? 0 : -1}
/>
</Grid>
)}
<Grid item>
<Tooltip title="Column settings">
<IconButton
size="small"
aria-label={`Column settings for ${column.name as string}`}
tabIndex={focusInsideCell ? 0 : -1}
id={`column-settings-${column.key}`}
color="inherit"
onClick={handleOpenMenu}
@@ -225,13 +218,17 @@ export default function DraggableHeaderRenderer({
}),
color: "text.disabled",
".column-header:hover &": { color: "text.primary" },
".column-header:hover &, &:focus": { color: "text.primary" },
}}
>
<DropdownIcon />
</IconButton>
</Tooltip>
</Grid>
{children}
</Grid>
);
}
});
export default ColumnHeader;

View File

@@ -1,4 +1,4 @@
import { useAtom } from "jotai";
import { useSetAtom } from "jotai";
import { colord } from "colord";
import { Tooltip, IconButton } from "@mui/material";
@@ -8,27 +8,22 @@ import IconSlash, {
} from "@src/components/IconSlash";
import { tableScope, tableSortsAtom } from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { ColumnConfig } from "@src/types/table";
const SORT_STATES = ["none", "desc", "asc"] as const;
export const SORT_STATES = ["none", "desc", "asc"] as const;
export interface IColumnHeaderSortProps {
column: ColumnConfig;
sortKey: string;
currentSort: typeof SORT_STATES[number];
tabIndex?: number;
}
export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope);
export default function ColumnHeaderSort({
sortKey,
currentSort,
tabIndex,
}: IColumnHeaderSortProps) {
const setTableSorts = useSetAtom(tableSortsAtom, tableScope);
const _sortKey = getFieldProp("sortKey", (column as any).type);
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
const currentSort: typeof SORT_STATES[number] =
tableSorts[0]?.key !== sortKey
? "none"
: tableSorts[0]?.direction || "none";
const nextSort =
SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0];
@@ -37,8 +32,6 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
else setTableSorts([{ key: sortKey, direction: nextSort }]);
};
if (column.type === FieldType.id) return null;
return (
<Tooltip
title={nextSort === "none" ? "Unsort" : `Sort by ${nextSort}ending`}
@@ -48,9 +41,10 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
size="small"
onClick={handleSortClick}
color="inherit"
tabIndex={tabIndex}
sx={{
bgcolor: "background.default",
"&:hover": {
"&:hover, &:focus": {
backgroundColor: (theme) =>
colord(theme.palette.background.default)
.mix(
@@ -74,7 +68,8 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
position: "relative",
opacity: currentSort !== "none" ? 1 : 0,
".column-header:hover &": { opacity: 1 },
"[role='columnheader']:hover &, [role='columnheader']:focus &, [role='columnheader']:focus-within &, &:focus":
{ opacity: 1 },
transition: (theme) =>
theme.transitions.create(["background-color", "opacity"], {
@@ -89,7 +84,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
transform: currentSort === "asc" ? "rotate(180deg)" : "none",
},
"&:hover .arrow": {
"&:hover .arrow, &:focus .arrow": {
transform:
currentSort === "asc" || nextSort === "asc"
? "rotate(180deg)"
@@ -100,7 +95,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
strokeDashoffset:
currentSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET,
},
"&:hover .icon-slash": {
"&:hover .icon-slash, &:focus .icon-slash": {
strokeDashoffset:
nextSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET,
},

View File

@@ -9,6 +9,7 @@ export const StyledResizer = styled("div", {
shouldForwardProp: (prop) => prop !== "isResizing",
})<IStyledResizerProps>(({ theme, isResizing }) => ({
position: "absolute",
zIndex: 5,
right: 0,
top: 0,
height: "100%",
@@ -39,7 +40,7 @@ export const StyledResizer = styled("div", {
height: "50%",
width: 4,
borderRadius: 2,
marginRight: 3,
marginRight: 2,
background: isResizing
? theme.palette.primary.main
@@ -51,3 +52,5 @@ export const StyledResizer = styled("div", {
},
}));
StyledResizer.displayName = "StyledResizer";
export default StyledResizer;

View File

@@ -32,3 +32,5 @@ export const StyledRow = styled("div")(({ theme }) => ({
},
}));
StyledRow.displayName = "StyledRow";
export default StyledRow;

View File

@@ -31,3 +31,5 @@ export const StyledTable = styled("div")(({ theme }) => ({
},
}));
StyledTable.displayName = "StyledTable";
export default StyledTable;

View File

@@ -1,29 +1,27 @@
import { useMemo, useRef, useState, useEffect, useCallback } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useThrottledCallback } from "use-debounce";
import {
DragDropContext,
DropResult,
Droppable,
Draggable,
} from "react-beautiful-dnd";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
DragDropContext,
DropResult,
Droppable,
Draggable,
} from "react-beautiful-dnd";
import { Portal } from "@mui/material";
import { StyledTable } from "./Styled/StyledTable";
import { StyledRow } from "./Styled/StyledRow";
import { StyledResizer } from "./Styled/StyledResizer";
import ColumnHeaderComponent from "./Column";
import StyledTable from "./Styled/StyledTable";
import StyledRow from "./Styled/StyledRow";
import ColumnHeader from "./ColumnHeader";
import StyledResizer from "./Styled/StyledResizer";
import OutOfOrderIndicator from "./OutOfOrderIndicator";
import ContextMenu from "./ContextMenu";
import { IconButton, Portal } from "@mui/material";
import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader";
import FinalColumnHeader from "./FinalColumnHeader";
import FinalColumn from "./formatters/FinalColumn";
// import TableRow from "./TableRow";
@@ -31,8 +29,6 @@ import EmptyState from "@src/components/EmptyState";
// import BulkActions from "./BulkActions";
import AddRow from "@src/components/TableToolbar/AddRow";
import { AddRow as AddRowIcon } from "@src/assets/icons";
import Loading from "@src/components/Loading";
import ContextMenu from "./ContextMenu";
import {
projectScope,
@@ -291,6 +287,8 @@ export default function TableComponent() {
ref={provided.innerRef}
>
{headerGroup.headers.map((header) => {
if (!header.column.columnDef.meta) return null;
const isSelectedCell =
(!selectedCell && header.index === 0) ||
(selectedCell?.path === "_rowy_header" &&
@@ -305,7 +303,7 @@ export default function TableComponent() {
disableInteractiveElementBlocking
>
{(provided, snapshot) => (
<ColumnHeaderComponent
<ColumnHeader
key={header.id}
data-row-id={"_rowy_header"}
data-col-id={header.id}
@@ -317,16 +315,11 @@ export default function TableComponent() {
}
ref={provided.innerRef}
{...provided.draggableProps}
role="columnheader"
tabIndex={isSelectedCell ? 0 : -1}
aria-colindex={header.index + 1}
aria-readonly={!canEditColumn}
// TODO: aria-sort={"none" | "ascending" | "descending" | "other" | undefined}
aria-selected={isSelectedCell}
label={
header.column.columnDef.meta?.name || header.id
}
type={header.column.columnDef.meta?.type}
column={header.column.columnDef.meta!}
style={{
width: header.getSize(),
left: header.column.getIsPinned()
@@ -335,6 +328,7 @@ export default function TableComponent() {
: undefined,
...provided.draggableProps.style,
}}
width={header.getSize()}
sx={[
snapshot.isDragging
? {}
@@ -347,6 +341,9 @@ export default function TableComponent() {
});
(e.target as HTMLDivElement).focus();
}}
focusInsideCell={
isSelectedCell && focusInsideCell
}
>
<div
{...provided.dragHandleProps}
@@ -360,7 +357,11 @@ export default function TableComponent() {
]
: undefined
}
style={{ position: "absolute", inset: 0 }}
style={{
position: "absolute",
inset: 0,
zIndex: 0,
}}
/>
{header.column.getCanResize() && (
@@ -370,7 +371,7 @@ export default function TableComponent() {
onTouchStart={header.getResizeHandler()}
/>
)}
</ColumnHeaderComponent>
</ColumnHeader>
)}
</Draggable>
);