fix(FN-1458): add safe-area-inset padding to mobile header floating search
- Add safe-area-inset padding to mobile header-floating-search to prevent viewport overlap - Add regression tests in Header.test.tsx for mobile search safe-area handling - Add regression tests in mobile-header-controls.test.tsx for viewport safety - Document the fix in .fusion/memory.md
This commit is contained in:
@@ -218,6 +218,12 @@ Tasks can get into contradictory states (e.g., `column: "done"` with `status: "b
|
|||||||
- Regex tests using `[\s\S]*` (greedy match across lines) to check CSS rules inside `@media` blocks are unreliable — they can match across block boundaries. Use non-greedy `[^}]*` scoped to a single rule block instead.
|
- Regex tests using `[\s\S]*` (greedy match across lines) to check CSS rules inside `@media` blocks are unreliable — they can match across block boundaries. Use non-greedy `[^}]*` scoped to a single rule block instead.
|
||||||
- Touch target sizing in `styles.css` mobile media queries uses 36px (reduced from the original 44px). The `.touch-target` opt-in utility class remains at 44px. Comments mentioning "44px" in the mobile sections have been updated to reflect the actual values.
|
- Touch target sizing in `styles.css` mobile media queries uses 36px (reduced from the original 44px). The `.touch-target` opt-in utility class remains at 44px. Comments mentioning "44px" in the mobile sections have been updated to reflect the actual values.
|
||||||
|
|
||||||
|
## FN-1458: Mobile Header Search Safe-Area-Inset Fix
|
||||||
|
|
||||||
|
- When fixing mobile header search positioning issues (search box clipping off-screen), add safe-area-inset handling to both `.header` and `.header-floating-search` in the mobile media query
|
||||||
|
- Use `padding-left: max(var(--space-md), env(safe-area-inset-left, 0px))` pattern to ensure content respects device notches
|
||||||
|
- CSS regression tests should verify: (1) `.mobile-search-expanded` has `width: 100%`, (2) no fixed negative offsets (`left: -NNpx`, `right: -NNpx`) exist in mobile search rules, (3) `.header-floating-search` has safe-area-inset handling
|
||||||
|
|
||||||
## UX Audit Findings (FN-1379)
|
## UX Audit Findings (FN-1379)
|
||||||
|
|
||||||
- **Header overload**: The Header component (`Header.tsx`) has 15+ icon buttons with no labels, making discovery difficult. Consider grouping secondary actions into overflow menus.
|
- **Header overload**: The Header component (`Header.tsx`) has 15+ icon buttons with no labels, making discovery difficult. Consider grouping secondary actions into overflow menus.
|
||||||
|
|||||||
@@ -126,4 +126,23 @@ describe("mobile-header-controls.css", () => {
|
|||||||
// The selector should have width: 100% rule
|
// The selector should have width: 100% rule
|
||||||
expect(mobileCss).toMatch(/\.mobile-search-expanded\s*\{[^}]*width:\s*100%/);
|
expect(mobileCss).toMatch(/\.mobile-search-expanded\s*\{[^}]*width:\s*100%/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not contain fixed-offset patterns that can push mobile search off-screen", () => {
|
||||||
|
// Extract all mobile .mobile-search-expanded rules and check they don't have
|
||||||
|
// position: absolute with negative left/right offsets
|
||||||
|
const expandedBlocks = mobileCss.match(/\.mobile-search-expanded\s*\{[^}]*\}/g) || [];
|
||||||
|
for (const block of expandedBlocks) {
|
||||||
|
// Fail if any fixed negative offset is found (these push the element off-screen)
|
||||||
|
expect(block).not.toMatch(/left:\s*-\d/);
|
||||||
|
expect(block).not.toMatch(/right:\s*-\d/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has mobile header-floating-search with safe-area-inset padding", () => {
|
||||||
|
// The header-floating-search in mobile must respect safe-area-inset
|
||||||
|
// to prevent clipping on notched devices
|
||||||
|
expect(mobileCss).toContain(".header-floating-search");
|
||||||
|
// Should have safe-area-inset handling for left/right
|
||||||
|
expect(mobileCss).toMatch(/\.header-floating-search\s*\{[^}]*env\(safe-area-inset/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -902,6 +902,31 @@ describe("Header", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mobile search with mobileNavEnabled", () => {
|
||||||
|
it("renders mobile search input when searchQuery is active with mobileNavEnabled", () => {
|
||||||
|
renderHeader({ view: "board", searchQuery: "test query", onSearchChange: vi.fn(), onChangeView: noop }, "mobile");
|
||||||
|
// Search should be visible even with mobileNavEnabled when query is active
|
||||||
|
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
||||||
|
expect(screen.getByDisplayValue("test query")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can open mobile search when mobileNavEnabled is true", () => {
|
||||||
|
renderHeader({ view: "board", searchQuery: "", onSearchChange: vi.fn(), onChangeView: noop }, "mobile");
|
||||||
|
// Should show the trigger button
|
||||||
|
expect(screen.getByTestId("mobile-header-search-btn")).toBeDefined();
|
||||||
|
// Expanded search should not be visible initially
|
||||||
|
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes mobile search and clears query when close button clicked with mobileNavEnabled", () => {
|
||||||
|
const onSearchChange = vi.fn();
|
||||||
|
renderHeader({ view: "board", searchQuery: "test query", onSearchChange, onChangeView: noop }, "mobile");
|
||||||
|
const closeBtn = screen.getByLabelText("Close search");
|
||||||
|
fireEvent.click(closeBtn);
|
||||||
|
expect(onSearchChange).toHaveBeenCalledWith("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Projects button", () => {
|
describe("Projects button", () => {
|
||||||
const singleProject = [
|
const singleProject = [
|
||||||
{ id: "1", name: "Test Project", path: "/path/to/project", status: "active" as const },
|
{ id: "1", name: "Test Project", path: "/path/to/project", status: "active" as const },
|
||||||
|
|||||||
@@ -946,6 +946,57 @@ describe("Header", () => {
|
|||||||
fireEvent.click(screen.getByTestId("overflow-scripts-manage"));
|
fireEvent.click(screen.getByTestId("overflow-scripts-manage"));
|
||||||
expect(onOpenScripts).toHaveBeenCalledOnce();
|
expect(onOpenScripts).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Mobile Search with mobileNavEnabled ───────────────────────
|
||||||
|
|
||||||
|
it("renders mobile search input when searchQuery is active with mobileNavEnabled", () => {
|
||||||
|
const onSearchChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<Header
|
||||||
|
view="board"
|
||||||
|
onChangeView={vi.fn()}
|
||||||
|
searchQuery="test query"
|
||||||
|
onSearchChange={onSearchChange}
|
||||||
|
mobileNavEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// Search should be visible even with mobileNavEnabled when query is active
|
||||||
|
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
||||||
|
expect(screen.getByDisplayValue("test query")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can open mobile search when mobileNavEnabled is true", () => {
|
||||||
|
const onSearchChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<Header
|
||||||
|
view="board"
|
||||||
|
onChangeView={vi.fn()}
|
||||||
|
searchQuery=""
|
||||||
|
onSearchChange={onSearchChange}
|
||||||
|
mobileNavEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// Should show the trigger button
|
||||||
|
expect(screen.getByTestId("mobile-header-search-btn")).toBeDefined();
|
||||||
|
// Expanded search should not be visible initially
|
||||||
|
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes mobile search and clears query when close button clicked with mobileNavEnabled", () => {
|
||||||
|
const onSearchChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<Header
|
||||||
|
view="board"
|
||||||
|
onChangeView={vi.fn()}
|
||||||
|
searchQuery="test query"
|
||||||
|
onSearchChange={onSearchChange}
|
||||||
|
mobileNavEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const closeBtn = screen.getByLabelText("Close search");
|
||||||
|
fireEvent.click(closeBtn);
|
||||||
|
expect(onSearchChange).toHaveBeenCalledWith("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Multi-Project Selector ────────────────────────────────────
|
// ── Multi-Project Selector ────────────────────────────────────
|
||||||
|
|||||||
@@ -6403,6 +6403,12 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: floating search must respect safe-area-inset to stay on-screen */
|
||||||
|
.header-floating-search {
|
||||||
|
padding-left: max(var(--space-md), env(safe-area-inset-left, 0px));
|
||||||
|
padding-right: max(var(--space-md), env(safe-area-inset-right, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal: full-screen on mobile */
|
/* Modal: full-screen on mobile */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user