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.
- package/CHANGELOG.md +12 -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 +130 -42
- 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 +110 -18
- 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 = "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
|
-
|
|
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
|
|
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
|
-
{
|
|
222
|
-
|
|
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
|
-
{
|
|
247
|
-
|
|
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
|
-
|
|
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["
|
|
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
|
|
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>
|