From 880ed2870d769b5947e56188c0b22daa41ab5dc4 Mon Sep 17 00:00:00 2001 From: Salihu Date: Tue, 1 Sep 2026 18:43:31 +0100 Subject: [PATCH] align input shortcuts --- .../row-detail-modal/fields/detail-field.tsx | 5 ++- .../fields/field-long-text.tsx | 14 ++++++- .../row-detail-modal/fields/field-number.tsx | 10 ++++- .../row-detail-modal/fields/field-text.tsx | 14 ++++++- .../row-detail-modal/property-row.tsx | 3 ++ .../row-detail-modal/row-detail-modal.tsx | 39 +++++++++++++++---- .../row-detail-modal/row-detail-title.tsx | 35 +++++++++++------ .../base/styles/row-detail-modal.module.css | 16 ++++++++ 8 files changed, 111 insertions(+), 25 deletions(-) diff --git a/apps/client/src/ee/base/components/row-detail-modal/fields/detail-field.tsx b/apps/client/src/ee/base/components/row-detail-modal/fields/detail-field.tsx index c3e8d8e60..5e2b0ad08 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/fields/detail-field.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/fields/detail-field.tsx @@ -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) { diff --git a/apps/client/src/ee/base/components/row-detail-modal/fields/field-long-text.tsx b/apps/client/src/ee/base/components/row-detail-modal/fields/field-long-text.tsx index 89501ed65..7fd11e8a0 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/fields/field-long-text.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/fields/field-long-text.tsx @@ -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) => { diff --git a/apps/client/src/ee/base/components/row-detail-modal/fields/field-number.tsx b/apps/client/src/ee/base/components/row-detail-modal/fields/field-number.tsx index b9495b375..62ab2255f 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/fields/field-number.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/fields/field-number.tsx @@ -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; diff --git a/apps/client/src/ee/base/components/row-detail-modal/fields/field-text.tsx b/apps/client/src/ee/base/components/row-detail-modal/fields/field-text.tsx index e211563a9..dbf1abbdf 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/fields/field-text.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/fields/field-text.tsx @@ -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) => { diff --git a/apps/client/src/ee/base/components/row-detail-modal/property-row.tsx b/apps/client/src/ee/base/components/row-detail-modal/property-row.tsx index d825d3c03..8658f6db4 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/property-row.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/property-row.tsx @@ -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} /> ); diff --git a/apps/client/src/ee/base/components/row-detail-modal/row-detail-modal.tsx b/apps/client/src/ee/base/components/row-detail-modal/row-detail-modal.tsx index 7d1cfdce9..d43d8c6bc 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/row-detail-modal.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/row-detail-modal.tsx @@ -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}
- {rowIndex >= 0 && rows.length > 1 && ( + {editingField ? ( <> - - - {t("to navigate")} + + Ctrl/Cmd + + + Enter + {t("to save")} + + + + + Esc + {t("to reset")} + + + ) : ( + <> + {rowIndex >= 0 && rows.length > 1 && ( + <> + + + {t("to navigate")} + + + )} + <> + Esc + {t("to close")} + )} - Esc - {t("to close")}
diff --git a/apps/client/src/ee/base/components/row-detail-modal/row-detail-title.tsx b/apps/client/src/ee/base/components/row-detail-modal/row-detail-title.tsx index 03f568b69..4c17d479c 100644 --- a/apps/client/src/ee/base/components/row-detail-modal/row-detail-title.tsx +++ b/apps/client/src/ee/base/components/row-detail-modal/row-detail-title.tsx @@ -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(); } }} /> diff --git a/apps/client/src/ee/base/styles/row-detail-modal.module.css b/apps/client/src/ee/base/styles/row-detail-modal.module.css index 650dfe73c..290996cee 100644 --- a/apps/client/src/ee/base/styles/row-detail-modal.module.css +++ b/apps/client/src/ee/base/styles/row-detail-modal.module.css @@ -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 {