react-native-terra-ui 0.8.1 → 0.9.1
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 +22 -0
- package/lib/module/components/screen/Screen.js +135 -25
- package/lib/module/components/screen/Screen.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 +20 -4
- package/lib/module/components/screen/ScreenList.js.map +1 -1
- package/lib/module/components/screen/ScreenScrollView.js +18 -4
- package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
- package/lib/module/components/screen/hooks/use-nested-container-warning.js +39 -0
- package/lib/module/components/screen/hooks/use-nested-container-warning.js.map +1 -0
- package/lib/module/components/screen/parts/ScreenBottomInset.android.js +42 -0
- package/lib/module/components/screen/parts/ScreenBottomInset.android.js.map +1 -0
- package/lib/module/components/screen/parts/ScreenBottomInset.js +37 -0
- package/lib/module/components/screen/parts/ScreenBottomInset.js.map +1 -0
- package/lib/module/components/screen/parts/ScreenFrame.js +1 -0
- package/lib/module/components/screen/parts/ScreenFrame.js.map +1 -1
- package/lib/typescript/src/components/screen/Screen.d.ts +27 -5
- package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenContext.d.ts +40 -1
- 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/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts +24 -0
- package/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts.map +1 -0
- package/lib/typescript/src/components/screen/parts/ScreenBottomInset.android.d.ts +11 -0
- package/lib/typescript/src/components/screen/parts/ScreenBottomInset.android.d.ts.map +1 -0
- package/lib/typescript/src/components/screen/parts/ScreenBottomInset.d.ts +17 -0
- package/lib/typescript/src/components/screen/parts/ScreenBottomInset.d.ts.map +1 -0
- package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/screen/Screen.tsx +147 -16
- package/src/components/screen/ScreenContext.tsx +48 -0
- package/src/components/screen/ScreenList.tsx +23 -2
- package/src/components/screen/ScreenScrollView.tsx +24 -2
- package/src/components/screen/hooks/use-nested-container-warning.ts +40 -0
- package/src/components/screen/parts/ScreenBottomInset.android.tsx +35 -0
- package/src/components/screen/parts/ScreenBottomInset.tsx +29 -0
- package/src/components/screen/parts/ScreenFrame.tsx +8 -1
|
@@ -6,8 +6,10 @@ import { StyleSheet } from "react-native-unistyles";
|
|
|
6
6
|
|
|
7
7
|
import { resolveScreenPadding } from "#theme/screen-padding";
|
|
8
8
|
import { PortalHost } from "../portal";
|
|
9
|
+
import { useNestedContainerWarning } from "./hooks/use-nested-container-warning";
|
|
9
10
|
import { useScreenRef } from "./hooks/use-screen-ref";
|
|
10
11
|
import { useScreenScrollHandler } from "./hooks/use-screen-scroll-handler";
|
|
12
|
+
import { ScreenBottomInset } from "./parts/ScreenBottomInset";
|
|
11
13
|
import { renderScreenPlaceholder } from "./parts/ScreenPlaceholder";
|
|
12
14
|
import { SCREEN_HEADER_PORTAL_HOST, useScreen } from "./ScreenContext";
|
|
13
15
|
import type { ScreenContentPadding, ScreenScrollableProps } from "./types";
|
|
@@ -64,7 +66,19 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
|
|
|
64
66
|
},
|
|
65
67
|
ref
|
|
66
68
|
) {
|
|
67
|
-
const {
|
|
69
|
+
const {
|
|
70
|
+
scrollRef,
|
|
71
|
+
scrollHandler,
|
|
72
|
+
contentPadding: screenPadding,
|
|
73
|
+
hasHeader,
|
|
74
|
+
background,
|
|
75
|
+
contentOwnsBottomInset,
|
|
76
|
+
platformAdjustsInsets,
|
|
77
|
+
} = useScreen();
|
|
78
|
+
// The root yields the bottom edge only to a vertical container, so a horizontal
|
|
79
|
+
// one here is a secondary container and must not pay the inset a second time.
|
|
80
|
+
const ownsBottomInset = contentOwnsBottomInset && !horizontal;
|
|
81
|
+
useNestedContainerWarning("Screen.ScrollView", horizontal);
|
|
68
82
|
const resolvedPadding = contentPadding ?? margins ?? screenPadding;
|
|
69
83
|
const mergedRef = useScreenRef(scrollRef, ref);
|
|
70
84
|
const placeholder = renderScreenPlaceholder({
|
|
@@ -80,7 +94,12 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
|
|
|
80
94
|
ref={mergedRef}
|
|
81
95
|
style={[{ backgroundColor: background }, style]}
|
|
82
96
|
horizontal={horizontal}
|
|
83
|
-
|
|
97
|
+
/*
|
|
98
|
+
UIKit owns the vertical insets on a header-less iOS screen — see
|
|
99
|
+
`platformAdjustsInsets`. Everywhere else the frame has already paid
|
|
100
|
+
them, so a second adjustment here would count them twice.
|
|
101
|
+
*/
|
|
102
|
+
contentInsetAdjustmentBehavior={platformAdjustsInsets ? "automatic" : "never"}
|
|
84
103
|
automaticallyAdjustContentInsets={false}
|
|
85
104
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
|
86
105
|
showsVerticalScrollIndicator={false}
|
|
@@ -96,11 +115,14 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
|
|
|
96
115
|
<PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
|
|
97
116
|
</View>
|
|
98
117
|
<View style={[styles.content(horizontal), contentStyle]}>{placeholder ?? children}</View>
|
|
118
|
+
{ownsBottomInset && <ScreenBottomInset />}
|
|
99
119
|
</Animated.ScrollView>
|
|
100
120
|
);
|
|
101
121
|
}
|
|
102
122
|
);
|
|
103
123
|
|
|
124
|
+
ScreenScrollView.displayName = "Screen.ScrollView";
|
|
125
|
+
|
|
104
126
|
const styles = StyleSheet.create((theme, runtime) => ({
|
|
105
127
|
scrollContent: (hasHeader: boolean, padding: ScreenContentPadding, extraBottomPadding: number) => {
|
|
106
128
|
const { x, y } = resolveScreenPadding(theme, runtime.breakpoint);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import { useScreen } from "../ScreenContext";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Warns, in development, when this container is the one the screen root failed
|
|
7
|
+
* to find.
|
|
8
|
+
*
|
|
9
|
+
* The root inspects its own children and fragments but does not execute
|
|
10
|
+
* component children, so a container rendered inside one is invisible to it (see
|
|
11
|
+
* `hasScrollContainerOwningBottom`). On a header-less iOS screen that changes
|
|
12
|
+
* behaviour rather than just bookkeeping: the frame pads the top edge, the scroll
|
|
13
|
+
* view no longer fills the screen, and UIKit stops collapsing the native large
|
|
14
|
+
* title — silently, with the screen otherwise looking correct.
|
|
15
|
+
*
|
|
16
|
+
* The container is the only place that can tell. `platformInsetsEligible` says
|
|
17
|
+
* the screen qualified; a container mounting while `platformAdjustsInsets` is
|
|
18
|
+
* false says the root never saw it. Both true together mean exactly this mistake,
|
|
19
|
+
* so there is nothing here to false-positive on: a screen with no scroll
|
|
20
|
+
* container at all never runs this hook, and a deliberate `safeArea` opt-out is
|
|
21
|
+
* not eligible in the first place.
|
|
22
|
+
*
|
|
23
|
+
* @param container Name to report, e.g. `"Screen.List"`.
|
|
24
|
+
* @param horizontal Whether this container scrolls horizontally; those are never
|
|
25
|
+
* candidates for the vertical insets and never warn.
|
|
26
|
+
*/
|
|
27
|
+
export function useNestedContainerWarning(container: string, horizontal: boolean): void {
|
|
28
|
+
const { platformAdjustsInsets, platformInsetsEligible } = useScreen();
|
|
29
|
+
const isNested = platformInsetsEligible && !platformAdjustsInsets && !horizontal;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!__DEV__ || !isNested) return;
|
|
33
|
+
console.warn(
|
|
34
|
+
`[react-native-terra-ui] ${container} is nested inside another component, so its Screen could not find it. ` +
|
|
35
|
+
"On iOS a Screen with no Screen.Header hands its vertical insets to UIKit, but only when the scroll " +
|
|
36
|
+
"container is among its own children — nested, the frame pads the top instead and a native large title " +
|
|
37
|
+
`will not collapse. Render ${container} as a direct child of Screen; fragments are fine.`
|
|
38
|
+
);
|
|
39
|
+
}, [container, isNested]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
5
|
+
|
|
6
|
+
// Keep native codegen declarations outside the library's generated public types.
|
|
7
|
+
const { SafeAreaView } = require("react-native-screens/src/components/safe-area") as {
|
|
8
|
+
SafeAreaView: ComponentType<
|
|
9
|
+
ViewProps & {
|
|
10
|
+
edges: Record<"top" | "bottom" | "left" | "right", boolean>;
|
|
11
|
+
insetType: "all";
|
|
12
|
+
}
|
|
13
|
+
>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const BOTTOM_EDGE = { top: false, bottom: true, left: false, right: false };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Android's bottom safe area as scroll content — see the iOS file for why this
|
|
20
|
+
* is a native view rather than a measured inset.
|
|
21
|
+
*
|
|
22
|
+
* `insetType: "all"` is the same combination the Screen frame uses: system insets
|
|
23
|
+
* plus native interface insets, which is what carries a native tab bar. The frame
|
|
24
|
+
* leaves the bottom edge unconsumed when the content owns it, so this view still
|
|
25
|
+
* sees the inset and is the one to consume it.
|
|
26
|
+
*/
|
|
27
|
+
export function ScreenBottomInset() {
|
|
28
|
+
return <SafeAreaView edges={BOTTOM_EDGE} insetType="all" style={styles.spacer} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create(() => ({
|
|
32
|
+
// `flex: 0` cancels the `flex: 1` this SafeAreaView sets on itself, which would
|
|
33
|
+
// otherwise let the spacer eat the content container's growth.
|
|
34
|
+
spacer: { flex: 0, flexGrow: 0, flexShrink: 0 },
|
|
35
|
+
}));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
2
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
3
|
+
|
|
4
|
+
const BOTTOM_EDGE = ["bottom"] as const;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The bottom safe area as a piece of scroll content rather than frame padding.
|
|
8
|
+
*
|
|
9
|
+
* A scroll container that yields its bottom edge to the content (see
|
|
10
|
+
* `contentOwnsBottomInset`) keeps a full-bleed viewport, so its rows pass under
|
|
11
|
+
* a tab bar or home indicator instead of stopping above one. This spacer is what
|
|
12
|
+
* keeps the *end* of that content clear of the bar: an empty safe-area view whose
|
|
13
|
+
* own padding is its whole height.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately a native safe-area view rather than a measured number. The inset
|
|
16
|
+
* that matters here includes bars this screen does not own — a native tab bar is
|
|
17
|
+
* a sibling view, not a window inset — and asking the platform for padding gets
|
|
18
|
+
* the same answer the frame would have applied, on both platforms, without a JS
|
|
19
|
+
* round trip through a value that lands a frame late.
|
|
20
|
+
*/
|
|
21
|
+
export function ScreenBottomInset() {
|
|
22
|
+
return <SafeAreaView edges={BOTTOM_EDGE} style={styles.spacer} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create(() => ({
|
|
26
|
+
// Height comes entirely from the inset, so the spacer must not flex with the
|
|
27
|
+
// content container's `flexGrow: 1`.
|
|
28
|
+
spacer: { flexGrow: 0, flexShrink: 0 },
|
|
29
|
+
}));
|
|
@@ -19,7 +19,14 @@ export function ScreenFrame({ children, edges }: PropsWithChildren<{ edges: Scre
|
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
21
|
<ScreenSafeArea edges={edges}>
|
|
22
|
-
|
|
22
|
+
{/*
|
|
23
|
+
`collapsable={false}` for the same reason as the `Screen` root: whatever
|
|
24
|
+
Fabric decides about this view, its children have to stay inside it.
|
|
25
|
+
UIKit finds a screen's scroll view by walking first subviews down from
|
|
26
|
+
the view controller, and a mounted-but-childless view in that chain is a
|
|
27
|
+
dead end — the large title then never collapses.
|
|
28
|
+
*/}
|
|
29
|
+
<View collapsable={false} style={styles.content} onLayout={onLayout}>
|
|
23
30
|
{children}
|
|
24
31
|
</View>
|
|
25
32
|
</ScreenSafeArea>
|