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 terminalRef = useRef<HTMLDivElement>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const xtermRef = useRef<XTerm | null>(null);
|
||||
const fitAddonRef = useRef<ITerminalAddon | null>(null);
|
||||
const hasInitialCommandRun = useRef<string | false>(false);
|
||||
@@ -83,7 +84,15 @@ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModal
|
||||
const vv = window.visualViewport;
|
||||
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
|
||||
vv.addEventListener("resize", update);
|
||||
@@ -475,6 +484,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModal
|
||||
data-testid="terminal-modal-overlay"
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="modal terminal-modal"
|
||||
data-testid="terminal-modal"
|
||||
style={
|
||||
|
||||
@@ -1694,6 +1694,51 @@ describe("TerminalModal — virtual keyboard overlap handling", () => {
|
||||
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 ---
|
||||
|
||||
Reference in New Issue
Block a user