react-native-terra-ui 0.11.6 → 0.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/module/components/header/parts/HeaderAccessory.js +25 -22
  3. package/lib/module/components/header/parts/HeaderAccessory.js.map +1 -1
  4. package/lib/module/components/header/parts/HeaderSurface.js +13 -20
  5. package/lib/module/components/header/parts/HeaderSurface.js.map +1 -1
  6. package/lib/module/components/header/variants/LargeTitleHeader/index.js +130 -42
  7. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  8. package/lib/module/components/header/variants/TitleHeader/index.js +5 -1
  9. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  10. package/lib/module/components/screen/ScreenContext.js +7 -1
  11. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  12. package/lib/module/components/screen/ScreenList.js +19 -2
  13. package/lib/module/components/screen/ScreenList.js.map +1 -1
  14. package/lib/module/components/screen/ScreenScrollView.js +5 -1
  15. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  16. package/lib/typescript/src/components/header/index.d.ts +1 -1
  17. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  18. package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts +11 -11
  19. package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts.map +1 -1
  20. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +8 -10
  21. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -1
  22. package/lib/typescript/src/components/header/types.d.ts +11 -7
  23. package/lib/typescript/src/components/header/types.d.ts.map +1 -1
  24. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +8 -1
  25. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  26. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +3 -4
  27. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  28. package/lib/typescript/src/components/screen/ScreenContext.d.ts +7 -0
  29. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  30. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  31. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/components/header/index.ts +1 -1
  34. package/src/components/header/parts/HeaderAccessory.tsx +21 -23
  35. package/src/components/header/parts/HeaderSurface.tsx +17 -20
  36. package/src/components/header/types.ts +12 -7
  37. package/src/components/header/variants/LargeTitleHeader/index.tsx +110 -18
  38. package/src/components/header/variants/TitleHeader/index.tsx +15 -6
  39. package/src/components/screen/ScreenContext.tsx +16 -0
  40. package/src/components/screen/ScreenList.tsx +22 -2
  41. package/src/components/screen/ScreenScrollView.tsx +16 -3
@@ -1,5 +1,5 @@
1
- import { useEffect } from "react";
2
- import { Text, View } from "react-native";
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { type LayoutChangeEvent, Text, View } from "react-native";
3
3
 
4
4
  import Animated, { interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
5
  import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -11,8 +11,10 @@ import { useHeaderBackground } from "../../../screen/parts/ScreenHeaderAppearanc
11
11
  import { SCREEN_HEADER_PORTAL_HOST, useScreen } from "../../../screen/ScreenContext";
12
12
  import { Toolbar } from "../../../toolbar";
13
13
  import { HeaderDismissButton } from "../../header-buttons";
14
- import { HeaderAccessory } from "../../parts/HeaderAccessory";
14
+ import { HeaderAccessory, HeaderAccessorySpacer } from "../../parts/HeaderAccessory";
15
+ import { HEADER_FADE_EXTENT } from "../../parts/HeaderGradientOverlay";
15
16
  import { HeaderSurface } from "../../parts/HeaderSurface";
17
+ import type { HeaderAccessoryBehavior } from "../../types";
16
18
  import { isCustomTitle, isTextTitle } from "../../utils";
17
19
  import type { TitleHeaderProps } from "../TitleHeader";
18
20
 
@@ -30,6 +32,12 @@ export interface LargeTitleHeaderProps extends TitleHeaderProps {
30
32
  subtitle?: string;
31
33
  /** Hide the large title (keeps the compact bar). Defaults to `false`. */
32
34
  isLargeTitleHidden?: boolean;
35
+ /**
36
+ * How `accessory` moves while scrolling. `'sticky'` starts below the large title
37
+ * and pins below the compact bar when scrolled up. A hidden large title always pins.
38
+ * @defaultValue 'pinned'
39
+ */
40
+ accessoryBehavior?: HeaderAccessoryBehavior;
33
41
  /**
34
42
  * Placement of the compact bar title. The large title stack always stays
35
43
  * left-aligned. Defaults to `'center'`.
@@ -41,7 +49,11 @@ function LargeTitlePortalContent({
41
49
  title,
42
50
  overline,
43
51
  subtitle,
44
- }: Pick<LargeTitleHeaderProps, "title" | "overline" | "subtitle">) {
52
+ onHeightChange,
53
+ }: Pick<LargeTitleHeaderProps, "title" | "overline" | "subtitle"> & {
54
+ /** Reports the large-title block's layout height in dp. */
55
+ onHeightChange?: (height: number) => void;
56
+ }) {
45
57
  const { theme } = useUnistyles();
46
58
  const { scrollY } = useScreen();
47
59
 
@@ -63,8 +75,11 @@ function LargeTitlePortalContent({
63
75
  };
64
76
  });
65
77
 
78
+ // Layout height excludes the collapse transform.
79
+ const handleLayout = (event: LayoutChangeEvent) => onHeightChange?.(event.nativeEvent.layout.height);
80
+
66
81
  return (
67
- <Animated.View style={[styles.portalContent, largeTitleAnimatedStyle]}>
82
+ <Animated.View style={[styles.portalContent, largeTitleAnimatedStyle]} onLayout={handleLayout}>
68
83
  {!!overline && (
69
84
  <Text
70
85
  style={styles.overline}
@@ -126,6 +141,7 @@ export function LargeTitleHeader({
126
141
  RightToolbar,
127
142
  accessory,
128
143
  accessoryPadding = "x",
144
+ accessoryBehavior = "sticky",
129
145
  dismissAction = "none",
130
146
  navigation,
131
147
  onDismiss,
@@ -136,7 +152,7 @@ export function LargeTitleHeader({
136
152
  }: LargeTitleHeaderProps) {
137
153
  const { theme } = useUnistyles();
138
154
 
139
- const { scrollY, background: screenBackground } = useScreen();
155
+ const { scrollY, background: screenBackground, setHeaderAccessoryHeight, contentTopPadding } = useScreen();
140
156
  const colorToken = background ?? bg;
141
157
  const bgColor =
142
158
  (colorToken ? resolveThemeColor(colorToken, theme) : screenBackground) ?? theme.color["surface.canvas"];
@@ -144,18 +160,62 @@ export function LargeTitleHeader({
144
160
  const trailingContent = trailingActions !== undefined ? trailingActions : RightToolbar;
145
161
 
146
162
  const gradientColor = bgColor;
163
+ const isSticky = accessoryBehavior === "sticky" && !isLargeTitleHidden;
164
+ const hasStickyAccessory = isSticky && accessory != null;
165
+ // Sticky accessories reserve space below the large title; pinned ones add top padding.
166
+ const [stickyAccessoryHeight, setStickyAccessoryHeight] = useState(0);
167
+ const [largeTitleHeight, setLargeTitleHeight] = useState(0);
147
168
  const isLargeTitleHiddenValue = useSharedValue(isLargeTitleHidden);
169
+
170
+ const handleAccessoryHeightChange = useCallback(
171
+ (height: number) => {
172
+ if (isSticky) setStickyAccessoryHeight(Math.max(0, Math.round(height)));
173
+ else setHeaderAccessoryHeight(height);
174
+ },
175
+ [isSticky, setHeaderAccessoryHeight]
176
+ );
177
+
178
+ const handleLargeTitleHeightChange = useCallback((height: number) => {
179
+ setLargeTitleHeight(Math.max(0, Math.round(height)));
180
+ }, []);
181
+
182
+ // Rest below the title, including the scroll container's top padding.
183
+ const stickyAccessoryOffset = Math.max(
184
+ 0,
185
+ contentTopPadding + largeTitleHeight - theme.layout.header.height
186
+ );
187
+
188
+ // Zero translation places the accessory directly below the compact bar.
189
+ const stickyAccessoryStyle = useAnimatedStyle(() => {
190
+ if (isLargeTitleHiddenValue.value) {
191
+ return { transform: [{ translateY: 0 }] };
192
+ }
193
+ return {
194
+ transform: [{ translateY: Math.max(0, stickyAccessoryOffset - scrollY.value) }],
195
+ };
196
+ });
148
197
  const headerCollapseHeight = theme.layout.header.height;
149
198
  styles.useVariants({ titleAlignment });
150
199
 
200
+ // Follow the accessory's bottom edge while keeping the background anchored to the bar.
201
+ const stickySurfaceStyle = useAnimatedStyle(() => ({
202
+ height: headerCollapseHeight + stickyAccessoryHeight + Math.max(0, stickyAccessoryOffset - scrollY.value),
203
+ }));
204
+
205
+ // Blend the accessory's resting surface into the shared header background.
206
+ const accessorySurfaceStyle = useAnimatedStyle(() => ({
207
+ opacity:
208
+ stickyAccessoryOffset > 0 ? interpolate(scrollY.value, [0, stickyAccessoryOffset], [1, 0], "clamp") : 0,
209
+ }));
210
+
151
211
  // Animate the gradient overlay's opacity: 0 (expanded) → 1 (collapsed).
152
212
  const gradientOpacityStyle = useAnimatedStyle(() => {
153
213
  if (isLargeTitleHiddenValue.value) {
154
214
  return { opacity: 1 };
155
215
  }
156
- const height = headerCollapseHeight;
216
+ const height = hasStickyAccessory ? stickyAccessoryOffset : headerCollapseHeight;
157
217
  if (height <= 0) {
158
- return { opacity: 0 };
218
+ return { opacity: hasStickyAccessory ? 1 : 0 };
159
219
  }
160
220
  return {
161
221
  opacity: interpolate(scrollY.value, [0, height], [0, 1], "clamp"),
@@ -212,16 +272,28 @@ export function LargeTitleHeader({
212
272
  );
213
273
 
214
274
  return (
215
- <View style={styles.container}>
275
+ // Let touches in the expanded header's empty space reach the scroll content.
276
+ <View style={styles.container(isSticky ? stickyAccessoryOffset : 0)} pointerEvents="box-none">
216
277
  {!isLargeTitleHidden && (
217
278
  <Portal hostName={SCREEN_HEADER_PORTAL_HOST}>
218
- <LargeTitlePortalContent title={title} overline={overline} subtitle={subtitle} />
279
+ <LargeTitlePortalContent
280
+ title={title}
281
+ overline={overline}
282
+ subtitle={subtitle}
283
+ onHeightChange={isSticky ? handleLargeTitleHeightChange : undefined}
284
+ />
285
+ {/* Reserve space for the accessory rendered in the overlay. */}
286
+ {isSticky && accessory != null && <HeaderAccessorySpacer height={stickyAccessoryHeight} />}
219
287
  </Portal>
220
288
  )}
221
- {/* The fade covers the bar and any accessory as one surface, so content
222
- scrolling under the header meets a single boundary rather than two. */}
289
+ {hasStickyAccessory && (
290
+ <Animated.View style={[styles.stickySurface, stickySurfaceStyle]} pointerEvents="none">
291
+ <HeaderSurface color={gradientColor} animatedStyle={gradientOpacityStyle} />
292
+ </Animated.View>
293
+ )}
294
+ {/* Share a background between the bar and a pinned accessory. */}
223
295
  <View style={styles.barGroup}>
224
- <HeaderSurface color={gradientColor} animatedStyle={gradientOpacityStyle} />
296
+ {!hasStickyAccessory && <HeaderSurface color={gradientColor} animatedStyle={gradientOpacityStyle} />}
225
297
  <View style={styles.bar}>
226
298
  {leading != null && <Toolbar>{leading}</Toolbar>}
227
299
  {/* Text can never be the touch target, but custom content may be, so its
@@ -243,21 +315,41 @@ export function LargeTitleHeader({
243
315
  </Animated.View>
244
316
  {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
245
317
  </View>
246
- {/* Pinned, so it stays legible while the large title collapses past it. */}
247
- {accessory != null && <HeaderAccessory padding={accessoryPadding}>{accessory}</HeaderAccessory>}
318
+ {accessory != null && !isSticky && (
319
+ <HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
320
+ {accessory}
321
+ </HeaderAccessory>
322
+ )}
248
323
  </View>
324
+ {accessory != null && isSticky && (
325
+ <Animated.View style={[styles.stickyAccessory, stickyAccessoryStyle]}>
326
+ <HeaderSurface
327
+ color={screenBackground ?? theme.color["surface.canvas"]}
328
+ animatedStyle={accessorySurfaceStyle}
329
+ fadeHeight={HEADER_FADE_EXTENT}
330
+ />
331
+ <HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
332
+ {accessory}
333
+ </HeaderAccessory>
334
+ </Animated.View>
335
+ )}
249
336
  </View>
250
337
  );
251
338
  }
252
339
 
253
340
  const styles = StyleSheet.create((theme) => ({
254
- container: {
341
+ stickySurface: { position: "absolute", top: 0, left: 0, right: 0 },
342
+ // Include the translated accessory in the parent's touchable bounds.
343
+ container: (accessoryOffset: number) => ({
255
344
  position: "absolute",
256
345
  top: 0,
257
346
  left: 0,
258
347
  right: 0,
259
348
  zIndex: 1000,
260
- },
349
+ paddingBottom: accessoryOffset,
350
+ }),
351
+ // Normal flow places the accessory directly below the bar.
352
+ stickyAccessory: { position: "relative" },
261
353
  // Anchors the absolutely positioned surface over the bar and the accessory both.
262
354
  barGroup: { position: "relative" },
263
355
  bar: {
@@ -283,7 +375,7 @@ const styles = StyleSheet.create((theme) => ({
283
375
  portalContent: {
284
376
  gap: 2,
285
377
  minHeight: theme.layout.header.height,
286
- paddingBottom: theme.spacing["3"],
378
+ paddingBottom: theme.spacing["1"],
287
379
  paddingHorizontal: theme.layout.screen.padding.x,
288
380
  },
289
381
  overline: {
@@ -1,4 +1,4 @@
1
- import { type ReactNode, useContext } from "react";
1
+ import { type ReactNode, useCallback, useContext } from "react";
2
2
  import { Text, View } from "react-native";
3
3
 
4
4
  import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -37,10 +37,9 @@ export interface TitleHeaderProps extends HeaderDismissProps {
37
37
  /** @deprecated Use trailingActions. */
38
38
  RightToolbar?: ReactNode;
39
39
  /**
40
- * Content pinned directly below the bar — a search field, a scrollable tab
41
- * strip, a filter row. It stays put while the screen scrolls, and the
42
- * surrounding `Screen`'s scroll containers reserve its measured height so
43
- * content starts below it rather than under it.
40
+ * Content below the header, such as a search field or tab strip.
41
+ * `Header.Title` always pins it below the bar. `Header.LargeTitle` also supports
42
+ * scrolling it with the large title via `accessoryBehavior`.
44
43
  */
45
44
  accessory?: ReactNode;
46
45
  /**
@@ -98,6 +97,12 @@ export function TitleHeader({
98
97
  useHeaderBackground(bgColor);
99
98
  const leadingContent = leadingActions !== undefined ? leadingActions : LeftToolbar;
100
99
  const trailingContent = trailingActions !== undefined ? trailingActions : RightToolbar;
100
+ const setAccessoryInset = screen?.setHeaderAccessoryHeight;
101
+ // Compact headers always reserve the accessory height as top padding.
102
+ const handleAccessoryHeightChange = useCallback(
103
+ (height: number) => setAccessoryInset?.(height),
104
+ [setAccessoryInset]
105
+ );
101
106
  const isTransparent = bgColor === "transparent";
102
107
  const gradientColor = bgColor;
103
108
  styles.useVariants({ titleAlignment });
@@ -139,7 +144,11 @@ export function TitleHeader({
139
144
  </View>
140
145
  {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
141
146
  </View>
142
- {accessory != null && <HeaderAccessory padding={accessoryPadding}>{accessory}</HeaderAccessory>}
147
+ {accessory != null && (
148
+ <HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
149
+ {accessory}
150
+ </HeaderAccessory>
151
+ )}
143
152
  </View>
144
153
  </View>
145
154
  );
@@ -78,6 +78,13 @@ export interface ScreenContextValue {
78
78
  * accessory; zero releases the reserved space.
79
79
  */
80
80
  setHeaderAccessoryHeight: (height: number) => void;
81
+ /**
82
+ * Primary scroll container's resolved top padding in dp, including header clearance.
83
+ * Used to position a sticky accessory below the large title at scroll offset zero.
84
+ */
85
+ contentTopPadding: number;
86
+ /** Reported by the primary scroll container from its layout or resolved padding. */
87
+ setContentTopPadding: (padding: number) => void;
81
88
  /** Vertical scroll offset, updated on the UI thread for header animations. */
82
89
  scrollY: SharedValue<number>;
83
90
  /**
@@ -142,6 +149,7 @@ export function ScreenScrollProvider({
142
149
  }: ScreenScrollProviderProps) {
143
150
  const scrollY = useSharedValue(0);
144
151
  const [headerAccessoryHeight, setAccessoryHeight] = useState(0);
152
+ const [contentTopPadding, setTopPadding] = useState(0);
145
153
  const headerCollapseHeight = useSharedValue(0);
146
154
  const [headerSnapOffsets, setHeaderSnapOffsets] = useState<number[]>();
147
155
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
@@ -161,6 +169,10 @@ export function ScreenScrollProvider({
161
169
  setAccessoryHeight(Math.max(0, Math.round(height)));
162
170
  }, []);
163
171
 
172
+ const setContentTopPadding = useCallback((padding: number) => {
173
+ setTopPadding(Math.max(0, Math.round(padding)));
174
+ }, []);
175
+
164
176
  const scrollHandler = useAnimatedScrollHandler({
165
177
  onScroll: (event) => {
166
178
  scrollY.value = event.contentOffset.y;
@@ -179,6 +191,8 @@ export function ScreenScrollProvider({
179
191
  hasHeader,
180
192
  headerAccessoryHeight,
181
193
  setHeaderAccessoryHeight,
194
+ contentTopPadding,
195
+ setContentTopPadding,
182
196
  scrollY,
183
197
  headerCollapseHeight,
184
198
  headerSnapOffsets,
@@ -196,6 +210,8 @@ export function ScreenScrollProvider({
196
210
  hasHeader,
197
211
  headerAccessoryHeight,
198
212
  setHeaderAccessoryHeight,
213
+ contentTopPadding,
214
+ setContentTopPadding,
199
215
  scrollY,
200
216
  headerCollapseHeight,
201
217
  headerSnapOffsets,
@@ -7,12 +7,19 @@ import {
7
7
  type ReactElement,
8
8
  type Ref,
9
9
  type RefAttributes,
10
+ useEffect,
10
11
  useMemo,
11
12
  } from "react";
12
- import { FlatList, type FlatListProps, type ListRenderItem, View } from "react-native";
13
+ import {
14
+ FlatList,
15
+ type FlatListProps,
16
+ type ListRenderItem,
17
+ StyleSheet as NativeStyleSheet,
18
+ View,
19
+ } from "react-native";
13
20
 
14
21
  import Animated from "react-native-reanimated";
15
- import { StyleSheet } from "react-native-unistyles";
22
+ import { StyleSheet, useUnistyles } from "react-native-unistyles";
16
23
 
17
24
  import { resolveScreenPadding } from "#theme/screen-padding";
18
25
  import { PortalHost } from "../portal";
@@ -175,6 +182,7 @@ const ScreenListBase = forwardRef(function ScreenList(
175
182
  contentPadding: screenPadding,
176
183
  hasHeader,
177
184
  headerAccessoryHeight,
185
+ setContentTopPadding,
178
186
  contentOwnsBottomInset,
179
187
  platformAdjustsInsets,
180
188
  } = useScreen();
@@ -184,6 +192,18 @@ const ScreenListBase = forwardRef(function ScreenList(
184
192
  useNestedContainerWarning("Screen.List", horizontal);
185
193
  const mergedRef = useScreenRef(isDefaultFlatList ? scrollRef : undefined, ref);
186
194
  const resolvedPadding = contentPadding ?? margins ?? screenPadding;
195
+ const { theme, rt } = useUnistyles();
196
+ const { y: paddingY } = resolveScreenPadding(theme, rt.breakpoint);
197
+ const defaultTopPadding =
198
+ (hasHeader ? theme.layout.header.height + headerAccessoryHeight : 0) +
199
+ (hasVerticalPadding(resolvedPadding) ? paddingY : 0);
200
+ const paddingTopOverride = NativeStyleSheet.flatten(contentContainerStyle)?.paddingTop;
201
+ const contentTopPadding = typeof paddingTopOverride === "number" ? paddingTopOverride : defaultTopPadding;
202
+ // ListHeaderComponent is wrapped by the list, so its local layout.y excludes
203
+ // content padding. Report the resolved padding, including caller overrides.
204
+ useEffect(() => {
205
+ if (!horizontal) setContentTopPadding(contentTopPadding);
206
+ }, [contentTopPadding, horizontal, setContentTopPadding]);
187
207
  const resolvedLoading = isLoading ?? loading;
188
208
  const placeholder = renderScreenPlaceholder({
189
209
  isLoading: resolvedLoading,
@@ -1,5 +1,12 @@
1
- import { Children, type ComponentRef, forwardRef, type ReactNode } from "react";
2
- import { type ScrollView, type ScrollViewProps, type StyleProp, View, type ViewStyle } from "react-native";
1
+ import { Children, type ComponentRef, forwardRef, type ReactNode, useCallback } from "react";
2
+ import {
3
+ type LayoutChangeEvent,
4
+ type ScrollView,
5
+ type ScrollViewProps,
6
+ type StyleProp,
7
+ View,
8
+ type ViewStyle,
9
+ } from "react-native";
3
10
 
4
11
  import Animated from "react-native-reanimated";
5
12
  import { StyleSheet } from "react-native-unistyles";
@@ -72,6 +79,7 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
72
79
  contentPadding: screenPadding,
73
80
  hasHeader,
74
81
  headerAccessoryHeight,
82
+ setContentTopPadding,
75
83
  background,
76
84
  contentOwnsBottomInset,
77
85
  platformAdjustsInsets,
@@ -89,6 +97,11 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
89
97
  isEmpty: isEmpty ?? Children.toArray(children).length === 0,
90
98
  });
91
99
  const composedScrollHandler = useScreenScrollHandler(scrollHandler, onScroll, externalScrollHandler);
100
+ // As the first child, the header wrapper's layout.y includes resolved top padding.
101
+ const handleHeaderWrapperLayout = useCallback(
102
+ (event: LayoutChangeEvent) => setContentTopPadding(event.nativeEvent.layout.y),
103
+ [setContentTopPadding]
104
+ );
92
105
 
93
106
  return (
94
107
  <Animated.ScrollView
@@ -112,7 +125,7 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
112
125
  {...rest}
113
126
  onScroll={composedScrollHandler}
114
127
  >
115
- <View style={styles.headerWrapper(resolvedPadding)}>
128
+ <View style={styles.headerWrapper(resolvedPadding)} onLayout={handleHeaderWrapperLayout}>
116
129
  <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
117
130
  </View>
118
131
  <View style={[styles.content(horizontal), contentStyle]}>{placeholder ?? children}</View>