react-native-terra-ui 0.11.6 → 0.11.7
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.
- package/CHANGELOG.md +6 -0
- package/lib/module/components/header/parts/HeaderAccessory.js +25 -22
- package/lib/module/components/header/parts/HeaderAccessory.js.map +1 -1
- package/lib/module/components/header/parts/HeaderSurface.js +13 -20
- package/lib/module/components/header/parts/HeaderSurface.js.map +1 -1
- package/lib/module/components/header/variants/LargeTitleHeader/index.js +106 -40
- package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
- package/lib/module/components/header/variants/TitleHeader/index.js +5 -1
- package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
- package/lib/module/components/screen/ScreenContext.js +7 -1
- package/lib/module/components/screen/ScreenContext.js.map +1 -1
- package/lib/module/components/screen/ScreenList.js +19 -2
- package/lib/module/components/screen/ScreenList.js.map +1 -1
- package/lib/module/components/screen/ScreenScrollView.js +5 -1
- package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
- package/lib/typescript/src/components/header/index.d.ts +1 -1
- package/lib/typescript/src/components/header/index.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts +11 -11
- package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +8 -10
- package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -1
- package/lib/typescript/src/components/header/types.d.ts +11 -7
- package/lib/typescript/src/components/header/types.d.ts.map +1 -1
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +8 -1
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +3 -4
- package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenContext.d.ts +7 -0
- package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/header/index.ts +1 -1
- package/src/components/header/parts/HeaderAccessory.tsx +21 -23
- package/src/components/header/parts/HeaderSurface.tsx +17 -20
- package/src/components/header/types.ts +12 -7
- package/src/components/header/variants/LargeTitleHeader/index.tsx +86 -15
- package/src/components/header/variants/TitleHeader/index.tsx +15 -6
- package/src/components/screen/ScreenContext.tsx +16 -0
- package/src/components/screen/ScreenList.tsx +22 -2
- 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
|
-
|
|
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 = "pinned",
|
|
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,7 +160,39 @@ 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
|
+
// Sticky accessories reserve space below the large title; pinned ones add top padding.
|
|
165
|
+
const [stickyAccessoryHeight, setStickyAccessoryHeight] = useState(0);
|
|
166
|
+
const [largeTitleHeight, setLargeTitleHeight] = useState(0);
|
|
147
167
|
const isLargeTitleHiddenValue = useSharedValue(isLargeTitleHidden);
|
|
168
|
+
|
|
169
|
+
const handleAccessoryHeightChange = useCallback(
|
|
170
|
+
(height: number) => {
|
|
171
|
+
if (isSticky) setStickyAccessoryHeight(Math.max(0, Math.round(height)));
|
|
172
|
+
else setHeaderAccessoryHeight(height);
|
|
173
|
+
},
|
|
174
|
+
[isSticky, setHeaderAccessoryHeight]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const handleLargeTitleHeightChange = useCallback((height: number) => {
|
|
178
|
+
setLargeTitleHeight(Math.max(0, Math.round(height)));
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
// Rest below the title, including the scroll container's top padding.
|
|
182
|
+
const stickyAccessoryOffset = Math.max(
|
|
183
|
+
0,
|
|
184
|
+
contentTopPadding + largeTitleHeight - theme.layout.header.height
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Zero translation places the accessory directly below the compact bar.
|
|
188
|
+
const stickyAccessoryStyle = useAnimatedStyle(() => {
|
|
189
|
+
if (isLargeTitleHiddenValue.value) {
|
|
190
|
+
return { transform: [{ translateY: 0 }] };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
transform: [{ translateY: Math.max(0, stickyAccessoryOffset - scrollY.value) }],
|
|
194
|
+
};
|
|
195
|
+
});
|
|
148
196
|
const headerCollapseHeight = theme.layout.header.height;
|
|
149
197
|
styles.useVariants({ titleAlignment });
|
|
150
198
|
|
|
@@ -212,14 +260,21 @@ export function LargeTitleHeader({
|
|
|
212
260
|
);
|
|
213
261
|
|
|
214
262
|
return (
|
|
215
|
-
|
|
263
|
+
// Let touches in the expanded header's empty space reach the scroll content.
|
|
264
|
+
<View style={styles.container(isSticky ? stickyAccessoryOffset : 0)} pointerEvents="box-none">
|
|
216
265
|
{!isLargeTitleHidden && (
|
|
217
266
|
<Portal hostName={SCREEN_HEADER_PORTAL_HOST}>
|
|
218
|
-
<LargeTitlePortalContent
|
|
267
|
+
<LargeTitlePortalContent
|
|
268
|
+
title={title}
|
|
269
|
+
overline={overline}
|
|
270
|
+
subtitle={subtitle}
|
|
271
|
+
onHeightChange={isSticky ? handleLargeTitleHeightChange : undefined}
|
|
272
|
+
/>
|
|
273
|
+
{/* Reserve space for the accessory rendered in the overlay. */}
|
|
274
|
+
{isSticky && accessory != null && <HeaderAccessorySpacer height={stickyAccessoryHeight} />}
|
|
219
275
|
</Portal>
|
|
220
276
|
)}
|
|
221
|
-
{/*
|
|
222
|
-
scrolling under the header meets a single boundary rather than two. */}
|
|
277
|
+
{/* Share a background between the bar and a pinned accessory. */}
|
|
223
278
|
<View style={styles.barGroup}>
|
|
224
279
|
<HeaderSurface color={gradientColor} animatedStyle={gradientOpacityStyle} />
|
|
225
280
|
<View style={styles.bar}>
|
|
@@ -243,21 +298,37 @@ export function LargeTitleHeader({
|
|
|
243
298
|
</Animated.View>
|
|
244
299
|
{trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
|
|
245
300
|
</View>
|
|
246
|
-
{
|
|
247
|
-
|
|
301
|
+
{accessory != null && !isSticky && (
|
|
302
|
+
<HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
|
|
303
|
+
{accessory}
|
|
304
|
+
</HeaderAccessory>
|
|
305
|
+
)}
|
|
248
306
|
</View>
|
|
307
|
+
{accessory != null && isSticky && (
|
|
308
|
+
// The background moves with the sticky accessory.
|
|
309
|
+
<Animated.View style={[styles.stickyAccessory, stickyAccessoryStyle]}>
|
|
310
|
+
<HeaderSurface color={gradientColor} fadeHeight={HEADER_FADE_EXTENT} />
|
|
311
|
+
<HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
|
|
312
|
+
{accessory}
|
|
313
|
+
</HeaderAccessory>
|
|
314
|
+
</Animated.View>
|
|
315
|
+
)}
|
|
249
316
|
</View>
|
|
250
317
|
);
|
|
251
318
|
}
|
|
252
319
|
|
|
253
320
|
const styles = StyleSheet.create((theme) => ({
|
|
254
|
-
|
|
321
|
+
// Include the translated accessory in the parent's touchable bounds.
|
|
322
|
+
container: (accessoryOffset: number) => ({
|
|
255
323
|
position: "absolute",
|
|
256
324
|
top: 0,
|
|
257
325
|
left: 0,
|
|
258
326
|
right: 0,
|
|
259
327
|
zIndex: 1000,
|
|
260
|
-
|
|
328
|
+
paddingBottom: accessoryOffset,
|
|
329
|
+
}),
|
|
330
|
+
// Normal flow places the accessory directly below the bar.
|
|
331
|
+
stickyAccessory: { position: "relative" },
|
|
261
332
|
// Anchors the absolutely positioned surface over the bar and the accessory both.
|
|
262
333
|
barGroup: { position: "relative" },
|
|
263
334
|
bar: {
|
|
@@ -283,7 +354,7 @@ const styles = StyleSheet.create((theme) => ({
|
|
|
283
354
|
portalContent: {
|
|
284
355
|
gap: 2,
|
|
285
356
|
minHeight: theme.layout.header.height,
|
|
286
|
-
paddingBottom: theme.spacing["
|
|
357
|
+
paddingBottom: theme.spacing["1"],
|
|
287
358
|
paddingHorizontal: theme.layout.screen.padding.x,
|
|
288
359
|
},
|
|
289
360
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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 &&
|
|
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 {
|
|
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 {
|
|
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>
|