feat: Introduce a generic inline rename input component for document editing

This commit is contained in:
2025-12-03 12:00:57 +01:00
parent e1dd1990d7
commit e66451afbd
5 changed files with 59 additions and 64 deletions
+14 -3
View File
@@ -62,11 +62,12 @@ const DesktopDocumentContainer: React.FC<React.ComponentProps<typeof DesktopDocu
onDeselect: (ids: string[]) => void; onDeselect: (ids: string[]) => void;
onDocumentActivate?: (id: string, event?: any) => void; onDocumentActivate?: (id: string, event?: any) => void;
selection: string[]; selection: string[];
requestCanvasFocus?: () => void;
}> = React.memo((props) => { }> = 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 // 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 ( return (
<DesktopDocumentCard <DesktopDocumentCard
@@ -194,7 +195,11 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
}, [layoutStore.items]); }, [layoutStore.items]);
const handleShellKeyDown = useCallback(() => { }, []); const handleShellKeyDown = useCallback(() => { }, []);
const focusShell = useCallback(() => { }, []); const focusShell = useCallback(() => {
if (containerRef.current) {
containerRef.current.focus();
}
}, []);
// Tag Interactions // Tag Interactions
const tagInteractions = useDeskTagInteractions({ const tagInteractions = useDeskTagInteractions({
@@ -209,6 +214,11 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
useEffect(() => { useEffect(() => {
const handleWindowKeyDown = (e: KeyboardEvent) => { const handleWindowKeyDown = (e: KeyboardEvent) => {
// Only handle events if the container itself is the target (focused)
if (e.target !== containerRef.current) {
return;
}
// Space preview logic // Space preview logic
if (e.code === 'Space' && selectedDocumentIds.length > 0) { if (e.code === 'Space' && selectedDocumentIds.length > 0) {
const lastId = selectedDocumentIds[selectedDocumentIds.length - 1]; const lastId = selectedDocumentIds[selectedDocumentIds.length - 1];
@@ -422,6 +432,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
handleSelectionChange(Array.from(newSelection)); handleSelectionChange(Array.from(newSelection));
}} }}
selection={selectedDocumentIds} selection={selectedDocumentIds}
requestCanvasFocus={focusShell}
/> />
); );
})} })}
+4 -1
View File
@@ -14,7 +14,8 @@ export const useCardPointer = (
selection: string[], selection: string[],
onSelect: (ids: string[], extend?: boolean) => void, onSelect: (ids: string[], extend?: boolean) => void,
onDeselect: (ids: string[]) => 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<PointerState>('idle'); const [state, setState] = React.useState<PointerState>('idle');
const initialPosition = useRef<{ x: number, y: number } | null>(null); const initialPosition = useRef<{ x: number, y: number } | null>(null);
@@ -55,6 +56,8 @@ export const useCardPointer = (
// Register pointer with card ID // Register pointer with card ID
addPointer(e.pointerId, card.id); addPointer(e.pointerId, card.id);
requestCanvasFocus?.();
setState('click'); setState('click');
initialPosition.current = { x: e.clientX, y: e.clientY }; initialPosition.current = { x: e.clientX, y: e.clientY };
lastPosition.current = { x: e.clientX, y: e.clientY }; lastPosition.current = { x: e.clientX, y: e.clientY };
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons'; import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons';
import InlineRenameInput from './components/InlineRenameInput';
import SelectionAssignmentMenu, { import SelectionAssignmentMenu, {
SelectionAssignmentMenuItem, SelectionAssignmentMenuItem,
type NormalizedSelectionAssignmentItem, type NormalizedSelectionAssignmentItem,
@@ -631,38 +632,21 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
} }
const renderTitleEditForm = (extraClassName?: string) => ( const renderTitleEditForm = (extraClassName?: string) => (
<form className={`doc-title-edit${extraClassName ? ` ${extraClassName}` : ''}`} onSubmit={submitTitleEdit}> <InlineRenameInput
<input value={titleDraft}
value={titleDraft} onChange={(value) => {
onChange={(event) => { setTitleDraft(value);
setTitleDraft(event.target.value); if (titleError) {
if (titleError) { setTitleError(null);
setTitleError(null); }
} }}
}} onSubmit={() => submitTitleEdit({ preventDefault: () => { } } as any)}
onKeyDown={(event) => { onCancel={cancelTitleEdit}
if (event.key === 'Escape') { isSaving={titleSaving}
event.preventDefault(); className={`doc-title-edit${extraClassName ? ` ${extraClassName}` : ''}`}
cancelTitleEdit(); aria-label="Document title"
} autoFocus
}} />
aria-label="Document title"
autoFocus
disabled={titleSaving}
/>
<button type="submit" className="icon-button icon-button--accent" disabled={titleSaving} aria-label="Save title">
<CheckIcon size={16} />
</button>
<button
type="button"
className="icon-button"
onClick={cancelTitleEdit}
disabled={titleSaving}
aria-label="Cancel"
>
<IconX size={16} />
</button>
</form>
); );
const titleMetaDisplay = editableTitle && isTitleEditing const titleMetaDisplay = editableTitle && isTitleEditing
@@ -685,32 +669,21 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
); );
const issuedDisplay = editableIssued && isIssuedEditing ? ( const issuedDisplay = editableIssued && isIssuedEditing ? (
<form className="doc-issued-edit" onSubmit={submitIssuedEdit}> <InlineRenameInput
<input type="date"
type="date" value={issuedDraft}
value={issuedDraft} onChange={(value) => {
onChange={(event) => { setIssuedDraft(value);
setIssuedDraft(event.target.value); if (issuedError) {
if (issuedError) { setIssuedError(null);
setIssuedError(null); }
} }}
}} onSubmit={() => submitIssuedEdit({ preventDefault: () => { } } as any)}
aria-label="Issued on" onCancel={cancelIssuedEdit}
disabled={issuedSaving} isSaving={issuedSaving}
/> className="doc-issued-edit"
<button type="submit" className="icon-button icon-button--accent" disabled={issuedSaving} aria-label="Save issued date"> aria-label="Issued on"
<CheckIcon size={16} /> />
</button>
<button
type="button"
className="icon-button"
onClick={cancelIssuedEdit}
disabled={issuedSaving}
aria-label="Cancel"
>
<IconX size={16} />
</button>
</form>
) : ( ) : (
<> <>
<span className="detail-meta__value">{issuedDateLabel || 'Not set'}</span> <span className="detail-meta__value">{issuedDateLabel || 'Not set'}</span>
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { CheckIcon, CloseIcon } from '../../ui/icons'; import { CheckIcon, CloseIcon } from '../../ui/icons';
interface InlineRenameInputProps { interface InlineRenameInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSubmit' | 'value'> {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onSubmit: () => void; onSubmit: () => void;
@@ -21,11 +21,13 @@ const InlineRenameInput: React.FC<InlineRenameInputProps> = ({
canSubmit = true, canSubmit = true,
inputRef, inputRef,
className = 'doc-title-edit', className = 'doc-title-edit',
type = 'text',
...props
}) => { }) => {
return ( return (
<span className={className}> <span className={className}>
<input <input
type="text" type={type}
ref={inputRef} ref={inputRef}
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
@@ -45,6 +47,7 @@ const InlineRenameInput: React.FC<InlineRenameInputProps> = ({
onCancel(); onCancel();
} }
}} }}
{...props}
/> />
<button <button
type="button" type="button"
@@ -48,6 +48,11 @@ export const useDocumentsNavigation = ({
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => { (event: React.KeyboardEvent) => {
// Only handle events that target the container directly
if (event.target !== event.currentTarget) {
return;
}
const { key, shiftKey } = event; const { key, shiftKey } = event;
const triggers = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar']; const triggers = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar'];
if (!triggers.includes(key)) { if (!triggers.includes(key)) {