From e66451afbd18e3b57c4621bb6128764897772821 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 3 Dec 2025 12:00:57 +0100 Subject: [PATCH] feat: Introduce a generic inline rename input component for document editing --- frontend/src/desktop/DesktopWorkspace.tsx | 17 +++- frontend/src/desktop/useCardPointer.ts | 5 +- .../src/documents/DocumentSummarySection.tsx | 89 +++++++------------ .../components/InlineRenameInput.tsx | 7 +- .../documents/hooks/useDocumentsNavigation.ts | 5 ++ 5 files changed, 59 insertions(+), 64 deletions(-) diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index e3c5c25..d360361 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -62,11 +62,12 @@ const DesktopDocumentContainer: React.FC void; onDocumentActivate?: (id: string, event?: any) => void; selection: string[]; + requestCanvasFocus?: () => void; }> = React.memo((props) => { - const { layoutCard, selected, onSelect, onDeselect, onDocumentActivate, selection } = props; + const { layoutCard, selected, onSelect, onDeselect, onDocumentActivate, selection, requestCanvasFocus } = props; // We assume layoutCard is always present in this context - const cardPointerHandlers = useCardPointer(layoutCard!, !!selected, selection, onSelect, onDeselect, onDocumentActivate); + const cardPointerHandlers = useCardPointer(layoutCard!, !!selected, selection, onSelect, onDeselect, onDocumentActivate, requestCanvasFocus); return ( = ({ }, [layoutStore.items]); const handleShellKeyDown = useCallback(() => { }, []); - const focusShell = useCallback(() => { }, []); + const focusShell = useCallback(() => { + if (containerRef.current) { + containerRef.current.focus(); + } + }, []); // Tag Interactions const tagInteractions = useDeskTagInteractions({ @@ -209,6 +214,11 @@ const DesktopWorkspaceContent: React.FC = ({ useEffect(() => { const handleWindowKeyDown = (e: KeyboardEvent) => { + // Only handle events if the container itself is the target (focused) + if (e.target !== containerRef.current) { + return; + } + // Space preview logic if (e.code === 'Space' && selectedDocumentIds.length > 0) { const lastId = selectedDocumentIds[selectedDocumentIds.length - 1]; @@ -422,6 +432,7 @@ const DesktopWorkspaceContent: React.FC = ({ handleSelectionChange(Array.from(newSelection)); }} selection={selectedDocumentIds} + requestCanvasFocus={focusShell} /> ); })} diff --git a/frontend/src/desktop/useCardPointer.ts b/frontend/src/desktop/useCardPointer.ts index 03b8475..7b22e1b 100644 --- a/frontend/src/desktop/useCardPointer.ts +++ b/frontend/src/desktop/useCardPointer.ts @@ -14,7 +14,8 @@ export const useCardPointer = ( selection: string[], onSelect: (ids: string[], extend?: boolean) => void, onDeselect: (ids: string[]) => void, - onDocumentActivate?: (id: string, event?: React.PointerEvent) => void + onDocumentActivate?: (id: string, event?: React.PointerEvent) => void, + requestCanvasFocus?: () => void ) => { const [state, setState] = React.useState('idle'); const initialPosition = useRef<{ x: number, y: number } | null>(null); @@ -55,6 +56,8 @@ export const useCardPointer = ( // Register pointer with card ID addPointer(e.pointerId, card.id); + requestCanvasFocus?.(); + setState('click'); initialPosition.current = { x: e.clientX, y: e.clientY }; lastPosition.current = { x: e.clientX, y: e.clientY }; diff --git a/frontend/src/documents/DocumentSummarySection.tsx b/frontend/src/documents/DocumentSummarySection.tsx index 34b5e1e..dc258f4 100644 --- a/frontend/src/documents/DocumentSummarySection.tsx +++ b/frontend/src/documents/DocumentSummarySection.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react'; import { Link } from 'react-router-dom'; import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons'; +import InlineRenameInput from './components/InlineRenameInput'; import SelectionAssignmentMenu, { SelectionAssignmentMenuItem, type NormalizedSelectionAssignmentItem, @@ -631,38 +632,21 @@ const DocumentSummarySection: React.FC = ({ } const renderTitleEditForm = (extraClassName?: string) => ( -
- { - setTitleDraft(event.target.value); - if (titleError) { - setTitleError(null); - } - }} - onKeyDown={(event) => { - if (event.key === 'Escape') { - event.preventDefault(); - cancelTitleEdit(); - } - }} - aria-label="Document title" - autoFocus - disabled={titleSaving} - /> - - -
+ { + setTitleDraft(value); + if (titleError) { + setTitleError(null); + } + }} + onSubmit={() => submitTitleEdit({ preventDefault: () => { } } as any)} + onCancel={cancelTitleEdit} + isSaving={titleSaving} + className={`doc-title-edit${extraClassName ? ` ${extraClassName}` : ''}`} + aria-label="Document title" + autoFocus + /> ); const titleMetaDisplay = editableTitle && isTitleEditing @@ -685,32 +669,21 @@ const DocumentSummarySection: React.FC = ({ ); const issuedDisplay = editableIssued && isIssuedEditing ? ( -
- { - setIssuedDraft(event.target.value); - if (issuedError) { - setIssuedError(null); - } - }} - aria-label="Issued on" - disabled={issuedSaving} - /> - - -
+ { + setIssuedDraft(value); + if (issuedError) { + setIssuedError(null); + } + }} + onSubmit={() => submitIssuedEdit({ preventDefault: () => { } } as any)} + onCancel={cancelIssuedEdit} + isSaving={issuedSaving} + className="doc-issued-edit" + aria-label="Issued on" + /> ) : ( <> {issuedDateLabel || 'Not set'} diff --git a/frontend/src/documents/components/InlineRenameInput.tsx b/frontend/src/documents/components/InlineRenameInput.tsx index 839546f..07f40c7 100644 --- a/frontend/src/documents/components/InlineRenameInput.tsx +++ b/frontend/src/documents/components/InlineRenameInput.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { CheckIcon, CloseIcon } from '../../ui/icons'; -interface InlineRenameInputProps { +interface InlineRenameInputProps extends Omit, 'onChange' | 'onSubmit' | 'value'> { value: string; onChange: (value: string) => void; onSubmit: () => void; @@ -21,11 +21,13 @@ const InlineRenameInput: React.FC = ({ canSubmit = true, inputRef, className = 'doc-title-edit', + type = 'text', + ...props }) => { return ( onChange(event.target.value)} @@ -45,6 +47,7 @@ const InlineRenameInput: React.FC = ({ onCancel(); } }} + {...props} />