feat(FN-1335): render header search box as floating element below header
- Convert header search box to floating dropdown positioned below header - Add mobile header controls test for floating search behavior - Add Header component tests for search box rendering - Update Header.tsx to render search as floating element with absolute positioning - Add styles for floating search box with backdrop blur and shadow
This commit is contained in:
@@ -712,6 +712,24 @@ describe("Header", () => {
|
||||
const input = screen.getByPlaceholderText("Search tasks...");
|
||||
expect(input).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders search input inside header-floating-search on desktop board view", () => {
|
||||
const { container } = renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
||||
expect(container.querySelector(".header-floating-search .header-search")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not render search input inside header-actions", () => {
|
||||
const { container } = renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
||||
expect(container.querySelector(".header-actions .header-search")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders header-wrapper containing both header and floating search", () => {
|
||||
const { container } = renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
||||
const wrapper = container.querySelector(".header-wrapper");
|
||||
expect(wrapper).not.toBeNull();
|
||||
expect(wrapper.querySelector("header.header")).not.toBeNull();
|
||||
expect(wrapper.querySelector(".header-floating-search")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("schedules button", () => {
|
||||
|
||||
@@ -215,9 +215,10 @@ export function Header({
|
||||
}, [onSearchChange]);
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
<div className="header-brand">
|
||||
<div className="header-wrapper">
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
<div className="header-brand">
|
||||
<svg
|
||||
className="header-logo"
|
||||
width={24}
|
||||
@@ -284,68 +285,18 @@ export function Header({
|
||||
</div>
|
||||
|
||||
<div className="header-actions">
|
||||
{/* Desktop Search - only show in board view */}
|
||||
{onSearchChange && (view === "board" || view === "list") && !isMobile && (
|
||||
<div className="header-search">
|
||||
<Search size={14} className="header-search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tasks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="header-search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="header-search-clear"
|
||||
onClick={() => onSearchChange("")}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Search Trigger - show in board and list views when mobile nav is hidden (hideFullNav) or in board view when mobile nav is visible */}
|
||||
{onSearchChange && isMobile && (hideFullNav || view === "board" || view === "list") && (
|
||||
<>
|
||||
{!shouldShowMobileSearch ? (
|
||||
<button
|
||||
className="btn-icon mobile-search-trigger"
|
||||
onClick={handleMobileSearchToggle}
|
||||
title="Open search"
|
||||
aria-label="Open search"
|
||||
aria-expanded={false}
|
||||
data-testid="mobile-header-search-btn"
|
||||
>
|
||||
<Search size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
ref={mobileSearchRef}
|
||||
className="header-search mobile-search-expanded"
|
||||
>
|
||||
<Search size={14} className="header-search-icon" />
|
||||
<input
|
||||
ref={mobileSearchInputRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Search tasks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="header-search-input"
|
||||
/>
|
||||
<button
|
||||
className="header-search-clear"
|
||||
onClick={handleMobileSearchClose}
|
||||
aria-label="Close search"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{/* Mobile Search Trigger - only on mobile, show trigger button in header */}
|
||||
{onSearchChange && isMobile && (hideFullNav || view === "board" || view === "list") && !shouldShowMobileSearch && (
|
||||
<button
|
||||
className="btn-icon mobile-search-trigger"
|
||||
onClick={handleMobileSearchToggle}
|
||||
title="Open search"
|
||||
aria-label="Open search"
|
||||
aria-expanded={false}
|
||||
data-testid="mobile-header-search-btn"
|
||||
>
|
||||
<Search size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Usage button on mobile when mobile bottom nav is active */}
|
||||
@@ -817,5 +768,59 @@ export function Header({
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
{/* Desktop Search - floating below header, only in board view */}
|
||||
{onSearchChange && view === "board" && !isMobile && (
|
||||
<div className="header-floating-search">
|
||||
<div className="header-search">
|
||||
<Search size={14} className="header-search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tasks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="header-search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="header-search-clear"
|
||||
onClick={() => onSearchChange("")}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Search Expanded - floating below header */}
|
||||
{onSearchChange && isMobile && shouldShowMobileSearch && (
|
||||
<div className="header-floating-search">
|
||||
<div
|
||||
ref={mobileSearchRef}
|
||||
className="header-search mobile-search-expanded"
|
||||
>
|
||||
<Search size={14} className="header-search-icon" />
|
||||
<input
|
||||
ref={mobileSearchInputRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Search tasks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="header-search-input"
|
||||
/>
|
||||
<button
|
||||
className="header-search-clear"
|
||||
onClick={handleMobileSearchClose}
|
||||
aria-label="Close search"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ describe("Header", () => {
|
||||
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows search input when view is 'list'", () => {
|
||||
it("hides search input when view is 'list' on desktop", () => {
|
||||
const onSearchChange = vi.fn();
|
||||
render(
|
||||
<Header
|
||||
@@ -353,7 +353,8 @@ describe("Header", () => {
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
||||
// Desktop search only appears in board view
|
||||
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||
});
|
||||
|
||||
it("hides search input when view is 'agents'", () => {
|
||||
|
||||
Reference in New Issue
Block a user