fix(app): keep commits above the safe area (#4005)

The bottom-anchored commits section did not consume the device inset, so Android system navigation covered its disclosure row. Keep the inset with the section that owns that edge.
This commit is contained in:
Mohamed Boudra
2026-08-28 23:05:21 +02:00
committed by GitHub
parent 3c6e4e39ec
commit 693280d996
2 changed files with 90 additions and 1 deletions
@@ -0,0 +1,83 @@
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { JSDOM } from "jsdom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CommitsSection } from "./commits-section";
vi.mock("react-native-unistyles", () => ({
StyleSheet: {
create: (factory: unknown) =>
typeof factory === "function"
? factory({
borderRadius: { full: 9999, sm: 4 },
borderWidth: { 1: 1 },
colors: {
border: "#333",
foreground: "#fff",
foregroundMuted: "#aaa",
statusDanger: "#c33",
surface2: "#222",
},
fontSize: { base: 15, sm: 13 },
spacing: [0, 4, 8, 12],
})
: factory,
},
}));
vi.mock("react-native-safe-area-context", () => ({
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 34, left: 0 }),
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock("@/git/use-commits-query", () => ({
useCheckoutCommitsQuery: () => ({ status: "idle" }),
}));
vi.mock("@/git/themed-chevron", () => ({
ThemedChevron: () => null,
chevronColorMapping: () => ({}),
}));
vi.mock("./commit-row", () => ({
CommitRow: () => null,
}));
let root: Root | null = null;
beforeEach(() => {
const dom = new JSDOM("<!doctype html><html><body></body></html>");
vi.stubGlobal("React", React);
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("window", dom.window);
vi.stubGlobal("document", dom.window.document);
vi.stubGlobal("HTMLElement", dom.window.HTMLElement);
vi.stubGlobal("Node", dom.window.Node);
vi.stubGlobal("navigator", dom.window.navigator);
const container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root?.unmount());
root = null;
vi.unstubAllGlobals();
});
describe("CommitsSection", () => {
it("keeps its bottom edge above the device safe area", () => {
act(() => {
root?.render(
<CommitsSection serverId="server" cwd="/repo" onCommitPress={vi.fn()} collapsed />,
);
});
const header = document.querySelector('[data-testid="commits-section-header"]');
expect(header?.parentElement?.getAttribute("style")).toContain("padding-bottom: 34px");
});
});
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useCheckoutCommitsQuery, type CheckoutCommitsQueryResult } from "@/git/use-commits-query";
@@ -91,6 +92,7 @@ export function CommitsSection({
onCollapsedChange,
}: CommitsSectionProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const isPanelActive = useRetainedPanelActive();
const [now, setNow] = useState(() => new Date());
const displayNow = useMemo(() => (isPanelActive ? new Date() : now), [isPanelActive, now]);
@@ -119,6 +121,10 @@ export function CommitsSection({
() => [styles.headerChevron, !collapsed && styles.headerChevronExpanded],
[collapsed],
);
const containerStyle = useMemo(
() => [styles.container, { paddingBottom: insets.bottom }],
[insets.bottom],
);
if (query.status === "unsupported") {
return null;
@@ -129,7 +135,7 @@ export function CommitsSection({
: null;
return (
<View style={styles.container}>
<View style={containerStyle}>
<Pressable
accessibilityRole="button"
testID="commits-section-header"