From 4f49503e7a20bd54649b6b8fface6aeb88f26747 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 17 Oct 2022 17:57:26 +1100 Subject: [PATCH] add cell navigation --- src/atoms/tableScope/ui.ts | 5 +- src/components/Table/Styled/StyledTable.tsx | 11 + src/components/Table/Table.tsx | 218 +++++++++++++++++--- 3 files changed, 207 insertions(+), 27 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index fa987118..a26ea065 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -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(null); diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx index fc855036..ab65a84c 100644 --- a/src/components/Table/Styled/StyledTable.tsx +++ b/src/components/Table/Styled/StyledTable.tsx @@ -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"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 6dbddcaf..547016a7 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -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(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) => {}; + const handleKeyDown = (e: React.KeyboardEvent) => { + 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 ( <> -
+
{table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {/*
+ {headerGroup.headers.map((header) => { + const isFocusable = + (!selectedCell && header.index === 0) || + (selectedCell?.path === "_rowy_header" && + selectedCell?.columnKey === header.id); + + return ( + { + setSelectedCell({ + path: "_rowy_header", + columnKey: header.id, + }); + (e.target as HTMLDivElement).focus(); + }} + > + {/*
*/} - - ))} + + ); + })} ))}
-
+
{table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - - ))} + + {row.getVisibleCells().map((cell, cellIndex) => { + const isFocusable = + selectedCell?.path === row.original._rowy_ref.path && + selectedCell?.columnKey === cell.column.id; + + return ( + { + setSelectedCell({ + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + }); + (e.target as HTMLDivElement).focus(); + }} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + + ); + })} ))}