diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx index 0543695..d686acd 100644 --- a/src/components/Screen.tsx +++ b/src/components/Screen.tsx @@ -1,12 +1,21 @@ import { BlurTargetView } from 'expo-blur'; import { type PropsWithChildren, type ReactNode, useCallback, useRef, useState } from 'react'; -import { AccessibilityInfo, ScrollView, View, type NativeScrollEvent, type ScrollViewProps, type NativeSyntheticEvent } from 'react-native'; +import { AccessibilityInfo, Platform, ScrollView, View, type NativeScrollEvent, type ScrollViewProps, type NativeSyntheticEvent } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { AppText } from './AppText'; import { ScrollToTopButton } from './ScrollToTopButton'; import { StickyBackdrop } from './StickyBackdrop'; import { useLayout, useTheme } from '@/theme/ThemeContext'; +import { spacing as spacingTokens } from '@/theme/tokens'; + +/** + * Native screens start below the status bar's safe-area inset. Browsers have no + * status bar, so without this the title would touch the top of the window. + */ +export function screenTopPadding(platform: string, nativeHeader: boolean): number { + return platform === 'web' && !nativeHeader ? spacingTokens.xxl : 0; +} type ScreenProps = PropsWithChildren<{ title: string; @@ -91,7 +100,7 @@ function StickyScreenContent({ children, description, headerAccessory, isExpande ref={scrollRef} testID="screen-scroll" keyboardShouldPersistTaps={keyboardShouldPersistTaps} - contentContainerStyle={{ alignItems: 'center', flexGrow: 1, gap: spacing.md, paddingBottom: paddingBottom + (scrollToTop ? 48 + spacing.md : 0), paddingHorizontal }} + contentContainerStyle={{ alignItems: 'center', flexGrow: 1, gap: spacing.md, paddingBottom: paddingBottom + (scrollToTop ? 48 + spacing.md : 0), paddingHorizontal, paddingTop: screenTopPadding(Platform.OS, nativeHeader) }} onScroll={handleScroll} onScrollBeginDrag={scrollToTop ? cancelScrollToTop : undefined} onTouchStart={scrollToTop ? cancelScrollToTop : undefined} @@ -138,7 +147,7 @@ export function Screen({ title, description, headerAccessory, bottomAccessory, n const paddingHorizontal = isExpanded ? spacing.xxl : spacing.xl; return ( - {stickyAccessory === undefined ? + {stickyAccessory === undefined ? {!nativeHeader ? : null} {children} diff --git a/src/components/screenTopPadding.test.ts b/src/components/screenTopPadding.test.ts new file mode 100644 index 0000000..42c1de1 --- /dev/null +++ b/src/components/screenTopPadding.test.ts @@ -0,0 +1,14 @@ +import { screenTopPadding } from './Screen'; +import { spacing } from '@/theme/tokens'; + +describe('screen top padding', () => { + it('adds space above web page titles, which have no status bar inset', () => { + expect(screenTopPadding('web', false)).toBe(spacing.xxl); + }); + + it('leaves native screens and stack-header screens unchanged', () => { + expect(screenTopPadding('ios', false)).toBe(0); + expect(screenTopPadding('android', false)).toBe(0); + expect(screenTopPadding('web', true)).toBe(0); + }); +});