feat(KB-264): add word wrap toggle and fix markdown preview scrolling
- Add word wrap toggle button with WrapText icon to FileEditor - Fix markdown preview scrolling with height: 100% CSS - Add comprehensive tests for word wrap toggle and scrollability
This commit is contained in:
@@ -177,4 +177,81 @@ describe("FileEditor", () => {
|
|||||||
expect(editButton).toBeDisabled();
|
expect(editButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("word wrap toggle", () => {
|
||||||
|
it("shows word wrap toggle button for markdown files in edit mode", () => {
|
||||||
|
render(<FileEditor content="# Hello" onChange={vi.fn()} filePath="readme.md" />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /toggle word wrap/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows word wrap toggle button for non-markdown files", () => {
|
||||||
|
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /toggle word wrap/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show word wrap toggle button in readOnly mode", () => {
|
||||||
|
render(<FileEditor content="# Hello" onChange={vi.fn()} filePath="readme.md" readOnly />);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("button", { name: /toggle word wrap/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("word wrap is enabled by default", () => {
|
||||||
|
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
|
||||||
|
|
||||||
|
const textarea = screen.getByRole("textbox");
|
||||||
|
expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggle button shows active state when word wrap is enabled", () => {
|
||||||
|
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
|
||||||
|
|
||||||
|
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
|
||||||
|
expect(wrapButton.classList.contains("btn-primary")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking toggle button disables word wrap", () => {
|
||||||
|
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
|
||||||
|
|
||||||
|
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
|
||||||
|
fireEvent.click(wrapButton);
|
||||||
|
|
||||||
|
const textarea = screen.getByRole("textbox");
|
||||||
|
expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking toggle button again re-enables word wrap", () => {
|
||||||
|
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
|
||||||
|
|
||||||
|
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
|
||||||
|
fireEvent.click(wrapButton); // turn off
|
||||||
|
fireEvent.click(wrapButton); // turn on
|
||||||
|
|
||||||
|
const textarea = screen.getByRole("textbox");
|
||||||
|
expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggle button loses active state when word wrap is disabled", () => {
|
||||||
|
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
|
||||||
|
|
||||||
|
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
|
||||||
|
fireEvent.click(wrapButton);
|
||||||
|
|
||||||
|
expect(wrapButton.classList.contains("btn-primary")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("markdown preview scrollability", () => {
|
||||||
|
it("preview container has correct CSS classes for scrolling", () => {
|
||||||
|
render(<FileEditor content="# Hello World" onChange={vi.fn()} filePath="readme.md" />);
|
||||||
|
|
||||||
|
// Switch to preview mode
|
||||||
|
const previewButton = screen.getByRole("button", { name: /preview/i });
|
||||||
|
fireEvent.click(previewButton);
|
||||||
|
|
||||||
|
const preview = document.querySelector(".file-editor-preview");
|
||||||
|
expect(preview).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { FileEdit, Eye } from "lucide-react";
|
import { FileEdit, Eye, WrapText } from "lucide-react";
|
||||||
|
|
||||||
interface FileEditorProps {
|
interface FileEditorProps {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -18,6 +18,7 @@ function isMarkdownFile(filePath?: string): boolean {
|
|||||||
|
|
||||||
export function FileEditor({ content, onChange, readOnly, filePath }: FileEditorProps) {
|
export function FileEditor({ content, onChange, readOnly, filePath }: FileEditorProps) {
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const [wordWrap, setWordWrap] = useState(true);
|
||||||
const isMarkdown = isMarkdownFile(filePath);
|
const isMarkdown = isMarkdownFile(filePath);
|
||||||
|
|
||||||
// For markdown files in readOnly mode, default to preview
|
// For markdown files in readOnly mode, default to preview
|
||||||
@@ -31,9 +32,13 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
|
|||||||
setShowPreview(true);
|
setShowPreview(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleWordWrapToggle = useCallback(() => {
|
||||||
|
setWordWrap((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-editor-container">
|
<div className="file-editor-container">
|
||||||
{isMarkdown && (
|
{isMarkdown ? (
|
||||||
<div className="file-editor-toolbar">
|
<div className="file-editor-toolbar">
|
||||||
<div className="file-editor-mode-toggle">
|
<div className="file-editor-mode-toggle">
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
@@ -57,7 +62,31 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
|
|||||||
Preview
|
Preview
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||||
|
onClick={handleWordWrapToggle}
|
||||||
|
aria-label="Toggle word wrap"
|
||||||
|
title="Toggle word wrap"
|
||||||
|
>
|
||||||
|
<WrapText size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
!readOnly && (
|
||||||
|
<div className="file-editor-toolbar">
|
||||||
|
<div className="file-editor-mode-toggle" />
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||||
|
onClick={handleWordWrapToggle}
|
||||||
|
aria-label="Toggle word wrap"
|
||||||
|
title="Toggle word wrap"
|
||||||
|
>
|
||||||
|
<WrapText size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{effectiveShowPreview ? (
|
{effectiveShowPreview ? (
|
||||||
@@ -68,7 +97,7 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
className="file-editor-textarea"
|
className={`file-editor-textarea ${wordWrap ? "file-editor-textarea--wrap" : ""}`}
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -6279,9 +6279,10 @@ html .column.drag-over * {
|
|||||||
/* File Editor Preview */
|
/* File Editor Preview */
|
||||||
.file-editor-preview {
|
.file-editor-preview {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Textarea-based file editor */
|
/* Textarea-based file editor */
|
||||||
@@ -6307,6 +6308,11 @@ html .column.drag-over * {
|
|||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-editor-textarea.file-editor-textarea--wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.file-editor-textarea:focus {
|
.file-editor-textarea:focus {
|
||||||
outline: 1px solid var(--border);
|
outline: 1px solid var(--border);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
|
|||||||
Reference in New Issue
Block a user