react-native-terra-ui 0.9.0 → 0.10.0

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 (49) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/module/components/avatar/Avatar.js +7 -2
  3. package/lib/module/components/avatar/Avatar.js.map +1 -1
  4. package/lib/module/components/header/header-buttons.js +1 -1
  5. package/lib/module/components/header/header-buttons.js.map +1 -1
  6. package/lib/module/components/icon/Icon.js +2 -2
  7. package/lib/module/components/screen/Screen.js +87 -27
  8. package/lib/module/components/screen/Screen.js.map +1 -1
  9. package/lib/module/components/screen/ScreenContext.js +5 -1
  10. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  11. package/lib/module/components/screen/ScreenList.js +11 -3
  12. package/lib/module/components/screen/ScreenList.js.map +1 -1
  13. package/lib/module/components/screen/ScreenScrollView.js +11 -3
  14. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  15. package/lib/module/components/screen/hooks/use-nested-container-warning.js +39 -0
  16. package/lib/module/components/screen/hooks/use-nested-container-warning.js.map +1 -0
  17. package/lib/module/components/screen/parts/ScreenFrame.js +1 -0
  18. package/lib/module/components/screen/parts/ScreenFrame.js.map +1 -1
  19. package/lib/module/components/toast/Toast.js +1 -1
  20. package/lib/module/components/toast/Toast.js.map +1 -1
  21. package/lib/module/theme/defaults/icons.js +26 -19
  22. package/lib/module/theme/defaults/icons.js.map +1 -1
  23. package/lib/typescript/src/components/avatar/Avatar.d.ts.map +1 -1
  24. package/lib/typescript/src/components/icon/Icon.d.ts +2 -2
  25. package/lib/typescript/src/components/screen/Screen.d.ts +6 -0
  26. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  27. package/lib/typescript/src/components/screen/ScreenContext.d.ts +27 -1
  28. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  29. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  30. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  31. package/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts +24 -0
  32. package/lib/typescript/src/components/screen/hooks/use-nested-container-warning.d.ts.map +1 -0
  33. package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts.map +1 -1
  34. package/lib/typescript/src/theme/defaults/icons.d.ts.map +1 -1
  35. package/lib/typescript/src/theme/types.d.ts +1 -1
  36. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/components/avatar/Avatar.tsx +6 -1
  39. package/src/components/header/header-buttons.tsx +1 -1
  40. package/src/components/icon/Icon.tsx +2 -2
  41. package/src/components/screen/Screen.tsx +81 -17
  42. package/src/components/screen/ScreenContext.tsx +32 -0
  43. package/src/components/screen/ScreenList.tsx +9 -1
  44. package/src/components/screen/ScreenScrollView.tsx +9 -1
  45. package/src/components/screen/hooks/use-nested-container-warning.ts +40 -0
  46. package/src/components/screen/parts/ScreenFrame.tsx +8 -1
  47. package/src/components/toast/Toast.tsx +1 -1
  48. package/src/theme/defaults/icons.tsx +22 -15
  49. package/src/theme/types.ts +6 -5
@@ -37,6 +37,28 @@ export interface ScreenContextValue {
37
37
  * or a horizontal container needs.
38
38
  */
39
39
  contentOwnsBottomInset: boolean;
40
+ /**
41
+ * Whether the platform's own content-inset adjustment pays the vertical insets.
42
+ *
43
+ * True on an iOS screen with no `Screen.Header`: the navigation bar above such a
44
+ * screen is UIKit's, and only UIKit can inset a scroll view for one — a native
45
+ * large title resizes on every frame of its collapse, and it refuses to collapse
46
+ * at all for a scroll view that does not fill the screen. The frame therefore
47
+ * keeps no vertical edge, the container asks for
48
+ * `contentInsetAdjustmentBehavior: "automatic"` rather than `"never"`, and
49
+ * `contentOwnsBottomInset` stays false because UIKit pays that edge too.
50
+ */
51
+ platformAdjustsInsets: boolean;
52
+ /**
53
+ * Whether this screen *would* hand its vertical insets to the platform, if it
54
+ * found a vertical scroll container among its children.
55
+ *
56
+ * Everything `platformAdjustsInsets` needs except the container itself. The two
57
+ * differ only when the container is nested inside another component, where the
58
+ * root cannot see it — which is a mistake on such a screen, and the difference
59
+ * is how a mounting container detects that it was the one missed.
60
+ */
61
+ platformInsetsEligible: boolean;
40
62
  /** @deprecated Use contentPadding. */
41
63
  margins: ScreenContentPadding;
42
64
  /** Whether a `Screen.Header` is present. Used by scroll containers to add top inset. */
@@ -78,6 +100,10 @@ export interface ScreenScrollProviderProps {
78
100
  safeArea?: ScreenSafeArea;
79
101
  /** Whether the primary scroll container pads the bottom safe area. Default false. */
80
102
  contentOwnsBottomInset?: boolean;
103
+ /** Whether the platform adjusts the scroll container's vertical insets. Default false. */
104
+ platformAdjustsInsets?: boolean;
105
+ /** Whether the screen qualifies for that, container aside. Default false. */
106
+ platformInsetsEligible?: boolean;
81
107
  /** Whether a `Screen.Header` child is present. Default false. */
82
108
  hasHeader?: boolean;
83
109
 
@@ -95,6 +121,8 @@ export function ScreenScrollProvider({
95
121
  contentPadding = "all",
96
122
  safeArea = "auto",
97
123
  contentOwnsBottomInset = false,
124
+ platformAdjustsInsets = false,
125
+ platformInsetsEligible = false,
98
126
  hasHeader = false,
99
127
  }: ScreenScrollProviderProps) {
100
128
  const scrollY = useSharedValue(0);
@@ -123,6 +151,8 @@ export function ScreenScrollProvider({
123
151
  contentPadding,
124
152
  safeArea,
125
153
  contentOwnsBottomInset,
154
+ platformAdjustsInsets,
155
+ platformInsetsEligible,
126
156
  margins: contentPadding,
127
157
  hasHeader,
128
158
  scrollY,
@@ -137,6 +167,8 @@ export function ScreenScrollProvider({
137
167
  contentPadding,
138
168
  safeArea,
139
169
  contentOwnsBottomInset,
170
+ platformAdjustsInsets,
171
+ platformInsetsEligible,
140
172
  hasHeader,
141
173
  scrollY,
142
174
  headerCollapseHeight,
@@ -16,6 +16,7 @@ import { StyleSheet } from "react-native-unistyles";
16
16
 
17
17
  import { resolveScreenPadding } from "#theme/screen-padding";
18
18
  import { PortalHost } from "../portal";
19
+ import { useNestedContainerWarning } from "./hooks/use-nested-container-warning";
19
20
  import { useScreenRef } from "./hooks/use-screen-ref";
20
21
  import { useScreenScrollHandler } from "./hooks/use-screen-scroll-handler";
21
22
  import { ScreenBottomInset } from "./parts/ScreenBottomInset";
@@ -174,10 +175,12 @@ const ScreenListBase = forwardRef(function ScreenList(
174
175
  contentPadding: screenPadding,
175
176
  hasHeader,
176
177
  contentOwnsBottomInset,
178
+ platformAdjustsInsets,
177
179
  } = useScreen();
178
180
  // The root yields the bottom edge only to a vertical container, so a horizontal
179
181
  // one here is a secondary container and must not pay the inset a second time.
180
182
  const ownsBottomInset = contentOwnsBottomInset && !horizontal;
183
+ useNestedContainerWarning("Screen.List", horizontal);
181
184
  const mergedRef = useScreenRef(isDefaultFlatList ? scrollRef : undefined, ref);
182
185
  const resolvedPadding = contentPadding ?? margins ?? screenPadding;
183
186
  const resolvedLoading = isLoading ?? loading;
@@ -212,7 +215,12 @@ const ScreenListBase = forwardRef(function ScreenList(
212
215
  renderItem={renderItem}
213
216
  style={[{ backgroundColor: background }, style]}
214
217
  horizontal={horizontal}
215
- contentInsetAdjustmentBehavior="never"
218
+ /*
219
+ UIKit owns the vertical insets on a header-less iOS screen — see
220
+ `platformAdjustsInsets`. Everywhere else the frame has already paid them,
221
+ so a second adjustment here would count them twice.
222
+ */
223
+ contentInsetAdjustmentBehavior={platformAdjustsInsets ? "automatic" : "never"}
216
224
  automaticallyAdjustContentInsets={false}
217
225
  automaticallyAdjustsScrollIndicatorInsets={false}
218
226
  contentContainerStyle={[
@@ -6,6 +6,7 @@ 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";
11
12
  import { ScreenBottomInset } from "./parts/ScreenBottomInset";
@@ -72,10 +73,12 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
72
73
  hasHeader,
73
74
  background,
74
75
  contentOwnsBottomInset,
76
+ platformAdjustsInsets,
75
77
  } = useScreen();
76
78
  // The root yields the bottom edge only to a vertical container, so a horizontal
77
79
  // one here is a secondary container and must not pay the inset a second time.
78
80
  const ownsBottomInset = contentOwnsBottomInset && !horizontal;
81
+ useNestedContainerWarning("Screen.ScrollView", horizontal);
79
82
  const resolvedPadding = contentPadding ?? margins ?? screenPadding;
80
83
  const mergedRef = useScreenRef(scrollRef, ref);
81
84
  const placeholder = renderScreenPlaceholder({
@@ -91,7 +94,12 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
91
94
  ref={mergedRef}
92
95
  style={[{ backgroundColor: background }, style]}
93
96
  horizontal={horizontal}
94
- 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"}
95
103
  automaticallyAdjustContentInsets={false}
96
104
  automaticallyAdjustsScrollIndicatorInsets={false}
97
105
  showsVerticalScrollIndicator={false}
@@ -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
+ }
@@ -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>
@@ -300,7 +300,7 @@ const ToastClose = forwardRef<ComponentRef<typeof Button>, ToastCloseProps>(func
300
300
  >
301
301
  {children ?? (
302
302
  <Icon
303
- name="navigation.close"
303
+ name="terra.x"
304
304
  size={iconProps?.size ?? 20}
305
305
  color={iconProps?.color ?? colors.icon}
306
306
  strokeWidth={iconProps?.strokeWidth}
@@ -19,6 +19,11 @@ const strokeProps = (color: string, strokeWidth: number) => ({
19
19
  strokeLinejoin: "round" as const,
20
20
  });
21
21
 
22
+ // Every registry icon draws through this: one 24×24 viewBox rendered at exactly
23
+ // the requested `size`, so `<Icon size={24} />` is the same optical weight
24
+ // whichever name it resolves to. A glyph that needs to sit smaller than its box
25
+ // (Avatar's person fallback) asks for a smaller `size` — it must not shrink
26
+ // itself here, or it would scale differently from every other icon.
22
27
  const SvgIcon = ({ size, children }: { size: number; children: React.ReactNode }) => (
23
28
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
24
29
  {children}
@@ -36,25 +41,25 @@ const PathIcon = ({ d, ...props }: TerraIconProps & { d: string }) => {
36
41
  };
37
42
 
38
43
  // Lucide chevron geometry scaled 20% from the 24×24 viewBox center (45° arms unchanged).
39
- const NAV_CENTER = 12;
40
- const NAV_CHEVRON_OPEN = 3 * 1.2;
41
- const NAV_CHEVRON_ARM = 6 * 1.2;
44
+ const CHEVRON_CENTER = 12;
45
+ const CHEVRON_OPEN = 3 * 1.2;
46
+ const CHEVRON_ARM = 6 * 1.2;
42
47
 
43
- const BackIcon: TerraIconComponent = (props) => (
48
+ const ChevronLeftIcon: TerraIconComponent = (props) => (
44
49
  <PathIcon
45
- d={`M${NAV_CENTER + NAV_CHEVRON_OPEN} ${NAV_CENTER + NAV_CHEVRON_ARM}L${NAV_CENTER - NAV_CHEVRON_OPEN} ${NAV_CENTER} ${NAV_CENTER + NAV_CHEVRON_OPEN} ${NAV_CENTER - NAV_CHEVRON_ARM}`}
50
+ d={`M${CHEVRON_CENTER + CHEVRON_OPEN} ${CHEVRON_CENTER + CHEVRON_ARM}L${CHEVRON_CENTER - CHEVRON_OPEN} ${CHEVRON_CENTER} ${CHEVRON_CENTER + CHEVRON_OPEN} ${CHEVRON_CENTER - CHEVRON_ARM}`}
46
51
  {...props}
47
52
  />
48
53
  );
49
54
 
50
- const ForwardIcon: TerraIconComponent = (props) => (
55
+ const ChevronRightIcon: TerraIconComponent = (props) => (
51
56
  <PathIcon
52
- d={`M${NAV_CENTER - NAV_CHEVRON_OPEN} ${NAV_CENTER + NAV_CHEVRON_ARM}L${NAV_CENTER + NAV_CHEVRON_OPEN} ${NAV_CENTER} ${NAV_CENTER - NAV_CHEVRON_OPEN} ${NAV_CENTER - NAV_CHEVRON_ARM}`}
57
+ d={`M${CHEVRON_CENTER - CHEVRON_OPEN} ${CHEVRON_CENTER + CHEVRON_ARM}L${CHEVRON_CENTER + CHEVRON_OPEN} ${CHEVRON_CENTER} ${CHEVRON_CENTER - CHEVRON_OPEN} ${CHEVRON_CENTER - CHEVRON_ARM}`}
53
58
  {...props}
54
59
  />
55
60
  );
56
61
 
57
- const CloseIcon: TerraIconComponent = (props) => <PathIcon d="M18 6L6 18M6 6l12 12" {...props} />;
62
+ const XIcon: TerraIconComponent = (props) => <PathIcon d="M18 6L6 18M6 6l12 12" {...props} />;
58
63
 
59
64
  const InfoIcon: TerraIconComponent = (props) => {
60
65
  const { color, size, strokeWidth } = resolveIconProps(props);
@@ -107,26 +112,28 @@ const DangerIcon: TerraIconComponent = (props) => {
107
112
  );
108
113
  };
109
114
 
115
+ const CheckIcon: TerraIconComponent = (props) => <PathIcon d="M20 6 9 17l-5-5" {...props} />;
116
+
110
117
  export const PersonIcon: TerraIconComponent = (props) => {
111
118
  const { color, size, strokeWidth } = resolveIconProps(props);
112
119
  const stroke = strokeProps(color, strokeWidth);
113
- const iconSize = size * 0.55;
114
120
 
115
121
  return (
116
- <Svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none">
122
+ <SvgIcon size={size}>
117
123
  <Circle cx={12} cy={8} r={4} {...stroke} />
118
124
  <Path d="M4 20c0-4 3.582-7 8-7s8 3 8 7" {...stroke} />
119
- </Svg>
125
+ </SvgIcon>
120
126
  );
121
127
  };
122
128
 
123
129
  export const defaultIcons: Record<TerraSemanticIconName, TerraIconComponent> = {
124
- "navigation.back": BackIcon,
125
- "navigation.forward": ForwardIcon,
126
- "navigation.close": CloseIcon,
130
+ "terra.chevron-left": ChevronLeftIcon,
131
+ "terra.chevron-right": ChevronRightIcon,
132
+ "terra.x": XIcon,
133
+ "terra.check": CheckIcon,
134
+ "terra.person": PersonIcon,
127
135
  "status.info": InfoIcon,
128
136
  "status.success": SuccessIcon,
129
137
  "status.warning": WarningIcon,
130
138
  "status.danger": DangerIcon,
131
- person: PersonIcon,
132
139
  };
@@ -412,14 +412,15 @@ declare global {
412
412
  export interface TerraIconRegistry extends TerraUI.IconRegistry {}
413
413
 
414
414
  export type TerraSemanticIconName =
415
- | "navigation.back"
416
- | "navigation.forward"
417
- | "navigation.close"
415
+ | "terra.chevron-left"
416
+ | "terra.chevron-right"
417
+ | "terra.x"
418
+ | "terra.check"
419
+ | "terra.person"
418
420
  | "status.info"
419
421
  | "status.success"
420
422
  | "status.warning"
421
- | "status.danger"
422
- | "person";
423
+ | "status.danger";
423
424
 
424
425
  export type TerraConfiguredIconName = keyof TerraIconRegistry & string;
425
426