mirror of
https://github.com/docmost/docmost.git
synced 2026-06-22 10:51:49 +10:00
fix grid cells on mobile
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { memo, useCallback, useMemo, useRef } from "react";
|
import { memo, useCallback, useMemo } from "react";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
import { Cell } from "@tanstack/react-table";
|
import { Cell } from "@tanstack/react-table";
|
||||||
import { Popover, Tooltip } from "@mantine/core";
|
import { Popover, Tooltip } from "@mantine/core";
|
||||||
import { IconArrowsDiagonal } from "@tabler/icons-react";
|
import { IconArrowsDiagonal } from "@tabler/icons-react";
|
||||||
@@ -24,8 +25,6 @@ import { useRowExpand } from "@/ee/base/context/row-expand";
|
|||||||
import { RowNumberCell } from "./row-number-cell";
|
import { RowNumberCell } from "./row-number-cell";
|
||||||
import classes from "@/ee/base/styles/grid.module.css";
|
import classes from "@/ee/base/styles/grid.module.css";
|
||||||
|
|
||||||
const TOUCH_TAP_SLOP_PX = 10;
|
|
||||||
|
|
||||||
type GridCellProps = {
|
type GridCellProps = {
|
||||||
cell: Cell<IBaseRow, unknown>;
|
cell: Cell<IBaseRow, unknown>;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
@@ -74,18 +73,14 @@ export const GridCell = memo(function GridCell({
|
|||||||
editingCell?.propertyId === property?.id &&
|
editingCell?.propertyId === property?.id &&
|
||||||
(editable || property?.type === "file");
|
(editable || property?.type === "file");
|
||||||
|
|
||||||
const tapStartRef = useRef<{ x: number; y: number } | null>(null);
|
const handleEdit = useCallback(() => {
|
||||||
const suppressClickRef = useRef(false);
|
|
||||||
const expandClickGuardRef = useRef(false);
|
|
||||||
|
|
||||||
const handleDoubleClick = useCallback(() => {
|
|
||||||
if (!property || isRowNumber) return;
|
if (!property || isRowNumber) return;
|
||||||
if (property.type === "checkbox") return;
|
if (property.type === "checkbox") return;
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
// Read-only: only the file cell opens (a download-only popover) so
|
// Read-only: only the file cell opens (a download-only popover) so
|
||||||
// attachments stay reachable.
|
// attachments stay reachable.
|
||||||
if (property.type === "file") {
|
if (property.type === "file") {
|
||||||
setEditingCell({ rowId, propertyId: property.id });
|
flushSync(() => setEditingCell({ rowId, propertyId: property.id }));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,7 +89,7 @@ export const GridCell = memo(function GridCell({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isSystemPropertyType(property.type)) return;
|
if (isSystemPropertyType(property.type)) return;
|
||||||
setEditingCell({ rowId, propertyId: property.id });
|
flushSync(() => setEditingCell({ rowId, propertyId: property.id }));
|
||||||
}, [property, isRowNumber, rowId, readOnly, setEditingCell, setActiveFormulaEditor]);
|
}, [property, isRowNumber, rowId, readOnly, setEditingCell, setActiveFormulaEditor]);
|
||||||
|
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
@@ -108,10 +103,6 @@ export const GridCell = memo(function GridCell({
|
|||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (!property) return;
|
if (!property) return;
|
||||||
if (suppressClickRef.current) {
|
|
||||||
suppressClickRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFocusedCell({ rowId, propertyId: property.id });
|
setFocusedCell({ rowId, propertyId: property.id });
|
||||||
(e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({
|
(e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({
|
||||||
preventScroll: true,
|
preventScroll: true,
|
||||||
@@ -120,46 +111,6 @@ export const GridCell = memo(function GridCell({
|
|||||||
[property, rowId, setFocusedCell],
|
[property, rowId, setFocusedCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
|
||||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
||||||
expandClickGuardRef.current = false;
|
|
||||||
if (e.pointerType === "mouse") return;
|
|
||||||
suppressClickRef.current = false;
|
|
||||||
tapStartRef.current = { x: e.clientX, y: e.clientY };
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePointerUp = useCallback(
|
|
||||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
||||||
if (e.pointerType === "mouse") return;
|
|
||||||
const start = tapStartRef.current;
|
|
||||||
tapStartRef.current = null;
|
|
||||||
if (!start) return;
|
|
||||||
if (
|
|
||||||
Math.abs(e.clientX - start.x) > TOUCH_TAP_SLOP_PX ||
|
|
||||||
Math.abs(e.clientY - start.y) > TOUCH_TAP_SLOP_PX
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (onExpandRow && target.closest("[data-base-row-expand]")) {
|
|
||||||
suppressClickRef.current = true;
|
|
||||||
expandClickGuardRef.current = true;
|
|
||||||
onExpandRow(rowId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.closest("button, a, input")) return;
|
|
||||||
suppressClickRef.current = true;
|
|
||||||
handleDoubleClick();
|
|
||||||
},
|
|
||||||
[handleDoubleClick, onExpandRow, rowId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePointerCancel = useCallback(() => {
|
|
||||||
tapStartRef.current = null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const cellReadOnly = property
|
const cellReadOnly = property
|
||||||
? readOnly || isSystemPropertyType(property.type)
|
? readOnly || isSystemPropertyType(property.type)
|
||||||
: false;
|
: false;
|
||||||
@@ -249,10 +200,7 @@ export const GridCell = memo(function GridCell({
|
|||||||
}
|
}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleEdit}
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
onPointerCancel={handlePointerCancel}
|
|
||||||
>
|
>
|
||||||
<CellComponent
|
<CellComponent
|
||||||
value={value}
|
value={value}
|
||||||
@@ -273,13 +221,7 @@ export const GridCell = memo(function GridCell({
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
data-base-row-expand=""
|
data-base-row-expand=""
|
||||||
className={classes.rowExpandButton}
|
className={classes.rowExpandButton}
|
||||||
onClick={() => {
|
onClick={() => onExpandRow(rowId)}
|
||||||
if (expandClickGuardRef.current) {
|
|
||||||
expandClickGuardRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onExpandRow(rowId);
|
|
||||||
}}
|
|
||||||
onDoubleClick={(e) => e.stopPropagation()}
|
onDoubleClick={(e) => e.stopPropagation()}
|
||||||
aria-label={t("Expand row {{number}}", { number: rowIndex + 1 })}
|
aria-label={t("Expand row {{number}}", { number: rowIndex + 1 })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -276,6 +276,12 @@ export function GridContainer({
|
|||||||
[virtualizer, pinnedLeftWidth],
|
[virtualizer, pinnedLeftWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editingCell) return;
|
||||||
|
const idx = rowIdsRef.current.indexOf(editingCell.rowId);
|
||||||
|
if (idx >= 0) scrollCellIntoView(editingCell, idx);
|
||||||
|
}, [editingCell, scrollCellIntoView]);
|
||||||
|
|
||||||
const openEditor = useCallback(
|
const openEditor = useCallback(
|
||||||
(coord: CellCoord) => {
|
(coord: CellCoord) => {
|
||||||
const prop = properties.find((p) => p.id === coord.propertyId);
|
const prop = properties.find((p) => p.id === coord.propertyId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { useStore, type PrimitiveAtom } from "jotai";
|
import { useStore, type PrimitiveAtom } from "jotai";
|
||||||
import { pendingTypeInsertAtom, type PendingTypeInsert } from "@/ee/base/atoms/base-atoms";
|
import { pendingTypeInsertAtom, type PendingTypeInsert } from "@/ee/base/atoms/base-atoms";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export function useEditableTextCell({
|
|||||||
toDraftRef.current = toDraft;
|
toDraftRef.current = toDraft;
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (isEditing && !wasEditingRef.current) {
|
if (isEditing && !wasEditingRef.current) {
|
||||||
committedRef.current = false;
|
committedRef.current = false;
|
||||||
const pending = store.get(pendingTypeInsertAtom);
|
const pending = store.get(pendingTypeInsertAtom);
|
||||||
@@ -49,27 +49,21 @@ export function useEditableTextCell({
|
|||||||
pending != null &&
|
pending != null &&
|
||||||
pending.rowId === rowId &&
|
pending.rowId === rowId &&
|
||||||
pending.propertyId === propertyId;
|
pending.propertyId === propertyId;
|
||||||
|
const nextDraft = seeded ? pending.char : toDraftRef.current(value);
|
||||||
if (seeded) {
|
if (seeded) {
|
||||||
setDraft(pending.char);
|
|
||||||
store.set(pendingTypeInsertAtom as PrimitiveAtom<PendingTypeInsert>, null);
|
store.set(pendingTypeInsertAtom as PrimitiveAtom<PendingTypeInsert>, null);
|
||||||
requestAnimationFrame(() => {
|
}
|
||||||
const el = inputRef.current;
|
setDraft(nextDraft);
|
||||||
if (el) {
|
const el = inputRef.current;
|
||||||
el.focus();
|
if (el) {
|
||||||
const len = el.value.length;
|
el.value = nextDraft;
|
||||||
el.setSelectionRange(len, len);
|
el.focus({ preventScroll: true });
|
||||||
}
|
try {
|
||||||
});
|
el.setSelectionRange(nextDraft.length, nextDraft.length);
|
||||||
} else {
|
} catch {
|
||||||
setDraft(toDraftRef.current(value));
|
// email/number inputs reject setSelectionRange
|
||||||
requestAnimationFrame(() => {
|
}
|
||||||
const el = inputRef.current;
|
el.scrollLeft = el.scrollWidth;
|
||||||
if (el) {
|
|
||||||
el.focus();
|
|
||||||
const len = el.value.length;
|
|
||||||
el.setSelectionRange(len, len);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wasEditingRef.current = isEditing;
|
wasEditingRef.current = isEditing;
|
||||||
|
|||||||
@@ -184,11 +184,13 @@
|
|||||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
|
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:hover .cell {
|
@media (hover: hover) {
|
||||||
background-color: light-dark(
|
.row:hover .cell {
|
||||||
var(--mantine-color-gray-0),
|
background-color: light-dark(
|
||||||
var(--mantine-color-dark-7)
|
var(--mantine-color-gray-0),
|
||||||
);
|
var(--mantine-color-dark-7)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
@@ -196,6 +198,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
|
touch-action: manipulation;
|
||||||
font-size: var(--mantine-font-size-sm);
|
font-size: var(--mantine-font-size-sm);
|
||||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||||
background-color: light-dark(
|
background-color: light-dark(
|
||||||
@@ -227,11 +230,13 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:hover .cellPinned {
|
@media (hover: hover) {
|
||||||
background-color: light-dark(
|
.row:hover .cellPinned {
|
||||||
var(--mantine-color-gray-0),
|
background-color: light-dark(
|
||||||
var(--mantine-color-dark-7)
|
var(--mantine-color-gray-0),
|
||||||
);
|
var(--mantine-color-dark-7)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cellEditing {
|
.cellEditing {
|
||||||
@@ -247,6 +252,14 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.cellFocused {
|
||||||
|
outline: 2px solid var(--mantine-color-blue-5);
|
||||||
|
outline-offset: -2px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.headerCell:focus-visible {
|
.headerCell:focus-visible {
|
||||||
outline: 2px solid var(--mantine-color-blue-5);
|
outline: 2px solid var(--mantine-color-blue-5);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
@@ -470,6 +483,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
touch-action: manipulation;
|
||||||
transition: opacity 80ms ease, color 80ms ease;
|
transition: opacity 80ms ease, color 80ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,17 +500,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rowExpandButton:hover {
|
@media (hover: hover) {
|
||||||
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
.rowExpandButton:hover {
|
||||||
color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
|
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||||
|
color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:hover .rowNumberIndex {
|
@media (hover: hover) {
|
||||||
display: none;
|
.row:hover .rowNumberIndex {
|
||||||
}
|
display: none;
|
||||||
.row:hover .rowNumberCheckbox,
|
}
|
||||||
.row:hover .rowNumberDragHandle {
|
.row:hover .rowNumberCheckbox,
|
||||||
display: inline-flex;
|
.row:hover .rowNumberDragHandle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rowSelected .rowNumberIndex {
|
.rowSelected .rowNumberIndex {
|
||||||
@@ -511,13 +529,23 @@
|
|||||||
.bodyGrid:focus .cellFocused .rowNumberCheckbox {
|
.bodyGrid:focus .cellFocused .rowNumberCheckbox {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
@media (hover: none) {
|
||||||
|
.cellFocused .rowNumberIndex {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.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));
|
||||||
}
|
}
|
||||||
|
|
||||||
.row.rowSelected:hover .cell,
|
@media (hover: hover) {
|
||||||
.row.rowSelected:hover .cellPinned {
|
.row.rowSelected:hover .cell,
|
||||||
background-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-5));
|
.row.rowSelected:hover .cellPinned {
|
||||||
|
background-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-5));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rowNumberHeaderInner {
|
.rowNumberHeaderInner {
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 21621440ef...2211f14b2c
Reference in New Issue
Block a user