react-native-terra-ui 0.11.5 → 0.11.6

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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/module/components/chip/Chip.js +25 -8
  3. package/lib/module/components/chip/Chip.js.map +1 -1
  4. package/lib/module/components/chip/utils.js +1 -1
  5. package/lib/module/components/chip/utils.js.map +1 -1
  6. package/lib/module/components/header/index.js.map +1 -1
  7. package/lib/module/components/header/parts/HeaderAccessory.js +55 -0
  8. package/lib/module/components/header/parts/HeaderAccessory.js.map +1 -0
  9. package/lib/module/components/header/parts/HeaderSurface.js +65 -0
  10. package/lib/module/components/header/parts/HeaderSurface.js.map +1 -0
  11. package/lib/module/components/header/types.js +2 -0
  12. package/lib/module/components/header/types.js.map +1 -0
  13. package/lib/module/components/header/variants/LargeTitleHeader/index.js +30 -28
  14. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  15. package/lib/module/components/header/variants/TitleHeader/index.js +28 -27
  16. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  17. package/lib/module/components/screen/ScreenContext.js +10 -1
  18. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  19. package/lib/module/components/screen/ScreenList.js +5 -3
  20. package/lib/module/components/screen/ScreenList.js.map +1 -1
  21. package/lib/module/components/screen/ScreenScrollView.js +5 -3
  22. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  23. package/lib/typescript/src/components/chip/Chip.d.ts +8 -0
  24. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -1
  25. package/lib/typescript/src/components/chip/utils.d.ts +6 -1
  26. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -1
  27. package/lib/typescript/src/components/header/index.d.ts +1 -0
  28. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts +19 -0
  30. package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts.map +1 -0
  31. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +24 -0
  32. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -0
  33. package/lib/typescript/src/components/header/types.d.ts +11 -0
  34. package/lib/typescript/src/components/header/types.d.ts.map +1 -0
  35. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +1 -1
  36. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +15 -1
  38. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  39. package/lib/typescript/src/components/screen/ScreenContext.d.ts +15 -0
  40. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  41. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  42. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/components/chip/Chip.tsx +22 -9
  45. package/src/components/chip/utils.ts +7 -2
  46. package/src/components/header/index.ts +1 -0
  47. package/src/components/header/parts/HeaderAccessory.tsx +57 -0
  48. package/src/components/header/parts/HeaderSurface.tsx +65 -0
  49. package/src/components/header/types.ts +10 -0
  50. package/src/components/header/variants/LargeTitleHeader/index.tsx +33 -32
  51. package/src/components/header/variants/TitleHeader/index.tsx +35 -21
  52. package/src/components/screen/ScreenContext.tsx +26 -0
  53. package/src/components/screen/ScreenList.tsx +8 -3
  54. package/src/components/screen/ScreenScrollView.tsx +12 -3
@@ -0,0 +1,57 @@
1
+ import { type ReactNode, useCallback, useContext, useEffect } from "react";
2
+ import { type LayoutChangeEvent, View } from "react-native";
3
+
4
+ import { StyleSheet } from "react-native-unistyles";
5
+
6
+ import { ScreenContext } from "../../screen/ScreenContext";
7
+ import type { HeaderAccessoryPadding } from "../types";
8
+
9
+ export interface HeaderAccessoryProps {
10
+ children: ReactNode;
11
+ /** Horizontal padding around the accessory. */
12
+ padding: HeaderAccessoryPadding;
13
+ }
14
+
15
+ /**
16
+ * Pinned content below a header bar — a search field, a tab strip.
17
+ *
18
+ * Reports its measured height to the surrounding `Screen` so scroll containers
19
+ * reserve it above their content. The theme header height covers the bar alone
20
+ * and the accessory is whatever the caller passed, so nothing but a measurement
21
+ * can know how far down the content has to start.
22
+ *
23
+ * Outside a `Screen` there is nothing to report to and it simply renders.
24
+ */
25
+ export function HeaderAccessory({ children, padding }: HeaderAccessoryProps) {
26
+ const setHeaderAccessoryHeight = useContext(ScreenContext)?.setHeaderAccessoryHeight;
27
+
28
+ const handleLayout = useCallback(
29
+ (event: LayoutChangeEvent) => {
30
+ setHeaderAccessoryHeight?.(event.nativeEvent.layout.height);
31
+ },
32
+ [setHeaderAccessoryHeight]
33
+ );
34
+
35
+ // An unmounted accessory never lays out again, so without this the screen
36
+ // would keep reserving room for content that is no longer there.
37
+ useEffect(() => () => setHeaderAccessoryHeight?.(0), [setHeaderAccessoryHeight]);
38
+
39
+ styles.useVariants({ padding });
40
+
41
+ return (
42
+ <View style={styles.accessory} onLayout={handleLayout}>
43
+ {children}
44
+ </View>
45
+ );
46
+ }
47
+
48
+ const styles = StyleSheet.create((theme) => ({
49
+ accessory: {
50
+ variants: {
51
+ padding: {
52
+ x: { paddingHorizontal: theme.layout.screen.padding.x },
53
+ none: {},
54
+ },
55
+ },
56
+ },
57
+ }));
@@ -0,0 +1,65 @@
1
+ import { View } from "react-native";
2
+
3
+ import Animated, { type useAnimatedStyle } from "react-native-reanimated";
4
+ import { StyleSheet } from "react-native-unistyles";
5
+
6
+ import { HEADER_FADE_EXTENT, HeaderGradientOverlay } from "./HeaderGradientOverlay";
7
+
8
+ type HeaderOpacityStyle = ReturnType<typeof useAnimatedStyle<{ opacity: number }>>;
9
+
10
+ export interface HeaderSurfaceProps {
11
+ /** Fill color, fading to fully transparent along the bottom edge. */
12
+ color: string;
13
+ /** Opacity style for variants that fade the whole surface in as they collapse. */
14
+ animatedStyle?: HeaderOpacityStyle;
15
+ }
16
+
17
+ /**
18
+ * The painted background behind a header — the bar, any accessory below it, and
19
+ * the fade that releases scrolling content at the bottom edge.
20
+ *
21
+ * Split into a flat fill and a fixed-height ramp rather than one gradient over
22
+ * the whole box, because {@link HeaderGradientOverlay}'s curve is expressed in
23
+ * fractions of its own height. Stretched over a tall accessory the fade would
24
+ * begin partway up it, and content passing behind would show through the thing
25
+ * the caller put there to be read. Pinning the ramp to the bar's height keeps it
26
+ * identical with or without an accessory, and everything above it stays solid.
27
+ */
28
+ export function HeaderSurface({ color, animatedStyle }: HeaderSurfaceProps) {
29
+ return (
30
+ <Animated.View style={[styles.surface, animatedStyle]} pointerEvents="none">
31
+ <View style={styles.fill(color)} />
32
+ <View style={styles.fade}>
33
+ <HeaderGradientOverlay color={color} />
34
+ </View>
35
+ </Animated.View>
36
+ );
37
+ }
38
+
39
+ const styles = StyleSheet.create((theme) => ({
40
+ // Covers the bar and any accessory, overhanging so the fade's tail finishes in
41
+ // open space rather than being clipped at the header's edge.
42
+ surface: {
43
+ position: "absolute",
44
+ top: 0,
45
+ left: 0,
46
+ right: 0,
47
+ bottom: -HEADER_FADE_EXTENT,
48
+ },
49
+ // Collapses to nothing when there is no accessory, leaving the ramp alone.
50
+ fill: (color: string) => ({
51
+ position: "absolute",
52
+ top: 0,
53
+ left: 0,
54
+ right: 0,
55
+ bottom: theme.layout.header.height + HEADER_FADE_EXTENT,
56
+ backgroundColor: color,
57
+ }),
58
+ fade: {
59
+ position: "absolute",
60
+ left: 0,
61
+ right: 0,
62
+ bottom: 0,
63
+ height: theme.layout.header.height + HEADER_FADE_EXTENT,
64
+ },
65
+ }));
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Horizontal padding applied to a header accessory.
3
+ *
4
+ * - `'x'` — the screen's horizontal padding, matching the bar above it. What a
5
+ * field-shaped accessory (a search input, a segmented control) wants.
6
+ * - `'none'` — full bleed. What a horizontally scrolling accessory (a tab strip,
7
+ * a filter row) wants, so its content can run to the screen edge instead of
8
+ * being clipped at an inset the caller cannot scroll past.
9
+ */
10
+ export type HeaderAccessoryPadding = "x" | "none";
@@ -11,7 +11,8 @@ 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 { HEADER_FADE_EXTENT, HeaderGradientOverlay } from "../../parts/HeaderGradientOverlay";
14
+ import { HeaderAccessory } from "../../parts/HeaderAccessory";
15
+ import { HeaderSurface } from "../../parts/HeaderSurface";
15
16
  import { isCustomTitle, isTextTitle } from "../../utils";
16
17
  import type { TitleHeaderProps } from "../TitleHeader";
17
18
 
@@ -123,6 +124,8 @@ export function LargeTitleHeader({
123
124
  trailingActions,
124
125
  LeftToolbar,
125
126
  RightToolbar,
127
+ accessory,
128
+ accessoryPadding = "x",
126
129
  dismissAction = "none",
127
130
  navigation,
128
131
  onDismiss,
@@ -215,29 +218,33 @@ export function LargeTitleHeader({
215
218
  <LargeTitlePortalContent title={title} overline={overline} subtitle={subtitle} />
216
219
  </Portal>
217
220
  )}
218
- <View style={styles.bar}>
219
- <Animated.View style={[styles.gradient, gradientOpacityStyle]} pointerEvents="none">
220
- <HeaderGradientOverlay color={gradientColor} />
221
- </Animated.View>
222
- {leading != null && <Toolbar>{leading}</Toolbar>}
223
- {/* Text can never be the touch target, but custom content may be, so its
224
- container passes touches through to it instead of swallowing them. */}
225
- <Animated.View
226
- style={[styles.titleContainer, compactTitleAnimatedStyle]}
227
- pointerEvents={isCustomTitle(title) ? "box-none" : "none"}
228
- >
229
- {isTextTitle(title) ? (
230
- <AnimatedText
231
- style={styles.compactTitle}
232
- maxFontSizeMultiplier={theme.typography.variants["title-md"].maxFontSizeMultiplier}
233
- >
234
- {title}
235
- </AnimatedText>
236
- ) : (
237
- title
238
- )}
239
- </Animated.View>
240
- {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
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. */}
223
+ <View style={styles.barGroup}>
224
+ <HeaderSurface color={gradientColor} animatedStyle={gradientOpacityStyle} />
225
+ <View style={styles.bar}>
226
+ {leading != null && <Toolbar>{leading}</Toolbar>}
227
+ {/* Text can never be the touch target, but custom content may be, so its
228
+ container passes touches through to it instead of swallowing them. */}
229
+ <Animated.View
230
+ style={[styles.titleContainer, compactTitleAnimatedStyle]}
231
+ pointerEvents={isCustomTitle(title) ? "box-none" : "none"}
232
+ >
233
+ {isTextTitle(title) ? (
234
+ <AnimatedText
235
+ style={styles.compactTitle}
236
+ maxFontSizeMultiplier={theme.typography.variants["title-md"].maxFontSizeMultiplier}
237
+ >
238
+ {title}
239
+ </AnimatedText>
240
+ ) : (
241
+ title
242
+ )}
243
+ </Animated.View>
244
+ {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
245
+ </View>
246
+ {/* Pinned, so it stays legible while the large title collapses past it. */}
247
+ {accessory != null && <HeaderAccessory padding={accessoryPadding}>{accessory}</HeaderAccessory>}
241
248
  </View>
242
249
  </View>
243
250
  );
@@ -251,14 +258,8 @@ const styles = StyleSheet.create((theme) => ({
251
258
  right: 0,
252
259
  zIndex: 1000,
253
260
  },
254
- // Let the fade extend below the bar without intercepting touches.
255
- gradient: {
256
- position: "absolute",
257
- top: 0,
258
- left: 0,
259
- right: 0,
260
- bottom: -HEADER_FADE_EXTENT,
261
- },
261
+ // Anchors the absolutely positioned surface over the bar and the accessory both.
262
+ barGroup: { position: "relative" },
262
263
  bar: {
263
264
  height: theme.layout.header.height,
264
265
  flexDirection: "row",
@@ -10,7 +10,9 @@ import { useHeaderBackground } from "../../../screen/parts/ScreenHeaderAppearanc
10
10
  import { ScreenContext } from "../../../screen/ScreenContext";
11
11
  import { Toolbar } from "../../../toolbar";
12
12
  import { HeaderDismissButton, type HeaderDismissProps } from "../../header-buttons";
13
- import { HEADER_FADE_EXTENT, HeaderGradientOverlay } from "../../parts/HeaderGradientOverlay";
13
+ import { HeaderAccessory } from "../../parts/HeaderAccessory";
14
+ import { HeaderSurface } from "../../parts/HeaderSurface";
15
+ import type { HeaderAccessoryPadding } from "../../types";
14
16
  import { isCustomTitle, isTextTitle } from "../../utils";
15
17
 
16
18
  export interface TitleHeaderProps extends HeaderDismissProps {
@@ -34,6 +36,19 @@ export interface TitleHeaderProps extends HeaderDismissProps {
34
36
  trailingActions?: ReactNode;
35
37
  /** @deprecated Use trailingActions. */
36
38
  RightToolbar?: ReactNode;
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.
44
+ */
45
+ accessory?: ReactNode;
46
+ /**
47
+ * Horizontal padding around `accessory`. Use `'none'` for content that should
48
+ * run to the screen edge, such as a horizontally scrolling tab strip.
49
+ * Defaults to `'x'`, matching the bar.
50
+ */
51
+ accessoryPadding?: HeaderAccessoryPadding;
37
52
  /** Title placement within the bar. Defaults to `'center'`. */
38
53
  titleAlignment?: "center" | "left";
39
54
  /** Color token for the top safe area and fading bar overlay. Defaults to the Screen background. */
@@ -65,6 +80,8 @@ export function TitleHeader({
65
80
  trailingActions,
66
81
  LeftToolbar,
67
82
  RightToolbar,
83
+ accessory,
84
+ accessoryPadding = "x",
68
85
  dismissAction = "none",
69
86
  navigation,
70
87
  onDismiss,
@@ -107,19 +124,22 @@ export function TitleHeader({
107
124
  return (
108
125
  <View style={styles.container}>
109
126
  <View style={styles.safeArea(isTransparent, bgColor, screen !== null)} />
110
- <View style={styles.bar}>
111
- <View style={styles.gradient} pointerEvents="none">
112
- <HeaderGradientOverlay color={gradientColor} />
113
- </View>
114
- {leading != null && <Toolbar>{leading}</Toolbar>}
115
- {/* Text can never be the touch target, but custom content may be, so its
116
- container passes touches through to it instead of swallowing them. */}
117
- <View style={styles.titleContainer} pointerEvents={isCustomTitle(title) ? "box-none" : "none"}>
118
- {isTextTitle(title)
119
- ? String(title).trim().length > 0 && <Text style={styles.title}>{title}</Text>
120
- : title}
127
+ {/* The fade covers the bar and any accessory as one surface, so content
128
+ scrolling under the header meets a single boundary rather than two. */}
129
+ <View style={styles.barGroup}>
130
+ <HeaderSurface color={gradientColor} />
131
+ <View style={styles.bar}>
132
+ {leading != null && <Toolbar>{leading}</Toolbar>}
133
+ {/* Text can never be the touch target, but custom content may be, so its
134
+ container passes touches through to it instead of swallowing them. */}
135
+ <View style={styles.titleContainer} pointerEvents={isCustomTitle(title) ? "box-none" : "none"}>
136
+ {isTextTitle(title)
137
+ ? String(title).trim().length > 0 && <Text style={styles.title}>{title}</Text>
138
+ : title}
139
+ </View>
140
+ {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
121
141
  </View>
122
- {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
142
+ {accessory != null && <HeaderAccessory padding={accessoryPadding}>{accessory}</HeaderAccessory>}
123
143
  </View>
124
144
  </View>
125
145
  );
@@ -137,14 +157,8 @@ const styles = StyleSheet.create((theme, runtime) => ({
137
157
  height: withinScreen ? 0 : runtime.insets.top,
138
158
  backgroundColor: isTransparent ? "transparent" : bgColor,
139
159
  }),
140
- // Extend the fade below the bar without clipping content or intercepting touches.
141
- gradient: {
142
- position: "absolute",
143
- top: 0,
144
- left: 0,
145
- right: 0,
146
- bottom: -HEADER_FADE_EXTENT,
147
- },
160
+ // Anchors the absolutely positioned surface over the bar and the accessory both.
161
+ barGroup: { position: "relative" },
148
162
  bar: {
149
163
  height: theme.layout.header.height,
150
164
  flexDirection: "row",
@@ -63,6 +63,21 @@ export interface ScreenContextValue {
63
63
  margins: ScreenContentPadding;
64
64
  /** Whether a `Screen.Header` is present. Used by scroll containers to add top inset. */
65
65
  hasHeader: boolean;
66
+ /**
67
+ * Measured height (dp) of the pinned accessory a header renders below its bar
68
+ * — a search field, a tab strip. Zero when there is none.
69
+ *
70
+ * The theme header height covers the bar alone, and an accessory is whatever
71
+ * the caller put there, so scroll containers reserve this on top of it. That
72
+ * is why it is measured rather than declared: only the accessory knows how
73
+ * tall it is, and it can change with Dynamic Type or its own content.
74
+ */
75
+ headerAccessoryHeight: number;
76
+ /**
77
+ * Reports the measured accessory height. Called by the header that owns the
78
+ * accessory; zero releases the reserved space.
79
+ */
80
+ setHeaderAccessoryHeight: (height: number) => void;
66
81
  /** Vertical scroll offset, updated on the UI thread for header animations. */
67
82
  scrollY: SharedValue<number>;
68
83
  /**
@@ -126,6 +141,7 @@ export function ScreenScrollProvider({
126
141
  hasHeader = false,
127
142
  }: ScreenScrollProviderProps) {
128
143
  const scrollY = useSharedValue(0);
144
+ const [headerAccessoryHeight, setAccessoryHeight] = useState(0);
129
145
  const headerCollapseHeight = useSharedValue(0);
130
146
  const [headerSnapOffsets, setHeaderSnapOffsets] = useState<number[]>();
131
147
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
@@ -139,6 +155,12 @@ export function ScreenScrollProvider({
139
155
  [headerCollapseHeight]
140
156
  );
141
157
 
158
+ // Rounded so a subpixel layout measurement cannot loop the content padding
159
+ // against the layout that produced it.
160
+ const setHeaderAccessoryHeight = useCallback((height: number) => {
161
+ setAccessoryHeight(Math.max(0, Math.round(height)));
162
+ }, []);
163
+
142
164
  const scrollHandler = useAnimatedScrollHandler({
143
165
  onScroll: (event) => {
144
166
  scrollY.value = event.contentOffset.y;
@@ -155,6 +177,8 @@ export function ScreenScrollProvider({
155
177
  platformInsetsEligible,
156
178
  margins: contentPadding,
157
179
  hasHeader,
180
+ headerAccessoryHeight,
181
+ setHeaderAccessoryHeight,
158
182
  scrollY,
159
183
  headerCollapseHeight,
160
184
  headerSnapOffsets,
@@ -170,6 +194,8 @@ export function ScreenScrollProvider({
170
194
  platformAdjustsInsets,
171
195
  platformInsetsEligible,
172
196
  hasHeader,
197
+ headerAccessoryHeight,
198
+ setHeaderAccessoryHeight,
173
199
  scrollY,
174
200
  headerCollapseHeight,
175
201
  headerSnapOffsets,
@@ -174,6 +174,7 @@ const ScreenListBase = forwardRef(function ScreenList(
174
174
  headerSnapOffsets,
175
175
  contentPadding: screenPadding,
176
176
  hasHeader,
177
+ headerAccessoryHeight,
177
178
  contentOwnsBottomInset,
178
179
  platformAdjustsInsets,
179
180
  } = useScreen();
@@ -224,7 +225,7 @@ const ScreenListBase = forwardRef(function ScreenList(
224
225
  automaticallyAdjustContentInsets={false}
225
226
  automaticallyAdjustsScrollIndicatorInsets={false}
226
227
  contentContainerStyle={[
227
- styles.content(resolvedPadding, hasHeader, bottomInset, extraBottomPadding),
228
+ styles.content(resolvedPadding, hasHeader, bottomInset, extraBottomPadding, headerAccessoryHeight),
228
229
  contentContainerStyle,
229
230
  ]}
230
231
  ListHeaderComponent={header}
@@ -265,12 +266,16 @@ const styles = StyleSheet.create((theme, runtime) => ({
265
266
  padding: ScreenContentPadding,
266
267
  hasHeader: boolean,
267
268
  bottomInset: number | undefined,
268
- extraBottomPadding: number
269
+ extraBottomPadding: number,
270
+ accessoryHeight: number
269
271
  ) => {
270
272
  const { x, y } = resolveScreenPadding(theme, runtime.breakpoint);
271
273
  return {
272
274
  flexGrow: 1,
273
- paddingTop: (hasHeader ? theme.layout.header.height : 0) + (hasVerticalPadding(padding) ? y : 0),
275
+ // The theme height covers the bar; an accessory pinned under it is extra.
276
+ paddingTop:
277
+ (hasHeader ? theme.layout.header.height + accessoryHeight : 0) +
278
+ (hasVerticalPadding(padding) ? y : 0),
274
279
  paddingHorizontal: hasHorizontalPadding(padding) ? x : 0,
275
280
  paddingBottom: (bottomInset ?? (hasVerticalPadding(padding) ? y : 0)) + extraBottomPadding,
276
281
  };
@@ -71,6 +71,7 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
71
71
  scrollHandler,
72
72
  contentPadding: screenPadding,
73
73
  hasHeader,
74
+ headerAccessoryHeight,
74
75
  background,
75
76
  contentOwnsBottomInset,
76
77
  platformAdjustsInsets,
@@ -105,7 +106,7 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
105
106
  showsVerticalScrollIndicator={false}
106
107
  scrollEventThrottle={16}
107
108
  contentContainerStyle={[
108
- styles.scrollContent(hasHeader, resolvedPadding, extraBottomPadding),
109
+ styles.scrollContent(hasHeader, resolvedPadding, extraBottomPadding, headerAccessoryHeight),
109
110
  contentContainerStyle,
110
111
  ]}
111
112
  {...rest}
@@ -124,11 +125,19 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
124
125
  ScreenScrollView.displayName = "Screen.ScrollView";
125
126
 
126
127
  const styles = StyleSheet.create((theme, runtime) => ({
127
- scrollContent: (hasHeader: boolean, padding: ScreenContentPadding, extraBottomPadding: number) => {
128
+ scrollContent: (
129
+ hasHeader: boolean,
130
+ padding: ScreenContentPadding,
131
+ extraBottomPadding: number,
132
+ accessoryHeight: number
133
+ ) => {
128
134
  const { x, y } = resolveScreenPadding(theme, runtime.breakpoint);
129
135
  return {
130
136
  flexGrow: 1,
131
- paddingTop: (hasHeader ? theme.layout.header.height : 0) + (hasVerticalPadding(padding) ? y : 0),
137
+ // The theme height covers the bar; an accessory pinned under it is extra.
138
+ paddingTop:
139
+ (hasHeader ? theme.layout.header.height + accessoryHeight : 0) +
140
+ (hasVerticalPadding(padding) ? y : 0),
132
141
  paddingHorizontal: hasHorizontalPadding(padding) ? x : 0,
133
142
  paddingBottom: (hasVerticalPadding(padding) ? y : 0) + extraBottomPadding,
134
143
  };