feat: Introduce a generic inline rename input component for document editing
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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={(event) => {
|
onChange={(value) => {
|
||||||
setTitleDraft(event.target.value);
|
setTitleDraft(value);
|
||||||
if (titleError) {
|
if (titleError) {
|
||||||
setTitleError(null);
|
setTitleError(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onSubmit={() => submitTitleEdit({ preventDefault: () => { } } as any)}
|
||||||
if (event.key === 'Escape') {
|
onCancel={cancelTitleEdit}
|
||||||
event.preventDefault();
|
isSaving={titleSaving}
|
||||||
cancelTitleEdit();
|
className={`doc-title-edit${extraClassName ? ` ${extraClassName}` : ''}`}
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-label="Document title"
|
aria-label="Document title"
|
||||||
autoFocus
|
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={(event) => {
|
onChange={(value) => {
|
||||||
setIssuedDraft(event.target.value);
|
setIssuedDraft(value);
|
||||||
if (issuedError) {
|
if (issuedError) {
|
||||||
setIssuedError(null);
|
setIssuedError(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onSubmit={() => submitIssuedEdit({ preventDefault: () => { } } as any)}
|
||||||
|
onCancel={cancelIssuedEdit}
|
||||||
|
isSaving={issuedSaving}
|
||||||
|
className="doc-issued-edit"
|
||||||
aria-label="Issued on"
|
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>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user