fix(base): place caret at end instead of selecting all when editing cells

This commit is contained in:
Philipinho
2026-06-16 21:40:15 +01:00
parent 2e80fc457c
commit 0e3549faf1
4 changed files with 78 additions and 20 deletions
@@ -76,6 +76,7 @@ export const GridCell = memo(function GridCell({
const tapStartRef = useRef<{ x: number; y: number } | null>(null);
const suppressClickRef = useRef(false);
const expandClickGuardRef = useRef(false);
const handleDoubleClick = useCallback(() => {
if (!property || isRowNumber) return;
@@ -121,6 +122,7 @@ export const GridCell = memo(function GridCell({
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 };
@@ -140,11 +142,18 @@ export const GridCell = memo(function GridCell({
) {
return;
}
if ((e.target as HTMLElement).closest("button, a, input")) 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],
[handleDoubleClick, onExpandRow, rowId],
);
const handlePointerCancel = useCallback(() => {
@@ -262,8 +271,15 @@ export const GridCell = memo(function GridCell({
<button
type="button"
tabIndex={-1}
data-base-row-expand=""
className={classes.rowExpandButton}
onClick={() => onExpandRow(rowId)}
onClick={() => {
if (expandClickGuardRef.current) {
expandClickGuardRef.current = false;
return;
}
onExpandRow(rowId);
}}
onDoubleClick={(e) => e.stopPropagation()}
aria-label={t("Expand row {{number}}", { number: rowIndex + 1 })}
>
@@ -52,6 +52,9 @@ export function CreatePropertyPopover({ pageId, properties, onPropertyCreated, r
const [dropdownNode, setDropdownNode] = useState<HTMLDivElement | null>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<"bottom-start" | "top-start">(
"bottom-start",
);
const createPropertyMutation = useCreatePropertyMutation();
@@ -95,10 +98,20 @@ export function CreatePropertyPopover({ pageId, properties, onPropertyCreated, r
setTypeOptions({});
}, []);
const handleOpen = useCallback(() => {
resetState();
setOpened(true);
}, [resetState]);
const handleOpen = useCallback(
(event?: React.SyntheticEvent) => {
resetState();
const trigger = event?.currentTarget as HTMLElement | undefined;
if (trigger) {
const rect = trigger.getBoundingClientRect();
const spaceAbove = rect.top;
const spaceBelow = window.innerHeight - rect.bottom;
setPosition(spaceAbove > spaceBelow ? "top-start" : "bottom-start");
}
setOpened(true);
},
[resetState],
);
const handleClose = useCallback(() => {
// Don't reset state here: resetting mid-close flashes the type picker.
@@ -217,13 +230,13 @@ export function CreatePropertyPopover({ pageId, properties, onPropertyCreated, r
onChange={(o) => {
if (!o) attemptClose();
}}
position="bottom-start"
position={position}
shadow="md"
closeOnClickOutside
closeOnEscape={false}
withinPortal
middlewares={{
flip: true,
flip: false,
shift: true,
size: {
padding: 8,
@@ -21,6 +21,8 @@ import {
import {
IBaseProperty,
BasePropertyType,
TypeOptions,
SelectTypeOptions,
} from "@/ee/base/types/base.types";
import { useAtom } from "jotai";
import { propertyMenuCloseRequestAtomFamily } from "@/ee/base/atoms/base-atoms";
@@ -36,7 +38,11 @@ import {
NON_USER_TARGET_TYPES,
} from "./conversion-warning";
import { useTranslation } from "react-i18next";
import { isSystemPropertyType, propertyTypes } from "@/ee/base/property-types/property-type.registry";
import {
isSystemPropertyType,
propertyTypes,
defaultTypeOptionsFor,
} from "@/ee/base/property-types/property-type.registry";
import cellClasses from "@/ee/base/styles/cells.module.css";
import classes from "@/ee/base/styles/property.module.css";
@@ -58,6 +64,31 @@ type MenuPanel =
| "confirmDelete"
| "confirmDiscard";
const CHOICE_TYPES = new Set<BasePropertyType>([
"select",
"multiSelect",
"status",
]);
function typeOptionsForConversion(
source: IBaseProperty,
target: BasePropertyType,
): TypeOptions {
if (!CHOICE_TYPES.has(source.type) || !CHOICE_TYPES.has(target)) {
return defaultTypeOptionsFor(target);
}
const opts = source.typeOptions as SelectTypeOptions | undefined;
const choices = opts?.choices ?? [];
const choiceOrder = opts?.choiceOrder?.length
? opts.choiceOrder
: choices.map((c) => c.id);
const carried: SelectTypeOptions = { choices, choiceOrder };
if (target === "status") {
carried.defaultValue = choices[0]?.id ?? null;
}
return carried;
}
export function PropertyMenuContent({
property,
opened,
@@ -185,16 +216,10 @@ export function PropertyMenuContent({
propertyId: property.id,
pageId: property.pageId,
type: pendingTargetType,
typeOptions: {},
typeOptions: typeOptionsForConversion(property, pendingTargetType),
});
onClose();
}, [
pendingTargetType,
property.id,
property.pageId,
updatePropertyMutation,
onClose,
]);
}, [pendingTargetType, property, updatePropertyMutation, onClose]);
const handleDelete = useCallback(() => {
deletePropertyMutation.mutate({
@@ -63,8 +63,12 @@ export function useEditableTextCell({
} else {
setDraft(toDraftRef.current(value));
requestAnimationFrame(() => {
inputRef.current?.focus();
inputRef.current?.select();
const el = inputRef.current;
if (el) {
el.focus();
const len = el.value.length;
el.setSelectionRange(len, len);
}
});
}
}