feat(FN-928): scroll terminal into view when mobile keyboard opens
- Detect mobile viewport resize in TerminalModal to infer keyboard open/close - Use visualViewport API and smooth scroll to keep terminal visible on mobile - Add tests for scroll behavior with mobile keyboard open/close scenarios
This commit is contained in:
@@ -62,6 +62,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModal
|
|||||||
const [keyboardOverlap, setKeyboardOverlap] = useState(0);
|
const [keyboardOverlap, setKeyboardOverlap] = useState(0);
|
||||||
|
|
||||||
const terminalRef = useRef<HTMLDivElement>(null);
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const xtermRef = useRef<XTerm | null>(null);
|
const xtermRef = useRef<XTerm | null>(null);
|
||||||
const fitAddonRef = useRef<ITerminalAddon | null>(null);
|
const fitAddonRef = useRef<ITerminalAddon | null>(null);
|
||||||
const hasInitialCommandRun = useRef<string | false>(false);
|
const hasInitialCommandRun = useRef<string | false>(false);
|
||||||
@@ -83,7 +84,15 @@ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModal
|
|||||||
const vv = window.visualViewport;
|
const vv = window.visualViewport;
|
||||||
if (!vv) return;
|
if (!vv) return;
|
||||||
|
|
||||||
const update = () => setKeyboardOverlap(getKeyboardOverlap());
|
const update = () => {
|
||||||
|
const overlap = getKeyboardOverlap();
|
||||||
|
setKeyboardOverlap(overlap);
|
||||||
|
// Scroll the modal so the status bar (bottom edge) stays visible
|
||||||
|
// when the virtual keyboard pushes the viewport up.
|
||||||
|
if (overlap > 0 && modalRef.current?.scrollIntoView) {
|
||||||
|
modalRef.current.scrollIntoView({ block: "end", behavior: "smooth" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
update(); // initial measurement
|
update(); // initial measurement
|
||||||
vv.addEventListener("resize", update);
|
vv.addEventListener("resize", update);
|
||||||
@@ -475,6 +484,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModal
|
|||||||
data-testid="terminal-modal-overlay"
|
data-testid="terminal-modal-overlay"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
ref={modalRef}
|
||||||
className="modal terminal-modal"
|
className="modal terminal-modal"
|
||||||
data-testid="terminal-modal"
|
data-testid="terminal-modal"
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -1694,6 +1694,51 @@ describe("TerminalModal — virtual keyboard overlap handling", () => {
|
|||||||
expect(modal.style.getPropertyValue("--keyboard-overlap")).toBe("");
|
expect(modal.style.getPropertyValue("--keyboard-overlap")).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scrolls modal into view when keyboard opens on mobile", async () => {
|
||||||
|
const scrollIntoViewSpy = vi.fn();
|
||||||
|
const { listeners } = simulateMobileDevice(250);
|
||||||
|
|
||||||
|
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const modal = screen.getByTestId("terminal-modal");
|
||||||
|
expect(modal.style.getPropertyValue("--keyboard-overlap")).toBe("250px");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach the spy to the rendered modal element
|
||||||
|
const modal = screen.getByTestId("terminal-modal");
|
||||||
|
modal.scrollIntoView = scrollIntoViewSpy;
|
||||||
|
|
||||||
|
// Trigger a resize event to re-run the update callback
|
||||||
|
act(() => {
|
||||||
|
for (const cb of listeners.resize) cb();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scrollIntoViewSpy).toHaveBeenCalledWith({ block: "end", behavior: "smooth" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not scroll modal when keyboard overlap is zero", async () => {
|
||||||
|
const scrollIntoViewSpy = vi.fn();
|
||||||
|
const { listeners } = simulateMobileDevice(0); // no overlap
|
||||||
|
|
||||||
|
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const modal = screen.getByTestId("terminal-modal");
|
||||||
|
expect(modal.style.getPropertyValue("--keyboard-overlap")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = screen.getByTestId("terminal-modal");
|
||||||
|
modal.scrollIntoView = scrollIntoViewSpy;
|
||||||
|
|
||||||
|
// Trigger a resize event
|
||||||
|
act(() => {
|
||||||
|
for (const cb of listeners.resize) cb();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scrollIntoViewSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Close/reopen regression tests ---
|
// --- Close/reopen regression tests ---
|
||||||
|
|||||||
Reference in New Issue
Block a user