FN-5707: fix Android executor footer overlap after keyboard dismiss
Adjust mobile keyboard bar flags so Android keeps the executor footer stacked above the nav bar while iOS retains collapse behavior. - extract mobile bar keyboard flag computation into a dedicated utility - keep nav keyboard pinning cross-platform while limiting footer keyboard-collapse to iOS - add focused unit tests for Android/iOS/modal/desktop keyboard flag scenarios - update ExecutorStatusBar keyboard-open test assertion and dashboard guide keyboard behavior notes Files changed: docs/dashboard-guide.md | 2 +- packages/dashboard/app/App.tsx | 23 ++++--- .../__tests__/ExecutorStatusBar.test.tsx | 6 +- .../utils/__tests__/mobileBarKeyboardFlags.test.ts | 73 ++++++++++++++++++++++ .../dashboard/app/utils/mobileBarKeyboardFlags.ts | 36 +++++++++++ 5 files changed, 129 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-5707 Fusion-Task-Lineage: ed01fcb0-d814-4595-9479-5baf97326ae7
This commit is contained in:
@@ -59,6 +59,7 @@ import { useFavorites } from "./hooks/useFavorites";
|
||||
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
|
||||
import { useMobileKeyboard } from "./hooks/useMobileKeyboard";
|
||||
import { isIOS, useMobileScrollLock } from "./hooks/useMobileScrollLock";
|
||||
import { computeMobileBarKeyboardFlags } from "./utils/mobileBarKeyboardFlags";
|
||||
import { useSetupReadiness } from "./hooks/useSetupReadiness";
|
||||
import { useUpdateCheck } from "./hooks/useUpdateCheck";
|
||||
import { useViewState, type TaskView } from "./hooks/useViewState";
|
||||
@@ -472,13 +473,19 @@ function AppInner() {
|
||||
// as the focus target moving and dismisses the keyboard immediately.
|
||||
// iOS doesn't shrink the layout viewport, so the iOS path keeps the
|
||||
// hide-nav-on-keyboard behavior intact.
|
||||
const mobileKeyboardOpen = isMobile && keyboardOpen && !modalManager.anyModalOpen && isIOS();
|
||||
// Nav-bar-only: pin the bar to the page bottom whenever the keyboard is up on
|
||||
// mobile, regardless of modal state or platform. The bar's `bottom` defaults
|
||||
// to `var(--icb-bottom-offset)`, which equals the keyboard height on iOS and
|
||||
// would otherwise float the bar above the keyboard. We want the keyboard to
|
||||
// simply cover the bar instead.
|
||||
const mobileNavKeyboardOpen = isMobile && keyboardOpen;
|
||||
//
|
||||
// FN-5707: keep nav pinning cross-platform, but only apply the footer
|
||||
// keyboard-collapse class on iOS. On Android, collapsing the footer to
|
||||
// `bottom: 0` overlaps it with the nav bar because the layout viewport
|
||||
// already shrinks and the stacked footer position is already correct.
|
||||
const { footerHidden, navKeyboardOpen, footerKeyboardOpen } = computeMobileBarKeyboardFlags({
|
||||
isMobile,
|
||||
keyboardOpen,
|
||||
anyModalOpen: modalManager.anyModalOpen,
|
||||
isIOS: isIOS(),
|
||||
});
|
||||
const mobileKeyboardOpen = footerHidden;
|
||||
const mobileNavKeyboardOpen = navKeyboardOpen;
|
||||
// App-level scroll lock for inline editing (TaskCard inline edit, etc.):
|
||||
// when the keyboard is up outside of any modal, pin the body so iOS can't
|
||||
// shift the document or visualViewport, and so the dashboard snaps back
|
||||
@@ -1909,7 +1916,7 @@ function AppInner() {
|
||||
lastFetchTimeMs={lastFetchTimeMs}
|
||||
currentProjectPath={currentProject.path}
|
||||
onOpenProjectDirectory={handleOpenProjectDirectory}
|
||||
keyboardOpen={mobileNavKeyboardOpen}
|
||||
keyboardOpen={footerKeyboardOpen}
|
||||
hideWhenKeyboardOpen={mobileKeyboardOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -506,9 +506,11 @@ describe("ExecutorStatusBar", () => {
|
||||
expect(screen.getByRole("status")).toHaveClass("executor-status-bar--keyboard-open");
|
||||
});
|
||||
|
||||
it("does not apply keyboard-open class when keyboardOpen is false", () => {
|
||||
it("does not apply keyboard-open class and remains rendered when keyboardOpen is false", () => {
|
||||
render(<ExecutorStatusBar tasks={emptyTasks} keyboardOpen={false} />);
|
||||
expect(screen.getByRole("status")).not.toHaveClass("executor-status-bar--keyboard-open");
|
||||
const status = screen.getByRole("status");
|
||||
expect(status).toBeInTheDocument();
|
||||
expect(status).not.toHaveClass("executor-status-bar--keyboard-open");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeMobileBarKeyboardFlags } from "../mobileBarKeyboardFlags";
|
||||
|
||||
describe("computeMobileBarKeyboardFlags", () => {
|
||||
it("keeps footer rendered and uncollapsed on Android when keyboard is open", () => {
|
||||
const flags = computeMobileBarKeyboardFlags({
|
||||
isMobile: true,
|
||||
keyboardOpen: true,
|
||||
anyModalOpen: false,
|
||||
isIOS: false,
|
||||
});
|
||||
|
||||
expect(flags.footerKeyboardOpen).toBe(false);
|
||||
expect(flags.footerHidden).toBe(false);
|
||||
expect(flags.navKeyboardOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("hides and collapses footer on iOS when keyboard is open and no modal is open", () => {
|
||||
const flags = computeMobileBarKeyboardFlags({
|
||||
isMobile: true,
|
||||
keyboardOpen: true,
|
||||
anyModalOpen: false,
|
||||
isIOS: true,
|
||||
});
|
||||
|
||||
expect(flags.footerHidden).toBe(true);
|
||||
expect(flags.navKeyboardOpen).toBe(true);
|
||||
expect(flags.footerKeyboardOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps footer visible when iOS keyboard is open over a modal", () => {
|
||||
const flags = computeMobileBarKeyboardFlags({
|
||||
isMobile: true,
|
||||
keyboardOpen: true,
|
||||
anyModalOpen: true,
|
||||
isIOS: true,
|
||||
});
|
||||
|
||||
expect(flags.footerHidden).toBe(false);
|
||||
expect(flags.footerKeyboardOpen).toBe(true);
|
||||
expect(flags.navKeyboardOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("returns all false when keyboard is closed", () => {
|
||||
const flags = computeMobileBarKeyboardFlags({
|
||||
isMobile: true,
|
||||
keyboardOpen: false,
|
||||
anyModalOpen: false,
|
||||
isIOS: true,
|
||||
});
|
||||
|
||||
expect(flags).toEqual({
|
||||
footerHidden: false,
|
||||
navKeyboardOpen: false,
|
||||
footerKeyboardOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns all false when not mobile", () => {
|
||||
const flags = computeMobileBarKeyboardFlags({
|
||||
isMobile: false,
|
||||
keyboardOpen: true,
|
||||
anyModalOpen: false,
|
||||
isIOS: true,
|
||||
});
|
||||
|
||||
expect(flags).toEqual({
|
||||
footerHidden: false,
|
||||
navKeyboardOpen: false,
|
||||
footerKeyboardOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
36
packages/dashboard/app/utils/mobileBarKeyboardFlags.ts
Normal file
36
packages/dashboard/app/utils/mobileBarKeyboardFlags.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface MobileBarKeyboardFlagsInput {
|
||||
isMobile: boolean;
|
||||
keyboardOpen: boolean;
|
||||
anyModalOpen: boolean;
|
||||
isIOS: boolean;
|
||||
}
|
||||
|
||||
export interface MobileBarKeyboardFlags {
|
||||
footerHidden: boolean;
|
||||
navKeyboardOpen: boolean;
|
||||
footerKeyboardOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* FN-5707: Android uses `interactive-widget=resizes-content`, so the layout
|
||||
* viewport shrinks with the keyboard and the footer's normal stacked bottom
|
||||
* position remains correct above the mobile nav. Only iOS should apply the
|
||||
* footer keyboard-collapse class (`bottom: 0`) used to let the keyboard cover
|
||||
* bars when visualViewport shifts independently.
|
||||
*/
|
||||
export function computeMobileBarKeyboardFlags({
|
||||
isMobile,
|
||||
keyboardOpen,
|
||||
anyModalOpen,
|
||||
isIOS,
|
||||
}: MobileBarKeyboardFlagsInput): MobileBarKeyboardFlags {
|
||||
const footerHidden = isMobile && keyboardOpen && !anyModalOpen && isIOS;
|
||||
const navKeyboardOpen = isMobile && keyboardOpen;
|
||||
const footerKeyboardOpen = navKeyboardOpen && isIOS;
|
||||
|
||||
return {
|
||||
footerHidden,
|
||||
navKeyboardOpen,
|
||||
footerKeyboardOpen,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user