Fix compact sheet bottom spacing

This commit is contained in:
Mohamed Boudra
2026-06-04 09:28:05 +07:00
parent f3b7dac768
commit 3e3e944cf4
5 changed files with 112 additions and 5 deletions

View File

@@ -94,6 +94,8 @@ Five primitives. The pick is determined by option count, the need to search, and
`<AdaptiveModalSheet>` is for a focused task. Multi-field forms (`packages/app/src/components/add-host-modal.tsx`, `packages/app/src/components/pair-link-modal.tsx`, `packages/app/src/components/project-picker-modal.tsx`), confirmations with detail, anything that earns a backdrop. Bottom sheet on compact, centered card on desktop. Raw `Modal` is wrong for any of these.
`<AdaptiveModalSheet>` owns compact bottom safe-area padding inside the sheet so the sheet background still reaches the screen bottom. If a sheet's first snap point is shorter than its header, content, and safe-area clearance, raise that snap point rather than moving the sheet container.
`confirmDialog` is for destructive yes/no and imperative confirmation. Promise-based: `await confirmDialog({ destructive: true, ... })`. Anything where a wrong click loses work.
Three themes is `DropdownMenu`. Thirty hosts is `Combobox`. A label and a value is `AdaptiveModalSheet`. "Are you sure?" is `confirmDialog`.

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { getCompactSheetSafeAreaPadding } from "@/components/adaptive-modal-sheet-layout";
describe("getCompactSheetSafeAreaPadding", () => {
it("adds the bottom inset to compact sheet footers", () => {
expect(
getCompactSheetSafeAreaPadding({
isCompact: true,
hasFooter: true,
baseContentPadding: 24,
baseFooterPadding: 12,
safeAreaBottom: 34,
}),
).toEqual({ footerPaddingBottom: 46 });
});
it("adds the bottom inset to compact sheet content when there is no footer", () => {
expect(
getCompactSheetSafeAreaPadding({
isCompact: true,
hasFooter: false,
baseContentPadding: 24,
baseFooterPadding: 12,
safeAreaBottom: 34,
}),
).toEqual({ contentPaddingBottom: 58 });
});
it("does not inset desktop sheets", () => {
expect(
getCompactSheetSafeAreaPadding({
isCompact: false,
hasFooter: false,
baseContentPadding: 24,
baseFooterPadding: 12,
safeAreaBottom: 34,
}),
).toEqual({});
});
});

View File

@@ -0,0 +1,30 @@
export interface CompactSheetSafeAreaPaddingInput {
isCompact: boolean;
hasFooter: boolean;
baseContentPadding: number;
baseFooterPadding: number;
safeAreaBottom: number;
}
export interface CompactSheetSafeAreaPadding {
contentPaddingBottom?: number;
footerPaddingBottom?: number;
}
export function getCompactSheetSafeAreaPadding({
isCompact,
hasFooter,
baseContentPadding,
baseFooterPadding,
safeAreaBottom,
}: CompactSheetSafeAreaPaddingInput): CompactSheetSafeAreaPadding {
if (!isCompact || safeAreaBottom <= 0) {
return {};
}
if (hasFooter) {
return { footerPaddingBottom: baseFooterPadding + safeAreaBottom };
}
return { contentPaddingBottom: baseContentPadding + safeAreaBottom };
}

View File

@@ -19,6 +19,7 @@ import {
IsolatedBottomSheetModal,
useIsolatedBottomSheetVisibility,
} from "@/components/ui/isolated-bottom-sheet-modal";
import { getCompactSheetSafeAreaPadding } from "@/components/adaptive-modal-sheet-layout";
import { isNative, isWeb } from "@/constants/platform";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -461,9 +462,43 @@ export function AdaptiveModalSheet({
const isMobile = useIsCompactFormFactor();
const insets = useSafeAreaInsets();
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
const compactSafeAreaPadding = useMemo(
() =>
getCompactSheetSafeAreaPadding({
isCompact: isMobile,
hasFooter: Boolean(footer),
baseContentPadding: theme.spacing[SHEET_HORIZONTAL_PADDING_SCALE],
baseFooterPadding: theme.spacing[3],
safeAreaBottom: insets.bottom,
}),
[footer, insets.bottom, isMobile, theme.spacing],
);
const bottomSheetContentStyle = useMemo(
() => [
styles.bottomSheetContent,
compactSafeAreaPadding.contentPaddingBottom != null
? { paddingBottom: compactSafeAreaPadding.contentPaddingBottom }
: null,
],
[compactSafeAreaPadding.contentPaddingBottom],
);
const bottomSheetStaticContentStyle = useMemo(
() => [
styles.bottomSheetStaticContent,
compactSafeAreaPadding.contentPaddingBottom != null
? { paddingBottom: compactSafeAreaPadding.contentPaddingBottom }
: null,
],
[compactSafeAreaPadding.contentPaddingBottom],
);
const footerStyle = useMemo(
() => [styles.footer, isMobile ? { paddingBottom: theme.spacing[3] + insets.bottom } : null],
[insets.bottom, isMobile, theme.spacing],
() => [
styles.footer,
compactSafeAreaPadding.footerPaddingBottom != null
? { paddingBottom: compactSafeAreaPadding.footerPaddingBottom }
: null,
],
[compactSafeAreaPadding.footerPaddingBottom],
);
const handleIndicatorStyle = useMemo(
() => ({ backgroundColor: theme.colors.surface2 }),
@@ -512,14 +547,14 @@ export function AdaptiveModalSheet({
<SheetHeaderView header={header} onClose={onClose} testID={testID} />
{scrollable ? (
<BottomSheetScrollView
contentContainerStyle={styles.bottomSheetContent}
contentContainerStyle={bottomSheetContentStyle}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</BottomSheetScrollView>
) : (
<View style={styles.bottomSheetStaticContent}>{children}</View>
<View style={bottomSheetStaticContentStyle}>{children}</View>
)}
{footer ? <View style={footerStyle}>{footer}</View> : null}
</IsolatedBottomSheetModal>

View File

@@ -143,7 +143,7 @@ const DEFAULT_MAX_INPUT_HEIGHT = 160;
const MAX_INPUT_VIEWPORT_RATIO = 0.5;
const MIN_INPUT_HEIGHT = isWeb ? MIN_INPUT_HEIGHT_DESKTOP : MIN_INPUT_HEIGHT_MOBILE;
const ATTACHMENT_SHEET_HEADER: SheetHeader = { title: "Add attachment" };
const ATTACHMENT_SHEET_SNAP_POINTS = ["28%", "45%"];
const ATTACHMENT_SHEET_SNAP_POINTS = ["34%", "45%"];
type WebTextInputKeyPressEvent = NativeSyntheticEvent<
TextInputKeyPressEventData & {