add cell navigation

This commit is contained in:
Sidney Alcantara
2022-10-17 17:57:26 +11:00
parent 8dddfcd533
commit 4f49503e7a
3 changed files with 207 additions and 27 deletions

View File

@@ -124,7 +124,10 @@ export const importAirtableAtom = atom<{
/** Store side drawer open state */
export const sideDrawerOpenAtom = atom(false);
export type SelectedCell = { path: string; columnKey: string };
export type SelectedCell = {
path: string | "_rowy_header";
columnKey: string | "_rowy_row_actions";
};
/** Store selected cell in table. Used in side drawer and context menu */
export const selectedCellAtom = atom<SelectedCell | null>(null);

View File

@@ -3,5 +3,16 @@ import { styled } from "@mui/material";
export const StyledTable = styled("div")(({ theme }) => ({
...(theme.typography.caption as any),
lineHeight: "inherit !important",
"& [role='columnheader'], & [role='gridcell']": {
"&[aria-selected='true']": {
outline: `1px solid ${theme.palette.primary.main}`,
outlineOffset: "-1px",
},
"&:focus": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: "-2px",
},
},
}));
StyledTable.displayName = "StyledTable";

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState, Suspense } from "react";
import React, { useMemo, useState, Suspense, useRef } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
import { DndProvider } from "react-dnd";
@@ -49,7 +49,9 @@ import {
updateColumnAtom,
updateFieldAtom,
selectedCellAtom,
SelectedCell,
} from "@src/atoms/tableScope";
import { COLLECTION_PAGE_SIZE } from "@src/config/db";
import { getFieldType, getFieldProp } from "@src/components/fields";
import { FieldType } from "@src/constants/fields";
@@ -84,7 +86,11 @@ export default function TableComponent() {
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
const updateField = useSetAtom(updateFieldAtom, tableScope);
const gridRef = useRef<HTMLDivElement>(null);
const [focusInsideCell, setFocusInsideCell] = useState(false);
const canAddColumn = userRoles.includes("ADMIN");
const canEditColumn = userRoles.includes("ADMIN");
const userDocHiddenFields =
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields;
@@ -103,6 +109,7 @@ export default function TableComponent() {
// })
.map((columnConfig) =>
columnHelper.accessor(columnConfig.fieldName, {
id: columnConfig.fieldName,
meta: columnConfig,
// draggable: true,
// resizable: true,
@@ -160,28 +167,162 @@ export default function TableComponent() {
columnResizeMode: "onChange",
// debugRows: true,
});
console.log(table);
console.log(table, selectedCell);
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {};
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
console.log(
"keydown",
// e.target,
e.key,
e.ctrlKey ? "ctrl" : "",
e.altKey ? "alt" : "",
e.metaKey ? "meta" : "",
e.shiftKey ? "shift" : ""
);
const LISTENED_KEYS = [
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"Enter",
"Escape",
"Home",
"End",
"PageUp",
"PageDown",
];
if (LISTENED_KEYS.includes(e.key)) e.preventDefault();
const target = e.target as HTMLDivElement;
if (
target.getAttribute("role") !== "columnheader" &&
target.getAttribute("role") !== "gridcell"
)
return;
const colIndex = Number(target.getAttribute("aria-colindex")) - 1;
const rowIndex =
Number(target.parentElement!.getAttribute("aria-rowindex")) - 2;
const rowId = target.getAttribute("data-rowId")!;
const colId = target.getAttribute("data-colId")!;
const isHeader = rowId === "_rowy_header";
let newColIndex = colIndex;
let newRowIndex = rowIndex;
// const newSelectedCell: SelectedCell = selectedCell
// ? { ...selectedCell }
// : { path: rowId, columnKey: colId };
switch (e.key) {
case "ArrowUp":
if (rowIndex > -1) newRowIndex = rowIndex - 1;
break;
case "ArrowDown":
if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1;
break;
case "ArrowLeft":
if (colIndex > 0) newColIndex = colIndex - 1;
break;
case "ArrowRight":
if (colIndex < columns.length - 1) newColIndex = colIndex + 1;
break;
case "PageUp":
newRowIndex = Math.max(0, rowIndex - COLLECTION_PAGE_SIZE);
break;
case "PageDown":
newRowIndex = Math.min(
tableRows.length - 1,
rowIndex + COLLECTION_PAGE_SIZE
);
break;
case "Home":
newColIndex = 0;
if (e.ctrlKey || e.metaKey) newRowIndex = -1;
break;
case "End":
newColIndex = columns.length - 1;
if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1;
break;
}
const newSelectedCell = {
path:
newRowIndex > -1
? tableRows[newRowIndex]._rowy_ref.path
: "_rowy_header",
columnKey: columns[newColIndex].id! || columns[0].id!,
};
console.log(newRowIndex, newColIndex, newSelectedCell);
setSelectedCell(newSelectedCell);
// Find matching DOM element for the cell
const newCellEl = gridRef.current?.querySelector(
`[aria-rowindex="${newRowIndex + 2}"] [aria-colindex="${
newColIndex + 1
}"]`
);
// Focus either the cell or the first focusable element in the cell
if (newCellEl) (newCellEl as HTMLDivElement).focus();
};
return (
<>
<StyledTable
ref={gridRef}
role="grid"
aria-readonly={tableSettings.readOnly}
aria-colcount={columns.length}
aria-rowcount={tableRows.length + 1}
style={{ width: table.getTotalSize(), userSelect: "none" }}
onKeyDown={handleKeyDown}
>
<div className="thead" style={{ position: "sticky", top: 0 }}>
<div
className="thead"
role="rowgroup"
style={{ position: "sticky", top: 0 }}
>
{table.getHeaderGroups().map((headerGroup) => (
<StyledRow key={headerGroup.id} role="row">
{headerGroup.headers.map((header) => (
<ColumnHeaderComponent
key={header.id}
label={header.column.columnDef.meta?.name || header.id}
type={header.column.columnDef.meta?.type}
style={{ width: header.getSize() }}
>
{/* <div
<StyledRow key={headerGroup.id} role="row" aria-rowindex={1}>
{headerGroup.headers.map((header) => {
const isFocusable =
(!selectedCell && header.index === 0) ||
(selectedCell?.path === "_rowy_header" &&
selectedCell?.columnKey === header.id);
return (
<ColumnHeaderComponent
key={header.id}
data-rowId={"_rowy_header"}
data-colId={header.id}
role="columnheader"
tabIndex={isFocusable ? 0 : -1}
aria-colindex={header.index + 1}
aria-readonly={canEditColumn}
// TODO: aria-sort={"none" | "ascending" | "descending" | "other" | undefined}
aria-selected={isFocusable}
label={header.column.columnDef.meta?.name || header.id}
type={header.column.columnDef.meta?.type}
style={{ width: header.getSize() }}
onClick={(e) => {
setSelectedCell({
path: "_rowy_header",
columnKey: header.id,
});
(e.target as HTMLDivElement).focus();
}}
>
{/* <div
{...{
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
@@ -199,24 +340,49 @@ export default function TableComponent() {
// },
}}
/> */}
</ColumnHeaderComponent>
))}
</ColumnHeaderComponent>
);
})}
</StyledRow>
))}
</div>
<div className="tbody">
<div className="tbody" role="rowgroup">
{table.getRowModel().rows.map((row) => (
<StyledRow key={row.id} role="row">
{row.getVisibleCells().map((cell) => (
<StyledCell
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
<button>f</button>
</StyledCell>
))}
<StyledRow key={row.id} role="row" aria-rowindex={row.index + 2}>
{row.getVisibleCells().map((cell, cellIndex) => {
const isFocusable =
selectedCell?.path === row.original._rowy_ref.path &&
selectedCell?.columnKey === cell.column.id;
return (
<StyledCell
key={cell.id}
data-rowId={row.id}
data-colId={cell.column.id}
role="gridcell"
tabIndex={isFocusable ? 0 : -1}
aria-colindex={cellIndex + 1}
aria-readonly={
cell.column.columnDef.meta?.editable === false
}
aria-selected={isFocusable}
style={{ width: cell.column.getSize() }}
onClick={(e) => {
setSelectedCell({
path: row.original._rowy_ref.path,
columnKey: cell.column.id,
});
(e.target as HTMLDivElement).focus();
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
<button tabIndex={isFocusable && focusInsideCell ? 0 : -1}>
{isFocusable ? "f" : "x"}
</button>
</StyledCell>
);
})}
</StyledRow>
))}
</div>