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;
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 (
<DesktopDocumentCard
@@ -194,7 +195,11 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
}, [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<DesktopWorkspaceProps> = ({
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<DesktopWorkspaceProps> = ({
handleSelectionChange(Array.from(newSelection));
}}
selection={selectedDocumentIds}
requestCanvasFocus={focusShell}
/>
);
})}
+4 -1
View File
@@ -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<PointerState>('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 };
@@ -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<DocumentSummarySectionProps> = ({
}
const renderTitleEditForm = (extraClassName?: string) => (
<form className={`doc-title-edit${extraClassName ? ` ${extraClassName}` : ''}`} onSubmit={submitTitleEdit}>
<input
value={titleDraft}
onChange={(event) => {
setTitleDraft(event.target.value);
if (titleError) {
setTitleError(null);
}
}}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
cancelTitleEdit();
}
}}
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>
<InlineRenameInput
value={titleDraft}
onChange={(value) => {
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<DocumentSummarySectionProps> = ({
);
const issuedDisplay = editableIssued && isIssuedEditing ? (
<form className="doc-issued-edit" onSubmit={submitIssuedEdit}>
<input
type="date"
value={issuedDraft}
onChange={(event) => {
setIssuedDraft(event.target.value);
if (issuedError) {
setIssuedError(null);
}
}}
aria-label="Issued on"
disabled={issuedSaving}
/>
<button type="submit" className="icon-button icon-button--accent" disabled={issuedSaving} aria-label="Save issued date">
<CheckIcon size={16} />
</button>
<button
type="button"
className="icon-button"
onClick={cancelIssuedEdit}
disabled={issuedSaving}
aria-label="Cancel"
>
<IconX size={16} />
</button>
</form>
<InlineRenameInput
type="date"
value={issuedDraft}
onChange={(value) => {
setIssuedDraft(value);
if (issuedError) {
setIssuedError(null);
}
}}
onSubmit={() => submitIssuedEdit({ preventDefault: () => { } } as any)}
onCancel={cancelIssuedEdit}
isSaving={issuedSaving}
className="doc-issued-edit"
aria-label="Issued on"
/>
) : (
<>
<span className="detail-meta__value">{issuedDateLabel || 'Not set'}</span>
@@ -1,7 +1,7 @@
import React from 'react';
import { CheckIcon, CloseIcon } from '../../ui/icons';
interface InlineRenameInputProps {
interface InlineRenameInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSubmit' | 'value'> {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
@@ -21,11 +21,13 @@ const InlineRenameInput: React.FC<InlineRenameInputProps> = ({
canSubmit = true,
inputRef,
className = 'doc-title-edit',
type = 'text',
...props
}) => {
return (
<span className={className}>
<input
type="text"
type={type}
ref={inputRef}
value={value}
onChange={(event) => onChange(event.target.value)}
@@ -45,6 +47,7 @@ const InlineRenameInput: React.FC<InlineRenameInputProps> = ({
onCancel();
}
}}
{...props}
/>
<button
type="button"
@@ -48,6 +48,11 @@ export const useDocumentsNavigation = ({
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
// Only handle events that target the container directly
if (event.target !== event.currentTarget) {
return;
}
const { key, shiftKey } = event;
const triggers = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar'];
if (!triggers.includes(key)) {