mirror of
https://github.com/docmost/docmost.git
synced 2026-08-28 09:17:06 +08:00
feat(base): keyboard-navigate the row-number column for selection
This commit is contained in:
@@ -125,7 +125,7 @@ export function GridContainer({
|
|||||||
const editingCellRef = useRef(editingCell);
|
const editingCellRef = useRef(editingCell);
|
||||||
editingCellRef.current = editingCell;
|
editingCellRef.current = editingCell;
|
||||||
|
|
||||||
const { selectionCount, clear: clearSelection } = useRowSelection(pageId);
|
const { selectionCount, clear: clearSelection, toggle: toggleRow } = useRowSelection(pageId);
|
||||||
const { deleteSelected } = useDeleteSelectedRows(pageId);
|
const { deleteSelected } = useDeleteSelectedRows(pageId);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -323,6 +323,17 @@ export function GridContainer({
|
|||||||
[editable, properties, setPendingTypeInsert, setEditingCell, openEditor],
|
[editable, properties, setPendingTypeInsert, setEditingCell, openEditor],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleRowSelection = useCallback(
|
||||||
|
(rowId: string) => {
|
||||||
|
toggleRow(rowId, {
|
||||||
|
shiftKey: false,
|
||||||
|
rowIndex: rowIdsRef.current.indexOf(rowId),
|
||||||
|
orderedRowIds: rowIdsRef.current,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[toggleRow],
|
||||||
|
);
|
||||||
|
|
||||||
const prevEditingRef = useRef(editingCell);
|
const prevEditingRef = useRef(editingCell);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prev = prevEditingRef.current;
|
const prev = prevEditingRef.current;
|
||||||
@@ -373,6 +384,7 @@ export function GridContainer({
|
|||||||
selectionCount,
|
selectionCount,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
deleteSelected,
|
deleteSelected,
|
||||||
|
toggleRowSelection,
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeCell = editingCell ?? focusedCell;
|
const activeCell = editingCell ?? focusedCell;
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
const isRowNumber = header.column.id === "__row_number";
|
const isRowNumber = header.column.id === "__row_number";
|
||||||
const isPinned = header.column.getIsPinned();
|
const isPinned = header.column.getIsPinned();
|
||||||
const pinOffset = isPinned ? header.column.getStart("left") : undefined;
|
const pinOffset = isPinned ? header.column.getStart("left") : undefined;
|
||||||
const { selectionCount } = useRowSelection(pageId);
|
const { selectionCount, toggleAll } = useRowSelection(pageId);
|
||||||
const hasSelection = selectionCount > 0;
|
const hasSelection = selectionCount > 0;
|
||||||
const editable = useBaseEditable();
|
const editable = useBaseEditable();
|
||||||
const isHeaderInteractive = editable && !!property && !isRowNumber;
|
const isHeaderInteractive = editable && !!property && !isRowNumber;
|
||||||
|
const isRowNumberHeaderInteractive =
|
||||||
|
isRowNumber && editable && loadedRowIds.length > 0;
|
||||||
|
|
||||||
const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void];
|
const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void];
|
||||||
const menuOpened = activePropertyMenu === header.column.id;
|
const menuOpened = activePropertyMenu === header.column.id;
|
||||||
@@ -209,9 +211,10 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={cellRef}
|
ref={cellRef}
|
||||||
role={isRowNumber ? undefined : "columnheader"}
|
role="columnheader"
|
||||||
tabIndex={isHeaderInteractive ? 0 : undefined}
|
tabIndex={isHeaderInteractive || isRowNumberHeaderInteractive ? 0 : undefined}
|
||||||
aria-haspopup={isHeaderInteractive ? "menu" : undefined}
|
aria-haspopup={isHeaderInteractive ? "menu" : undefined}
|
||||||
|
aria-label={isRowNumberHeaderInteractive ? t("Select all loaded rows") : undefined}
|
||||||
className={`${classes.headerCell} ${isPinned ? classes.headerCellPinned : ""} ${hasSelection ? classes.hasSelection : ""}`}
|
className={`${classes.headerCell} ${isPinned ? classes.headerCellPinned : ""} ${hasSelection ? classes.hasSelection : ""}`}
|
||||||
style={{
|
style={{
|
||||||
...(isPinned
|
...(isPinned
|
||||||
@@ -227,7 +230,11 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleHeaderClick();
|
if (isRowNumber) {
|
||||||
|
if (isRowNumberHeaderInteractive) toggleAll(loadedRowIds);
|
||||||
|
} else {
|
||||||
|
handleHeaderClick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data-dragging={isDragging || undefined}
|
data-dragging={isDragging || undefined}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback, useMemo } from "react";
|
||||||
import { Checkbox } from "@mantine/core";
|
import { Checkbox } from "@mantine/core";
|
||||||
import { IconGripVertical } from "@tabler/icons-react";
|
import { IconGripVertical } from "@tabler/icons-react";
|
||||||
|
import { useAtomValue, useSetAtom, type PrimitiveAtom } from "jotai";
|
||||||
|
import { selectAtom } from "jotai/utils";
|
||||||
import { useRowSelection } from "@/ee/base/hooks/use-row-selection";
|
import { useRowSelection } from "@/ee/base/hooks/use-row-selection";
|
||||||
|
import { focusedCellAtomFamily } from "@/ee/base/atoms/base-atoms";
|
||||||
|
import { FocusedCell } from "@/ee/base/types/base.types";
|
||||||
import { useBaseEditable } from "@/ee/base/context/base-editable";
|
import { useBaseEditable } from "@/ee/base/context/base-editable";
|
||||||
import { useGridRowOrder } from "@/ee/base/context/grid-row-order";
|
import { useGridRowOrder } from "@/ee/base/context/grid-row-order";
|
||||||
import classes from "@/ee/base/styles/grid.module.css";
|
import classes from "@/ee/base/styles/grid.module.css";
|
||||||
@@ -26,6 +30,30 @@ export const RowNumberCell = memo(function RowNumberCell({
|
|||||||
const editable = useBaseEditable();
|
const editable = useBaseEditable();
|
||||||
const getOrderedRowIds = useGridRowOrder();
|
const getOrderedRowIds = useGridRowOrder();
|
||||||
|
|
||||||
|
const setFocusedCell = useSetAtom(
|
||||||
|
focusedCellAtomFamily(pageId) as PrimitiveAtom<FocusedCell>,
|
||||||
|
);
|
||||||
|
const isFocused = useAtomValue(
|
||||||
|
useMemo(
|
||||||
|
() =>
|
||||||
|
selectAtom(
|
||||||
|
focusedCellAtomFamily(pageId),
|
||||||
|
(fc) => fc?.rowId === rowId && fc?.propertyId === "__row_number",
|
||||||
|
),
|
||||||
|
[pageId, rowId],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCellClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
setFocusedCell({ rowId, propertyId: "__row_number" });
|
||||||
|
(e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({
|
||||||
|
preventScroll: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[rowId, setFocusedCell],
|
||||||
|
);
|
||||||
|
|
||||||
const handleCheckboxChange = useCallback(
|
const handleCheckboxChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const nativeEvent = e.nativeEvent as MouseEvent;
|
const nativeEvent = e.nativeEvent as MouseEvent;
|
||||||
@@ -40,12 +68,15 @@ export const RowNumberCell = memo(function RowNumberCell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${classes.cell} ${classes.rowNumberCell} ${isPinned ? classes.cellPinned : ""}`}
|
id={`base-cell-${rowId}-__row_number`}
|
||||||
|
role="gridcell"
|
||||||
|
className={`${classes.cell} ${classes.rowNumberCell} ${isPinned ? classes.cellPinned : ""} ${isFocused ? classes.cellFocused : ""}`}
|
||||||
style={
|
style={
|
||||||
isPinned
|
isPinned
|
||||||
? ({ "--pin-offset": `${pinOffset ?? 0}px` } as React.CSSProperties)
|
? ({ "--pin-offset": `${pinOffset ?? 0}px` } as React.CSSProperties)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onClick={handleCellClick}
|
||||||
>
|
>
|
||||||
<div className={classes.rowNumberCellInner}>
|
<div className={classes.rowNumberCellInner}>
|
||||||
{editable && (
|
{editable && (
|
||||||
@@ -60,6 +91,7 @@ export const RowNumberCell = memo(function RowNumberCell({
|
|||||||
checked={selected}
|
checked={selected}
|
||||||
onChange={handleCheckboxChange}
|
onChange={handleCheckboxChange}
|
||||||
aria-label="Select row"
|
aria-label="Select row"
|
||||||
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const RowNumberHeaderCell = memo(function RowNumberHeaderCell({
|
|||||||
indeterminate={indeterminate}
|
indeterminate={indeterminate}
|
||||||
onChange={() => toggleAll(loadedRowIds)}
|
onChange={() => toggleAll(loadedRowIds)}
|
||||||
aria-label="Select all loaded rows"
|
aria-label="Select all loaded rows"
|
||||||
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type UseGridKeyboardNavOptions = {
|
|||||||
selectionCount: number;
|
selectionCount: number;
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
deleteSelected: () => void | Promise<void>;
|
deleteSelected: () => void | Promise<void>;
|
||||||
|
toggleRowSelection: (rowId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPrintableKey = (e: KeyboardEvent) =>
|
const isPrintableKey = (e: KeyboardEvent) =>
|
||||||
@@ -50,6 +51,7 @@ export function useGridKeyboardNav({
|
|||||||
selectionCount,
|
selectionCount,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
deleteSelected,
|
deleteSelected,
|
||||||
|
toggleRowSelection,
|
||||||
}: UseGridKeyboardNavOptions) {
|
}: UseGridKeyboardNavOptions) {
|
||||||
const getColIds = useCallback(
|
const getColIds = useCallback(
|
||||||
() =>
|
() =>
|
||||||
@@ -60,6 +62,11 @@ export function useGridKeyboardNav({
|
|||||||
[table],
|
[table],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getNavColIds = useCallback(
|
||||||
|
() => table.getVisibleLeafColumns().map((col) => col.id),
|
||||||
|
[table],
|
||||||
|
);
|
||||||
|
|
||||||
const getRowIds = useCallback(
|
const getRowIds = useCallback(
|
||||||
() => table.getRowModel().rows.map((row) => row.id),
|
() => table.getRowModel().rows.map((row) => row.id),
|
||||||
[table],
|
[table],
|
||||||
@@ -187,35 +194,35 @@ export function useGridKeyboardNav({
|
|||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
{
|
{
|
||||||
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, -1, 0, false);
|
const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, -1, 0, false);
|
||||||
if (next) goFocused(next);
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
{
|
{
|
||||||
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 1, 0, false);
|
const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, 1, 0, false);
|
||||||
if (next) goFocused(next);
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
{
|
{
|
||||||
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 0, -1, false);
|
const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, 0, -1, false);
|
||||||
if (next) goFocused(next);
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
{
|
{
|
||||||
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 0, 1, false);
|
const next = computeNextCell(getRowIds(), getNavColIds(), focusedCell, 0, 1, false);
|
||||||
if (next) goFocused(next);
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Tab": {
|
case "Tab": {
|
||||||
const next = computeNextCell(
|
const next = computeNextCell(
|
||||||
getRowIds(),
|
getRowIds(),
|
||||||
getColIds(),
|
getNavColIds(),
|
||||||
focusedCell,
|
focusedCell,
|
||||||
0,
|
0,
|
||||||
e.shiftKey ? -1 : 1,
|
e.shiftKey ? -1 : 1,
|
||||||
@@ -230,10 +237,17 @@ export function useGridKeyboardNav({
|
|||||||
case "Enter":
|
case "Enter":
|
||||||
case "F2":
|
case "F2":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openEditor(focusedCell);
|
if (focusedCell.propertyId === "__row_number") {
|
||||||
|
toggleRowSelection(focusedCell.rowId);
|
||||||
|
} else {
|
||||||
|
openEditor(focusedCell);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
if (e.key === " " && propertyType(focusedCell.propertyId) === "checkbox") {
|
if (e.key === " " && focusedCell.propertyId === "__row_number") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleRowSelection(focusedCell.rowId);
|
||||||
|
} else if (e.key === " " && propertyType(focusedCell.propertyId) === "checkbox") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openEditor(focusedCell);
|
openEditor(focusedCell);
|
||||||
} else if (isPrintableKey(e)) {
|
} else if (isPrintableKey(e)) {
|
||||||
@@ -248,6 +262,7 @@ export function useGridKeyboardNav({
|
|||||||
focusedCell,
|
focusedCell,
|
||||||
getRowIds,
|
getRowIds,
|
||||||
getColIds,
|
getColIds,
|
||||||
|
getNavColIds,
|
||||||
goEditing,
|
goEditing,
|
||||||
goFocused,
|
goFocused,
|
||||||
setEditingCell,
|
setEditingCell,
|
||||||
@@ -259,6 +274,7 @@ export function useGridKeyboardNav({
|
|||||||
selectionCount,
|
selectionCount,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
deleteSelected,
|
deleteSelected,
|
||||||
|
toggleRowSelection,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -498,6 +498,12 @@
|
|||||||
.rowSelected .rowNumberCheckbox {
|
.rowSelected .rowNumberCheckbox {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
.bodyGrid:focus-within .cellFocused .rowNumberIndex {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.bodyGrid:focus-within .cellFocused .rowNumberCheckbox {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
.rowSelected .cell {
|
.rowSelected .cell {
|
||||||
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
|
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
|
||||||
}
|
}
|
||||||
@@ -534,6 +540,13 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerCell:focus-visible .rowNumberHeaderHash {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.headerCell:focus-visible .rowNumberHeaderCheckbox {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.selectionActionBarWrapper {
|
.selectionActionBarWrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
Reference in New Issue
Block a user