feat(FN-1324): add fullscreen expand to task creation components
- Add Maximize2/Minimize2 toggle button to QuickEntryBox for fullscreen textarea expansion - Add Maximize2/Minimize2 toggle button to InlineCreateCard for fullscreen textarea expansion - Implement single-textarea pattern with positioned container for expand button overlay - Add description--fullscreen CSS class for fullscreen textarea styling - Add focus handoff when entering fullscreen mode - Add comprehensive unit tests for fullscreen expansion in both components - Tests cover collapsed/expanded states, toggle button visibility, and class application
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp, Bot } from "lucide-react";
|
||||
import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp, Bot, Maximize2, Minimize2 } from "lucide-react";
|
||||
import type { Task, TaskCreateInput, Settings } from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents } from "../api";
|
||||
@@ -99,6 +99,10 @@ export function InlineCreateCard({
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// isDescriptionExpanded controls fullscreen description editing mode
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
// Track textarea focus for expand button visibility
|
||||
const [isDescriptionFocused, setIsDescriptionFocused] = useState(false);
|
||||
const justResetRef = useRef(false);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
@@ -348,6 +352,7 @@ export function InlineCreateCard({
|
||||
|
||||
// Collapse and clear localStorage after successful task creation
|
||||
setIsExpanded(false);
|
||||
setIsDescriptionExpanded(false); // Exit fullscreen mode on submit
|
||||
justResetRef.current = true;
|
||||
if (typeof window !== "undefined") {
|
||||
removeScopedItem(STORAGE_KEY, projectId);
|
||||
@@ -380,6 +385,11 @@ export function InlineCreateCard({
|
||||
async (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
// Exit fullscreen mode first - highest priority
|
||||
if (isDescriptionExpanded) {
|
||||
setIsDescriptionExpanded(false);
|
||||
return;
|
||||
}
|
||||
// Close dropdowns first if open
|
||||
if (showDeps || showAgentPicker || isModelModalOpen || showPresets) {
|
||||
setShowDeps(false);
|
||||
@@ -414,6 +424,7 @@ export function InlineCreateCard({
|
||||
handleSubmit,
|
||||
onCancel,
|
||||
description,
|
||||
isDescriptionExpanded,
|
||||
showDeps,
|
||||
showAgentPicker,
|
||||
isModelModalOpen,
|
||||
@@ -579,38 +590,116 @@ export function InlineCreateCard({
|
||||
setIsExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleToggleDescriptionExpand = useCallback(() => {
|
||||
setIsDescriptionExpanded((prev) => {
|
||||
const next = !prev;
|
||||
// Focus the fullscreen textarea after it renders
|
||||
if (next && inputRef.current) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDescriptionFullscreenKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isDescriptionExpanded || e.key !== "Escape") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDescriptionExpanded(false);
|
||||
}, [isDescriptionExpanded]);
|
||||
|
||||
return (
|
||||
<div className={`inline-create-card ${isExpanded ? "inline-create-card--expanded" : "inline-create-card--collapsed"}`} ref={cardRef}>
|
||||
<div className="inline-create-main-row">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
rows={1}
|
||||
className="inline-create-input"
|
||||
placeholder="What needs to be done?"
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
const el = e.target;
|
||||
el.style.height = "auto";
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
disabled={submitting}
|
||||
aria-controls={isExpanded ? "inline-create-controls" : undefined}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm inline-create-toggle"
|
||||
onClick={toggleExpanded}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={isExpanded ? "inline-create-controls" : undefined}
|
||||
aria-label={isExpanded ? "Collapse advanced task options" : "Expand advanced task options"}
|
||||
data-testid="inline-create-toggle"
|
||||
title={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
<div
|
||||
className={`description-with-refine${isDescriptionExpanded ? " description--fullscreen" : ""}`}
|
||||
onKeyDown={handleDescriptionFullscreenKeyDown}
|
||||
>
|
||||
{isDescriptionExpanded && (
|
||||
<div className="description-fullscreen-header">
|
||||
<span>Editing Description</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm description-expand-btn"
|
||||
onClick={handleToggleDescriptionExpand}
|
||||
aria-label="Collapse description"
|
||||
title="Collapse description"
|
||||
data-testid="inline-create-collapse"
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isDescriptionExpanded && (
|
||||
<div className="inline-create-main-row">
|
||||
<div className="inline-create-textarea-wrap">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
rows={1}
|
||||
className="inline-create-input"
|
||||
placeholder="What needs to be done?"
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
const el = e.target;
|
||||
el.style.height = "auto";
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={() => setIsDescriptionFocused(true)}
|
||||
onBlur={() => setIsDescriptionFocused(false)}
|
||||
disabled={submitting}
|
||||
aria-controls={isExpanded ? "inline-create-controls" : undefined}
|
||||
/>
|
||||
{isDescriptionFocused && description.trim() && !submitting && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm inline-create-expand-btn"
|
||||
onClick={handleToggleDescriptionExpand}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
aria-label="Expand description"
|
||||
title="Expand description"
|
||||
data-testid="inline-create-expand"
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm inline-create-toggle"
|
||||
onClick={toggleExpanded}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={isExpanded ? "inline-create-controls" : undefined}
|
||||
aria-label={isExpanded ? "Collapse advanced task options" : "Expand advanced task options"}
|
||||
data-testid="inline-create-toggle"
|
||||
title={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isDescriptionExpanded && (
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
rows={10}
|
||||
className="inline-create-input inline-create-input--fullscreen"
|
||||
placeholder="What needs to be done?"
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
const el = e.target;
|
||||
el.style.height = "auto";
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={() => setIsDescriptionFocused(true)}
|
||||
onBlur={() => setIsDescriptionFocused(false)}
|
||||
disabled={submitting}
|
||||
data-testid="inline-create-input-fullscreen"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{pendingImages.length > 0 && (
|
||||
<div className="inline-create-previews">
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ToastType } from "../hooks/useToast";
|
||||
import type { Task, TaskCreateInput, Settings } from "@fusion/core";
|
||||
import type { ModelInfo, RefinementType, Agent } from "../api";
|
||||
import { fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings, fetchAgents, uploadAttachment } from "../api";
|
||||
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot } from "lucide-react";
|
||||
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
|
||||
@@ -92,6 +92,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// isDisclosureExpanded controls visibility of the controls panel (Deps, Models, etc.)
|
||||
// Always starts collapsed — user must explicitly toggle each session
|
||||
const [isDisclosureExpanded, setIsDisclosureExpanded] = useState(false);
|
||||
// isDescriptionExpanded controls fullscreen description editing mode
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
// Track textarea focus for expand button visibility
|
||||
const [isDescriptionFocused, setIsDescriptionFocused] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const justResetRef = useRef(false);
|
||||
@@ -254,14 +258,17 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// In fullscreen mode, CSS handles sizing via flex: 1, don't set inline height
|
||||
if (isDescriptionExpanded) return;
|
||||
|
||||
// Reset height to auto to get accurate scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
// Set to scrollHeight (capped at max-height via CSS)
|
||||
const newHeight = Math.min(textarea.scrollHeight, 200);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
}, []);
|
||||
}, [isDescriptionExpanded]);
|
||||
|
||||
// Resize when description changes
|
||||
// Resize when description changes (not in fullscreen mode since CSS handles it)
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
autoResize();
|
||||
@@ -362,6 +369,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setIsRefining(false);
|
||||
setIsExpanded(false); // Collapse textarea height on reset
|
||||
setIsDisclosureExpanded(false); // Always reset controls to collapsed after creation
|
||||
setIsDescriptionExpanded(false); // Exit fullscreen mode on reset
|
||||
justResetRef.current = true;
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
@@ -469,8 +477,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.shiftKey && isExpanded) {
|
||||
// Allow Shift+Enter to insert newline when expanded
|
||||
if (e.shiftKey && (isExpanded || isDescriptionExpanded)) {
|
||||
// Allow Shift+Enter to insert newline when expanded or in fullscreen mode
|
||||
// Don't prevent default - let the newline be inserted
|
||||
return;
|
||||
}
|
||||
@@ -479,6 +487,11 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
handleSubmit();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
// Exit fullscreen mode first - highest priority
|
||||
if (isDescriptionExpanded) {
|
||||
setIsDescriptionExpanded(false);
|
||||
return;
|
||||
}
|
||||
// Close model submenu first if open
|
||||
if (activeModelSubmenu) {
|
||||
setActiveModelSubmenu(null);
|
||||
@@ -524,6 +537,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
handleSubmit,
|
||||
description,
|
||||
isExpanded,
|
||||
isDescriptionExpanded,
|
||||
showDeps,
|
||||
showAgentPicker,
|
||||
isModelMenuOpen,
|
||||
@@ -540,6 +554,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
if (justResetRef.current) {
|
||||
justResetRef.current = false;
|
||||
}
|
||||
// Track description focus for expand button visibility
|
||||
setIsDescriptionFocused(false);
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
@@ -547,6 +563,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
if (autoExpand) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
// Track description focus for expand button visibility
|
||||
setIsDescriptionFocused(true);
|
||||
}, [autoExpand]);
|
||||
|
||||
const toggleDep = useCallback((id: string) => {
|
||||
@@ -898,36 +916,108 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleDescriptionExpand = useCallback(() => {
|
||||
setIsDescriptionExpanded((prev) => {
|
||||
const next = !prev;
|
||||
// Focus the fullscreen textarea after it renders
|
||||
if (next && textareaRef.current) {
|
||||
// Use setTimeout to ensure focus happens after the textarea is rendered
|
||||
setTimeout(() => textareaRef.current?.focus(), 0);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDescriptionFullscreenKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isDescriptionExpanded || e.key !== "Escape") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDescriptionExpanded(false);
|
||||
}, [isDescriptionExpanded]);
|
||||
|
||||
return (
|
||||
<div className={`quick-entry-box ${isDisclosureExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
|
||||
<div className="quick-entry-main-row">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={`quick-entry-input ${isExpanded ? "quick-entry-input--expanded" : ""}`}
|
||||
placeholder={isSubmitting ? "Creating..." : "Add a task..."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input"
|
||||
rows={1}
|
||||
aria-controls="quick-entry-controls"
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm quick-entry-toggle"
|
||||
onClick={toggleExpanded}
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
aria-controls="quick-entry-controls"
|
||||
data-testid="quick-entry-toggle"
|
||||
title={isDisclosureExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
<div
|
||||
className={`description-with-refine${isDescriptionExpanded ? " description--fullscreen" : ""}`}
|
||||
onKeyDown={handleDescriptionFullscreenKeyDown}
|
||||
>
|
||||
{isDescriptionExpanded && (
|
||||
<div className="description-fullscreen-header">
|
||||
<span>Editing Description</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm description-expand-btn"
|
||||
onClick={handleToggleDescriptionExpand}
|
||||
aria-label="Collapse description"
|
||||
title="Collapse description"
|
||||
data-testid="quick-entry-collapse"
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isDescriptionExpanded && (
|
||||
<div className="quick-entry-main-row">
|
||||
<div className="quick-entry-textarea-wrap">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={`quick-entry-input ${isExpanded ? "quick-entry-input--expanded" : ""}`}
|
||||
placeholder={isSubmitting ? "Creating..." : "Add a task..."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input"
|
||||
rows={1}
|
||||
aria-controls="quick-entry-controls"
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
/>
|
||||
{isDescriptionFocused && description.trim() && !isDisabled && !isSubmitting && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm quick-entry-expand-btn"
|
||||
onClick={handleToggleDescriptionExpand}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
aria-label="Expand description"
|
||||
title="Expand description"
|
||||
data-testid="quick-entry-expand"
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm quick-entry-toggle"
|
||||
onClick={toggleExpanded}
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
aria-controls="quick-entry-controls"
|
||||
data-testid="quick-entry-toggle"
|
||||
title={isDisclosureExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isDescriptionExpanded && (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="quick-entry-input quick-entry-input--fullscreen"
|
||||
placeholder={isSubmitting ? "Creating..." : "Add a task..."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input-fullscreen"
|
||||
rows={10}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
id="quick-entry-controls"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
||||
import { InlineCreateCard } from "../InlineCreateCard";
|
||||
import type { Task, Column } from "@fusion/core";
|
||||
import { fetchModels, fetchSettings, fetchAgents } from "../../api";
|
||||
@@ -21,6 +21,8 @@ vi.mock("lucide-react", () => ({
|
||||
ChevronDown: () => null,
|
||||
ChevronUp: () => null,
|
||||
Bot: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
}));
|
||||
|
||||
// Mock ModelSelectionModal (renders via portal, so mock for testability)
|
||||
@@ -1081,4 +1083,144 @@ describe("InlineCreateCard button visibility when collapsed", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("description fullscreen expansion", () => {
|
||||
it("shows expand button when textarea is focused and has content", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Focus and type content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test task description" } });
|
||||
|
||||
// Expand button should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("inline-create-expand")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides expand button when textarea is empty", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Focus without typing
|
||||
fireEvent.focus(textarea);
|
||||
|
||||
// Expand button should not be visible when empty
|
||||
expect(screen.queryByTestId("inline-create-expand")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides expand button when textarea is blurred", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Focus, type, then blur
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test content" } });
|
||||
|
||||
// Verify expand button is visible before blur
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("inline-create-expand")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Blur the textarea
|
||||
await act(async () => {
|
||||
fireEvent.blur(textarea);
|
||||
});
|
||||
|
||||
// Expand button should be hidden after blur
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("inline-create-expand")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("enters fullscreen mode when expand button is clicked", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Focus and type content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
|
||||
// Click expand button
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("inline-create-expand"));
|
||||
});
|
||||
|
||||
// Fullscreen textarea should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("inline-create-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Collapse button should be visible
|
||||
expect(screen.getByTestId("inline-create-collapse")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("exits fullscreen mode when collapse button is clicked", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Enter fullscreen mode
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("inline-create-expand"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("inline-create-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click collapse button
|
||||
fireEvent.click(screen.getByTestId("inline-create-collapse"));
|
||||
|
||||
// Fullscreen textarea should be hidden
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("inline-create-input-fullscreen")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("exits fullscreen mode when Escape key is pressed in fullscreen", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Enter fullscreen mode
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("inline-create-expand"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("inline-create-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Press Escape
|
||||
const fullscreenTextarea = screen.getByTestId("inline-create-input-fullscreen");
|
||||
fireEvent.keyDown(fullscreenTextarea, { key: "Escape" });
|
||||
|
||||
// Fullscreen should be exited
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("inline-create-input-fullscreen")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves description text when entering fullscreen", async () => {
|
||||
renderCard();
|
||||
const textarea = screen.getByPlaceholderText("What needs to be done?");
|
||||
|
||||
// Type some content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "My test task description" } });
|
||||
|
||||
// Enter fullscreen
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("inline-create-expand"));
|
||||
});
|
||||
|
||||
// Check fullscreen textarea has the content
|
||||
await waitFor(() => {
|
||||
const fullscreenTextarea = screen.getByTestId("inline-create-input-fullscreen") as HTMLTextAreaElement;
|
||||
expect(fullscreenTextarea.value).toBe("My test task description");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,6 +114,8 @@ vi.mock("lucide-react", () => ({
|
||||
ChevronUp: () => null,
|
||||
ChevronRight: () => null,
|
||||
Bot: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
}));
|
||||
|
||||
// Mock ModelSelectionModal (kept for backward compatibility - no longer directly rendered)
|
||||
@@ -2334,4 +2336,136 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("description fullscreen expansion", () => {
|
||||
it("shows expand button when textarea is focused and has content", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus and type content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test task description" } });
|
||||
|
||||
// Expand button should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-expand")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides expand button when textarea is empty", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus without typing
|
||||
fireEvent.focus(textarea);
|
||||
|
||||
// Expand button should not be visible when empty
|
||||
expect(screen.queryByTestId("quick-entry-expand")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides expand button when textarea is blurred", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus, type, then blur
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test content" } });
|
||||
fireEvent.blur(textarea);
|
||||
|
||||
// Expand button should be hidden after blur
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(screen.queryByTestId("quick-entry-expand")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enters fullscreen mode when expand button is clicked", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Focus and type content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
|
||||
// Click expand button
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
|
||||
// Fullscreen textarea should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Collapse button should be visible
|
||||
expect(screen.getByTestId("quick-entry-collapse")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("exits fullscreen mode when collapse button is clicked", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Enter fullscreen mode
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click collapse button
|
||||
fireEvent.click(screen.getByTestId("quick-entry-collapse"));
|
||||
|
||||
// Fullscreen textarea should be hidden
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("quick-entry-input-fullscreen")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("exits fullscreen mode when Escape key is pressed in fullscreen", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Enter fullscreen mode
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "Test description" } });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-input-fullscreen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Press Escape
|
||||
const fullscreenTextarea = screen.getByTestId("quick-entry-input-fullscreen");
|
||||
fireEvent.keyDown(fullscreenTextarea, { key: "Escape" });
|
||||
|
||||
// Fullscreen should be exited
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("quick-entry-input-fullscreen")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves description text when entering fullscreen", async () => {
|
||||
renderQuickEntryBox({});
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
// Type some content
|
||||
fireEvent.focus(textarea);
|
||||
fireEvent.change(textarea, { target: { value: "My test task description" } });
|
||||
|
||||
// Enter fullscreen
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId("quick-entry-expand"));
|
||||
});
|
||||
|
||||
// Check fullscreen textarea has the content
|
||||
await waitFor(() => {
|
||||
const fullscreenTextarea = screen.getByTestId("quick-entry-input-fullscreen") as HTMLTextAreaElement;
|
||||
expect(fullscreenTextarea.value).toBe("My test task description");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user