feat(KB-234): add double-click edit and Escape key handling to SpecEditor
- Add double-click handler to SpecEditor for quick edit mode entry - Implement Escape key handler to exit edit mode - Add comprehensive double-click interaction tests - Add CSS styles for improved editor UX
This commit is contained in:
@@ -47,7 +47,7 @@ export function SpecEditor({
|
|||||||
setFeedback("");
|
setFeedback("");
|
||||||
}, [onRequestRevision, feedback, isRequesting]);
|
}, [onRequestRevision, feedback, isRequesting]);
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl/Cmd+Enter to save in edit mode
|
// Keyboard shortcut: Ctrl/Cmd+Enter to save in edit mode, Escape to cancel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (isEditing && (e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
if (isEditing && (e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
@@ -55,6 +55,9 @@ export function SpecEditor({
|
|||||||
if (canSave) {
|
if (canSave) {
|
||||||
void handleSave();
|
void handleSave();
|
||||||
}
|
}
|
||||||
|
} else if (isEditing && e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCancelEdit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +75,13 @@ export function SpecEditor({
|
|||||||
setEditContent(content);
|
setEditContent(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!readOnly && !isEditing) {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEnterEditMode();
|
||||||
|
}
|
||||||
|
}, [readOnly, isEditing, content]);
|
||||||
|
|
||||||
const stripLeadingHeading = (text: string): string => {
|
const stripLeadingHeading = (text: string): string => {
|
||||||
return text.replace(/^#\s+[^\n]*\n+/, "");
|
return text.replace(/^#\s+[^\n]*\n+/, "");
|
||||||
};
|
};
|
||||||
@@ -130,7 +140,7 @@ export function SpecEditor({
|
|||||||
placeholder="Enter task specification in Markdown..."
|
placeholder="Enter task specification in Markdown..."
|
||||||
/>
|
/>
|
||||||
) : content ? (
|
) : content ? (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body" onDoubleClick={handleDoubleClick}>
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
{stripLeadingHeading(content)}
|
{stripLeadingHeading(content)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|||||||
@@ -85,6 +85,63 @@ Test mission description.
|
|||||||
expect(screen.getByText("Test mission description.")).toBeTruthy();
|
expect(screen.getByText("Test mission description.")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enters edit mode on double-click when not in readOnly mode", () => {
|
||||||
|
render(<SpecEditor content={mockContent} onSave={mockOnSave} />);
|
||||||
|
|
||||||
|
// Double-click on the markdown body
|
||||||
|
const markdownBody = document.querySelector(".markdown-body");
|
||||||
|
expect(markdownBody).toBeTruthy();
|
||||||
|
fireEvent.doubleClick(markdownBody!);
|
||||||
|
|
||||||
|
// Should now be in edit mode
|
||||||
|
expect(screen.getByRole("textbox")).toBeTruthy();
|
||||||
|
const editButton = screen.getByText("Edit") as HTMLButtonElement;
|
||||||
|
expect(editButton.classList.contains("btn-primary")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not enter edit mode on double-click when readOnly is true", () => {
|
||||||
|
render(<SpecEditor content={mockContent} readOnly={true} />);
|
||||||
|
|
||||||
|
// Double-click on the markdown body
|
||||||
|
const markdownBody = document.querySelector(".markdown-body");
|
||||||
|
expect(markdownBody).toBeTruthy();
|
||||||
|
fireEvent.doubleClick(markdownBody!);
|
||||||
|
|
||||||
|
// Should NOT be in edit mode (no textarea)
|
||||||
|
expect(screen.queryByRole("textbox")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not enter edit mode on double-click when already in edit mode", () => {
|
||||||
|
render(<SpecEditor content={mockContent} onSave={mockOnSave} />);
|
||||||
|
|
||||||
|
// Enter edit mode via Edit button
|
||||||
|
fireEvent.click(screen.getByText("Edit"));
|
||||||
|
expect(screen.getByRole("textbox")).toBeTruthy();
|
||||||
|
|
||||||
|
// Double-clicking again should not break anything
|
||||||
|
const markdownBody = document.querySelector(".markdown-body");
|
||||||
|
// Note: in edit mode, markdown-body is not rendered, so this query returns null
|
||||||
|
expect(markdownBody).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels edit mode when pressing Escape key", () => {
|
||||||
|
render(<SpecEditor content={mockContent} onSave={mockOnSave} />);
|
||||||
|
|
||||||
|
// Enter edit mode
|
||||||
|
fireEvent.click(screen.getByText("Edit"));
|
||||||
|
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
// Make a change
|
||||||
|
fireEvent.change(textarea, { target: { value: "Changed content" } });
|
||||||
|
|
||||||
|
// Press Escape
|
||||||
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||||
|
|
||||||
|
// Should be back in view mode
|
||||||
|
expect(screen.queryByRole("textbox")).toBeNull();
|
||||||
|
expect(screen.getByText("Test mission description.")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("disables Edit button when already in edit mode", () => {
|
it("disables Edit button when already in edit mode", () => {
|
||||||
render(<SpecEditor content={mockContent} onSave={mockOnSave} />);
|
render(<SpecEditor content={mockContent} onSave={mockOnSave} />);
|
||||||
|
|
||||||
|
|||||||
@@ -7857,6 +7857,11 @@ html .column.drag-over * {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Double-click to edit cursor - only when not readOnly */
|
||||||
|
.spec-editor:not(.spec-editor-readonly) .markdown-body {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toolbar - fixed at top */
|
/* Toolbar - fixed at top */
|
||||||
.spec-editor-toolbar {
|
.spec-editor-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user