mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
add cell navigation
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user