feat(FN-3341): fix mobile keyboard overlap in TodoModal and PlanningModeMod
Merges FN-3341 mobile keyboard handling improvements into the dashboard, wiring `useMobileKeyboard` into both TodoModal and PlanningModeModal with keyboard-aware CSS overrides for mobile, plus regression tests for both components. FN-3337's chat `isGenerating` state recovery is also included. Fusion-Task-Id: FN-3341
This commit is contained in:
@@ -61,4 +61,11 @@
|
||||
border-radius: 0;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.modal.todo-modal[style*="--keyboard-overlap"] {
|
||||
height: var(--vv-height, 100dvh);
|
||||
max-height: var(--vv-height, 100dvh);
|
||||
transform: translateY(var(--vv-offset-top, 0px));
|
||||
will-change: transform;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import "./TodoModal.css";
|
||||
import { useEffect } from "react";
|
||||
import { ListChecks, X } from "lucide-react";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||
import { useViewportMode } from "./Header";
|
||||
import { TodoView } from "./TodoView";
|
||||
|
||||
interface TodoModalProps {
|
||||
@@ -14,6 +16,20 @@ interface TodoModalProps {
|
||||
|
||||
export function TodoModal({ onClose, projectId, addToast, onPlanningMode }: TodoModalProps) {
|
||||
const overlayDismissProps = useOverlayDismiss(onClose);
|
||||
const mode = useViewportMode();
|
||||
const isMobile = mode === "mobile";
|
||||
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
|
||||
enabled: isMobile,
|
||||
});
|
||||
|
||||
const modalKeyboardStyle: React.CSSProperties =
|
||||
keyboardOpen
|
||||
? ({
|
||||
"--keyboard-overlap": `${keyboardOverlap}px`,
|
||||
"--vv-offset-top": `${viewportOffsetTop}px`,
|
||||
...(viewportHeight !== null ? { "--vv-height": `${viewportHeight}px` } : {}),
|
||||
} as React.CSSProperties)
|
||||
: {};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -28,7 +44,7 @@ export function TodoModal({ onClose, projectId, addToast, onPlanningMode }: Todo
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open" {...overlayDismissProps} role="dialog" aria-modal="true">
|
||||
<div className="modal todo-modal">
|
||||
<div className="modal todo-modal" style={modalKeyboardStyle}>
|
||||
<div className="modal-header todo-modal-header">
|
||||
<div className="todo-modal-header-title">
|
||||
<ListChecks size={18} />
|
||||
|
||||
@@ -3,6 +3,8 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { TodoModal } from "../TodoModal";
|
||||
|
||||
const mockTodoView = vi.fn();
|
||||
const mockUseMobileKeyboard = vi.fn();
|
||||
const mockUseViewportMode = vi.fn();
|
||||
|
||||
vi.mock("../TodoView", () => ({
|
||||
TodoView: (props: unknown) => {
|
||||
@@ -11,12 +13,27 @@ vi.mock("../TodoView", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useMobileKeyboard", () => ({
|
||||
useMobileKeyboard: (...args: unknown[]) => mockUseMobileKeyboard(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useViewportMode", () => ({
|
||||
useViewportMode: (...args: unknown[]) => mockUseViewportMode(...args),
|
||||
}));
|
||||
|
||||
describe("TodoModal", () => {
|
||||
const onClose = vi.fn();
|
||||
const addToast = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseViewportMode.mockReturnValue("desktop");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders modal dialog semantics and header content", () => {
|
||||
@@ -55,4 +72,44 @@ describe("TodoModal", () => {
|
||||
expect.objectContaining({ projectId: "proj-1", addToast }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("mobile keyboard behavior", () => {
|
||||
it("applies CSS variables when keyboard is open on mobile", () => {
|
||||
mockUseViewportMode.mockReturnValue("mobile");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 250,
|
||||
viewportHeight: 450,
|
||||
viewportOffsetTop: 40,
|
||||
keyboardOpen: true,
|
||||
});
|
||||
|
||||
render(<TodoModal onClose={onClose} addToast={addToast} />);
|
||||
const modal = screen.getByRole("dialog").querySelector(".modal.todo-modal");
|
||||
expect(modal).toBeTruthy();
|
||||
|
||||
const style = (modal as HTMLElement).style;
|
||||
expect(style.getPropertyValue("--keyboard-overlap")).toBe("250px");
|
||||
expect(style.getPropertyValue("--vv-offset-top")).toBe("40px");
|
||||
expect(style.getPropertyValue("--vv-height")).toBe("450px");
|
||||
});
|
||||
|
||||
it("does not apply keyboard CSS variables when keyboard is closed", () => {
|
||||
mockUseViewportMode.mockReturnValue("mobile");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
});
|
||||
|
||||
render(<TodoModal onClose={onClose} addToast={addToast} />);
|
||||
const modal = screen.getByRole("dialog").querySelector(".modal.todo-modal");
|
||||
expect(modal).toBeTruthy();
|
||||
|
||||
const style = (modal as HTMLElement).style;
|
||||
expect(style.getPropertyValue("--keyboard-overlap")).toBe("");
|
||||
expect(style.getPropertyValue("--vv-offset-top")).toBe("");
|
||||
expect(style.getPropertyValue("--vv-height")).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user