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.
Files changed (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/lib/module/components/screen/Screen.js +135 -25
  3. package/lib/module/components/screen/Screen.js.map +1 -1
  4. package/lib/module/components/screen/ScreenContext.js +7 -1
  5. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  6. package/lib/module/components/screen/ScreenList.js +20 -4
  7. package/lib/module/components/screen/ScreenList.js.map +1 -1
  8. package/lib/module/components/screen/ScreenScrollView.js +18 -4
  9. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  10. package/lib/module/components/screen/hooks/use-nested-container-warning.js +39 -0
  11. package/lib/module/components/screen/hooks/use-nested-container-warning.js.map +1 -0
  12. package/lib/module/components/screen/parts/ScreenBottomInset.android.js +42 -0
  13. package/lib/module/components/screen/parts/ScreenBottomInset.android.js.map +1 -0
  14. package/lib/module/components/screen/parts/ScreenBottomInset.js +37 -0
  15. package/lib/module/components/screen/parts/ScreenBottomInset.js.map +1 -0
  16. package/lib/module/components/screen/parts/ScreenFrame.js +1 -0
  17. package/lib/module/components/screen/parts/ScreenFrame.js.map +1 -1
  18. package/lib/typescript/src/components/screen/Screen.d.ts +27 -5
  19. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  20. package/lib/typescript/src/components/screen/ScreenContext.d.ts +40 -1
  21. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  22. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  23. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  24. package/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts +24 -0
  25. package/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts.map +1 -0
  26. package/lib/typescript/src/components/screen/parts/ScreenBottomInset.android.d.ts +11 -0
  27. package/lib/typescript/src/components/screen/parts/ScreenBottomInset.android.d.ts.map +1 -0
  28. package/lib/typescript/src/components/screen/parts/ScreenBottomInset.d.ts +17 -0
  29. package/lib/typescript/src/components/screen/parts/ScreenBottomInset.d.ts.map +1 -0
  30. package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/components/screen/Screen.tsx +147 -16
  33. package/src/components/screen/ScreenContext.tsx +48 -0
  34. package/src/components/screen/ScreenList.tsx +23 -2
  35. package/src/components/screen/ScreenScrollView.tsx +24 -2
  36. package/src/components/screen/hooks/use-nested-container-warning.ts +40 -0
  37. package/src/components/screen/parts/ScreenBottomInset.android.tsx +35 -0
  38. package/src/components/screen/parts/ScreenBottomInset.tsx +29 -0
  39. 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 { scrollRef, scrollHandler, contentPadding: screenPadding, hasHeader, background } = useScreen();
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
- contentInsetAdjustmentBehavior="never"
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
- <View style={styles.content} onLayout={onLayout}>
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>