fix: align input shortcuts (#2467)

* align input shortcuts

* minor fix

* minor fix
This commit is contained in:
Salihu
2026-09-08 01:54:41 +01:00
committed by GitHub
parent 0a87db4f1f
commit 949072744d
8 changed files with 111 additions and 25 deletions
@@ -18,6 +18,7 @@ export type FieldProps = {
rowId: string;
readOnly: boolean;
onChange: (value: unknown) => void;
onEditingChange?: (editing: boolean) => void;
};
type FieldShellProps = {
@@ -99,9 +100,10 @@ type DetailFieldProps = {
row: IBaseRow;
readOnly: boolean;
onUpdate: (propertyId: string, value: unknown) => void;
onEditingChange: (editing: boolean) => void;
};
export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldProps) {
export function DetailField({ property, row, readOnly, onUpdate, onEditingChange }: DetailFieldProps) {
const descriptor = getDescriptor(property.type);
const value = descriptor?.systemAccessor
? descriptor.systemAccessor(row)
@@ -112,6 +114,7 @@ export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldPr
rowId: row.id,
readOnly,
onChange: (next: unknown) => onUpdate(property.id, next),
onEditingChange
};
switch (property.type) {
@@ -9,7 +9,13 @@ const normalize = (s: string) => {
return trimmed.length ? trimmed : null;
};
export function FieldLongText({ property, value, readOnly, onChange }: FieldProps) {
export function FieldLongText({
property,
value,
readOnly,
onChange,
onEditingChange,
}: FieldProps) {
const text = toText(value);
const [draft, setDraft] = useState(text);
const [focused, setFocused] = useState(false);
@@ -23,6 +29,7 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
const commit = () => {
setFocused(false);
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setDraft(text);
@@ -50,7 +57,10 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
className={classes.fieldTextarea}
classNames={{ input: classes.fieldTextareaInput }}
value={draft}
onFocus={() => setFocused(true)}
onFocus={() => {
setFocused(true);
onEditingChange?.(true);
}}
onChange={(e) => setDraft(e.currentTarget.value)}
onBlur={commit}
onKeyDown={(e) => {
@@ -11,7 +11,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
const toDraft = (value: unknown) =>
typeof value === "number" ? String(value) : "";
export function FieldNumber({ property, value, readOnly, onChange }: FieldProps) {
export function FieldNumber({
property,
value,
readOnly,
onChange,
onEditingChange,
}: FieldProps) {
const typeOptions = property.typeOptions as NumberTypeOptions | undefined;
const numValue = typeof value === "number" ? value : null;
const [draft, setDraft] = useState(toDraft(value));
@@ -36,6 +42,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
const commit = () => {
setFocused(false);
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setDraft(toDraft(value));
@@ -54,6 +61,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
onFocus={() => {
setDraft(toDraft(value));
setFocused(true);
onEditingChange?.(true);
}}
onChange={(e) => {
const v = e.target.value;
@@ -5,7 +5,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
const toText = (value: unknown) => (typeof value === "string" ? value : "");
export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
export function FieldText({
property,
value,
readOnly,
onChange,
onEditingChange,
}: FieldProps) {
const text = toText(value);
const [draft, setDraft] = useState(text);
const [focused, setFocused] = useState(false);
@@ -20,6 +26,7 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
const commit = () => {
setFocused(false);
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setDraft(text);
@@ -54,7 +61,10 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
className={classes.fieldInput}
value={draft}
maxLength={1000}
onFocus={() => setFocused(true)}
onFocus={() => {
setFocused(true);
onEditingChange?.(true);
}}
onChange={(e) => setDraft(e.currentTarget.value)}
onBlur={commit}
onKeyDown={(e) => {
@@ -17,6 +17,7 @@ type PropertyRowProps = {
onMenuOpenChange: (opened: boolean) => void;
onMenuDirtyChange: (dirty: boolean) => void;
onUpdate: (propertyId: string, value: unknown) => void;
onEditingChange?: (editing: boolean) => void;
autoFocusValue?: boolean;
onAutoFocused?: () => void;
};
@@ -29,6 +30,7 @@ export function PropertyRow({
onMenuOpenChange,
onMenuDirtyChange,
onUpdate,
onEditingChange,
autoFocusValue,
onAutoFocused,
}: PropertyRowProps) {
@@ -112,6 +114,7 @@ export function PropertyRow({
row={row}
readOnly={!canEdit}
onUpdate={onUpdate}
onEditingChange={onEditingChange}
/>
</div>
);
@@ -75,6 +75,7 @@ export function RowDetailModal({
const isSaving = updateRowMutation.isPending;
const opened = !!openRowId;
const [editingField, setEditingField] = useState(false);
// One field menu open at a time, mirroring the grid header's semantics.
// The shared closeRequest atom asks an open dirty PropertyMenuContent to
@@ -90,6 +91,7 @@ export function RowDetailModal({
useEffect(() => {
setOpenMenuId(null);
menuDirtyRef.current = false;
setEditingField(false);
}, [openRowId]);
const handleMenuDirtyChange = useCallback((dirty: boolean) => {
@@ -293,7 +295,7 @@ export function RowDetailModal({
row={row}
primaryProperty={primaryProperty}
canEdit={canEdit}
onClose={onClose}
onEditingChange={setEditingField}
onCommit={(value) => {
if (!primaryProperty) return;
updateRowMutation.mutate({
@@ -317,6 +319,7 @@ export function RowDetailModal({
autoFocusValue={property.id === newPropertyId}
onAutoFocused={clearNewProperty}
menuOpened={openMenuId === property.id}
onEditingChange={setEditingField}
onMenuOpenChange={(nextOpened) =>
handleMenuOpenChange(property.id, nextOpened)
}
@@ -367,16 +370,38 @@ export function RowDetailModal({
) : null}
</div>
<div className={classes.kbdHint}>
{rowIndex >= 0 && rows.length > 1 && (
{editingField ? (
<>
<kbd className={classes.kbd}></kbd>
<kbd className={classes.kbd}></kbd>
<span>{t("to navigate")}</span>
<span className={classes.kbdGroup}>
<kbd className={classes.kbd}>Ctrl/Cmd</kbd>
<span className={classes.kbdPlus} >+</span>
<kbd className={classes.kbd}>Enter</kbd>
<span>{t("to save")}</span>
</span>
<span className={classes.kbdSeparator} />
<span className={classes.kbdGroup}>
<kbd className={classes.kbd}>Esc</kbd>
<span>{t("to reset")}</span>
</span>
</>
) : (
<>
{rowIndex >= 0 && rows.length > 1 && (
<>
<kbd className={classes.kbd}></kbd>
<kbd className={classes.kbd}></kbd>
<span>{t("to navigate")}</span>
<span className={classes.kbdSeparator} />
</>
)}
<>
<kbd className={classes.kbd}>Esc</kbd>
<span>{t("to close")}</span>
</>
</>
)}
<kbd className={classes.kbd}>Esc</kbd>
<span>{t("to close")}</span>
</div>
</footer>
</>
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { IBaseProperty, IBaseRow } from "@/ee/base/types/base.types";
import { timeAgo } from "@/lib/time.ts";
@@ -9,7 +9,7 @@ type RowDetailTitleProps = {
primaryProperty: IBaseProperty | undefined;
canEdit: boolean;
onCommit: (value: string) => void;
onClose: () => void;
onEditingChange?: (editing: boolean) => void;
};
export function RowDetailTitle({
@@ -17,13 +17,24 @@ export function RowDetailTitle({
primaryProperty,
canEdit,
onCommit,
onClose,
onEditingChange,
}: RowDetailTitleProps) {
const { t } = useTranslation();
const initial = primaryProperty
? (((row.cells ?? {})[primaryProperty.id] as string) ?? "")
: "";
const [value, setValue] = useState(initial);
const cancelRef = useRef(false);
const commit = () => {
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setValue(initial);
return;
}
if (value !== initial) onCommit(value);
};
// Re-sync when the row changes underneath us (navigation or remote edit).
useEffect(() => {
@@ -43,18 +54,18 @@ export function RowDetailTitle({
aria-label={primaryProperty?.name ?? t("Untitled")}
value={value}
maxLength={1000}
onChange={(e) => setValue(e.currentTarget.value)}
onBlur={() => {
if (value !== initial) onCommit(value);
onFocus={() => {
onEditingChange?.(true);
}}
onChange={(e) => setValue(e.currentTarget.value)}
onBlur={commit}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.key === "Escape") {
cancelRef.current = true;
e.currentTarget.blur();
} else if (e.key === "Enter") {
e.preventDefault();
(e.currentTarget as HTMLInputElement).blur();
} else if (e.key === "Escape") {
e.preventDefault();
(e.currentTarget as HTMLInputElement).blur();
onClose();
e.currentTarget.blur();
}
}}
/>
@@ -416,9 +416,25 @@
}
.kbdHint {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
width: 100%;
}
.kbdGroup {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
height: fit-content;
}
.kbdPlus {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
font-size: 11px;
}
.kbdSeparator {