react-native-terra-ui 0.11.5 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/module/components/chip/Chip.js +25 -8
  3. package/lib/module/components/chip/Chip.js.map +1 -1
  4. package/lib/module/components/chip/utils.js +1 -1
  5. package/lib/module/components/chip/utils.js.map +1 -1
  6. package/lib/module/components/header/index.js.map +1 -1
  7. package/lib/module/components/header/parts/HeaderAccessory.js +58 -0
  8. package/lib/module/components/header/parts/HeaderAccessory.js.map +1 -0
  9. package/lib/module/components/header/parts/HeaderSurface.js +58 -0
  10. package/lib/module/components/header/parts/HeaderSurface.js.map +1 -0
  11. package/lib/module/components/header/types.js +2 -0
  12. package/lib/module/components/header/types.js.map +1 -0
  13. package/lib/module/components/header/variants/LargeTitleHeader/index.js +114 -46
  14. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  15. package/lib/module/components/header/variants/TitleHeader/index.js +33 -28
  16. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  17. package/lib/module/components/screen/ScreenContext.js +16 -1
  18. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  19. package/lib/module/components/screen/ScreenList.js +24 -5
  20. package/lib/module/components/screen/ScreenList.js.map +1 -1
  21. package/lib/module/components/screen/ScreenScrollView.js +10 -4
  22. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  23. package/lib/typescript/src/components/chip/Chip.d.ts +8 -0
  24. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -1
  25. package/lib/typescript/src/components/chip/utils.d.ts +6 -1
  26. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -1
  27. package/lib/typescript/src/components/header/index.d.ts +1 -0
  28. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts +19 -0
  30. package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts.map +1 -0
  31. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +22 -0
  32. package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -0
  33. package/lib/typescript/src/components/header/types.d.ts +15 -0
  34. package/lib/typescript/src/components/header/types.d.ts.map +1 -0
  35. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +8 -1
  36. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +14 -1
  38. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  39. package/lib/typescript/src/components/screen/ScreenContext.d.ts +22 -0
  40. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  41. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  42. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/components/chip/Chip.tsx +22 -9
  45. package/src/components/chip/utils.ts +7 -2
  46. package/src/components/header/index.ts +1 -0
  47. package/src/components/header/parts/HeaderAccessory.tsx +55 -0
  48. package/src/components/header/parts/HeaderSurface.tsx +62 -0
  49. package/src/components/header/types.ts +15 -0
  50. package/src/components/header/variants/LargeTitleHeader/index.tsx +114 -42
  51. package/src/components/header/variants/TitleHeader/index.tsx +45 -22
  52. package/src/components/screen/ScreenContext.tsx +42 -0
  53. package/src/components/screen/ScreenList.tsx +30 -5
  54. package/src/components/screen/ScreenScrollView.tsx +28 -6
@@ -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";
@@ -10,7 +10,9 @@ import { useHeaderBackground } from "../../../screen/parts/ScreenHeaderAppearanc
10
10
  import { ScreenContext } from "../../../screen/ScreenContext";
11
11
  import { Toolbar } from "../../../toolbar";
12
12
  import { HeaderDismissButton, type HeaderDismissProps } from "../../header-buttons";
13
- import { HEADER_FADE_EXTENT, HeaderGradientOverlay } from "../../parts/HeaderGradientOverlay";
13
+ import { HeaderAccessory } from "../../parts/HeaderAccessory";
14
+ import { HeaderSurface } from "../../parts/HeaderSurface";
15
+ import type { HeaderAccessoryPadding } from "../../types";
14
16
  import { isCustomTitle, isTextTitle } from "../../utils";
15
17
 
16
18
  export interface TitleHeaderProps extends HeaderDismissProps {
@@ -34,6 +36,18 @@ export interface TitleHeaderProps extends HeaderDismissProps {
34
36
  trailingActions?: ReactNode;
35
37
  /** @deprecated Use trailingActions. */
36
38
  RightToolbar?: ReactNode;
39
+ /**
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`.
43
+ */
44
+ accessory?: ReactNode;
45
+ /**
46
+ * Horizontal padding around `accessory`. Use `'none'` for content that should
47
+ * run to the screen edge, such as a horizontally scrolling tab strip.
48
+ * Defaults to `'x'`, matching the bar.
49
+ */
50
+ accessoryPadding?: HeaderAccessoryPadding;
37
51
  /** Title placement within the bar. Defaults to `'center'`. */
38
52
  titleAlignment?: "center" | "left";
39
53
  /** Color token for the top safe area and fading bar overlay. Defaults to the Screen background. */
@@ -65,6 +79,8 @@ export function TitleHeader({
65
79
  trailingActions,
66
80
  LeftToolbar,
67
81
  RightToolbar,
82
+ accessory,
83
+ accessoryPadding = "x",
68
84
  dismissAction = "none",
69
85
  navigation,
70
86
  onDismiss,
@@ -81,6 +97,12 @@ export function TitleHeader({
81
97
  useHeaderBackground(bgColor);
82
98
  const leadingContent = leadingActions !== undefined ? leadingActions : LeftToolbar;
83
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
+ );
84
106
  const isTransparent = bgColor === "transparent";
85
107
  const gradientColor = bgColor;
86
108
  styles.useVariants({ titleAlignment });
@@ -107,19 +129,26 @@ export function TitleHeader({
107
129
  return (
108
130
  <View style={styles.container}>
109
131
  <View style={styles.safeArea(isTransparent, bgColor, screen !== null)} />
110
- <View style={styles.bar}>
111
- <View style={styles.gradient} pointerEvents="none">
112
- <HeaderGradientOverlay color={gradientColor} />
113
- </View>
114
- {leading != null && <Toolbar>{leading}</Toolbar>}
115
- {/* Text can never be the touch target, but custom content may be, so its
116
- container passes touches through to it instead of swallowing them. */}
117
- <View style={styles.titleContainer} pointerEvents={isCustomTitle(title) ? "box-none" : "none"}>
118
- {isTextTitle(title)
119
- ? String(title).trim().length > 0 && <Text style={styles.title}>{title}</Text>
120
- : title}
132
+ {/* The fade covers the bar and any accessory as one surface, so content
133
+ scrolling under the header meets a single boundary rather than two. */}
134
+ <View style={styles.barGroup}>
135
+ <HeaderSurface color={gradientColor} />
136
+ <View style={styles.bar}>
137
+ {leading != null && <Toolbar>{leading}</Toolbar>}
138
+ {/* Text can never be the touch target, but custom content may be, so its
139
+ container passes touches through to it instead of swallowing them. */}
140
+ <View style={styles.titleContainer} pointerEvents={isCustomTitle(title) ? "box-none" : "none"}>
141
+ {isTextTitle(title)
142
+ ? String(title).trim().length > 0 && <Text style={styles.title}>{title}</Text>
143
+ : title}
144
+ </View>
145
+ {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
121
146
  </View>
122
- {trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
147
+ {accessory != null && (
148
+ <HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
149
+ {accessory}
150
+ </HeaderAccessory>
151
+ )}
123
152
  </View>
124
153
  </View>
125
154
  );
@@ -137,14 +166,8 @@ const styles = StyleSheet.create((theme, runtime) => ({
137
166
  height: withinScreen ? 0 : runtime.insets.top,
138
167
  backgroundColor: isTransparent ? "transparent" : bgColor,
139
168
  }),
140
- // Extend the fade below the bar without clipping content or intercepting touches.
141
- gradient: {
142
- position: "absolute",
143
- top: 0,
144
- left: 0,
145
- right: 0,
146
- bottom: -HEADER_FADE_EXTENT,
147
- },
169
+ // Anchors the absolutely positioned surface over the bar and the accessory both.
170
+ barGroup: { position: "relative" },
148
171
  bar: {
149
172
  height: theme.layout.header.height,
150
173
  flexDirection: "row",
@@ -63,6 +63,28 @@ export interface ScreenContextValue {
63
63
  margins: ScreenContentPadding;
64
64
  /** Whether a `Screen.Header` is present. Used by scroll containers to add top inset. */
65
65
  hasHeader: boolean;
66
+ /**
67
+ * Measured height (dp) of the pinned accessory a header renders below its bar
68
+ * — a search field, a tab strip. Zero when there is none.
69
+ *
70
+ * The theme header height covers the bar alone, and an accessory is whatever
71
+ * the caller put there, so scroll containers reserve this on top of it. That
72
+ * is why it is measured rather than declared: only the accessory knows how
73
+ * tall it is, and it can change with Dynamic Type or its own content.
74
+ */
75
+ headerAccessoryHeight: number;
76
+ /**
77
+ * Reports the measured accessory height. Called by the header that owns the
78
+ * accessory; zero releases the reserved space.
79
+ */
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;
66
88
  /** Vertical scroll offset, updated on the UI thread for header animations. */
67
89
  scrollY: SharedValue<number>;
68
90
  /**
@@ -126,6 +148,8 @@ export function ScreenScrollProvider({
126
148
  hasHeader = false,
127
149
  }: ScreenScrollProviderProps) {
128
150
  const scrollY = useSharedValue(0);
151
+ const [headerAccessoryHeight, setAccessoryHeight] = useState(0);
152
+ const [contentTopPadding, setTopPadding] = useState(0);
129
153
  const headerCollapseHeight = useSharedValue(0);
130
154
  const [headerSnapOffsets, setHeaderSnapOffsets] = useState<number[]>();
131
155
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
@@ -139,6 +163,16 @@ export function ScreenScrollProvider({
139
163
  [headerCollapseHeight]
140
164
  );
141
165
 
166
+ // Rounded so a subpixel layout measurement cannot loop the content padding
167
+ // against the layout that produced it.
168
+ const setHeaderAccessoryHeight = useCallback((height: number) => {
169
+ setAccessoryHeight(Math.max(0, Math.round(height)));
170
+ }, []);
171
+
172
+ const setContentTopPadding = useCallback((padding: number) => {
173
+ setTopPadding(Math.max(0, Math.round(padding)));
174
+ }, []);
175
+
142
176
  const scrollHandler = useAnimatedScrollHandler({
143
177
  onScroll: (event) => {
144
178
  scrollY.value = event.contentOffset.y;
@@ -155,6 +189,10 @@ export function ScreenScrollProvider({
155
189
  platformInsetsEligible,
156
190
  margins: contentPadding,
157
191
  hasHeader,
192
+ headerAccessoryHeight,
193
+ setHeaderAccessoryHeight,
194
+ contentTopPadding,
195
+ setContentTopPadding,
158
196
  scrollY,
159
197
  headerCollapseHeight,
160
198
  headerSnapOffsets,
@@ -170,6 +208,10 @@ export function ScreenScrollProvider({
170
208
  platformAdjustsInsets,
171
209
  platformInsetsEligible,
172
210
  hasHeader,
211
+ headerAccessoryHeight,
212
+ setHeaderAccessoryHeight,
213
+ contentTopPadding,
214
+ setContentTopPadding,
173
215
  scrollY,
174
216
  headerCollapseHeight,
175
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 { FlatList, type FlatListProps, type ListRenderItem, View } from "react-native";
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";
@@ -174,6 +181,8 @@ const ScreenListBase = forwardRef(function ScreenList(
174
181
  headerSnapOffsets,
175
182
  contentPadding: screenPadding,
176
183
  hasHeader,
184
+ headerAccessoryHeight,
185
+ setContentTopPadding,
177
186
  contentOwnsBottomInset,
178
187
  platformAdjustsInsets,
179
188
  } = useScreen();
@@ -183,6 +192,18 @@ const ScreenListBase = forwardRef(function ScreenList(
183
192
  useNestedContainerWarning("Screen.List", horizontal);
184
193
  const mergedRef = useScreenRef(isDefaultFlatList ? scrollRef : undefined, ref);
185
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]);
186
207
  const resolvedLoading = isLoading ?? loading;
187
208
  const placeholder = renderScreenPlaceholder({
188
209
  isLoading: resolvedLoading,
@@ -224,7 +245,7 @@ const ScreenListBase = forwardRef(function ScreenList(
224
245
  automaticallyAdjustContentInsets={false}
225
246
  automaticallyAdjustsScrollIndicatorInsets={false}
226
247
  contentContainerStyle={[
227
- styles.content(resolvedPadding, hasHeader, bottomInset, extraBottomPadding),
248
+ styles.content(resolvedPadding, hasHeader, bottomInset, extraBottomPadding, headerAccessoryHeight),
228
249
  contentContainerStyle,
229
250
  ]}
230
251
  ListHeaderComponent={header}
@@ -265,12 +286,16 @@ const styles = StyleSheet.create((theme, runtime) => ({
265
286
  padding: ScreenContentPadding,
266
287
  hasHeader: boolean,
267
288
  bottomInset: number | undefined,
268
- extraBottomPadding: number
289
+ extraBottomPadding: number,
290
+ accessoryHeight: number
269
291
  ) => {
270
292
  const { x, y } = resolveScreenPadding(theme, runtime.breakpoint);
271
293
  return {
272
294
  flexGrow: 1,
273
- paddingTop: (hasHeader ? theme.layout.header.height : 0) + (hasVerticalPadding(padding) ? y : 0),
295
+ // The theme height covers the bar; an accessory pinned under it is extra.
296
+ paddingTop:
297
+ (hasHeader ? theme.layout.header.height + accessoryHeight : 0) +
298
+ (hasVerticalPadding(padding) ? y : 0),
274
299
  paddingHorizontal: hasHorizontalPadding(padding) ? x : 0,
275
300
  paddingBottom: (bottomInset ?? (hasVerticalPadding(padding) ? y : 0)) + extraBottomPadding,
276
301
  };
@@ -1,5 +1,12 @@
1
- import { Children, type ComponentRef, forwardRef, type ReactNode } from "react";
2
- import { type ScrollView, type ScrollViewProps, type StyleProp, View, type ViewStyle } from "react-native";
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";
@@ -71,6 +78,8 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
71
78
  scrollHandler,
72
79
  contentPadding: screenPadding,
73
80
  hasHeader,
81
+ headerAccessoryHeight,
82
+ setContentTopPadding,
74
83
  background,
75
84
  contentOwnsBottomInset,
76
85
  platformAdjustsInsets,
@@ -88,6 +97,11 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
88
97
  isEmpty: isEmpty ?? Children.toArray(children).length === 0,
89
98
  });
90
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
+ );
91
105
 
92
106
  return (
93
107
  <Animated.ScrollView
@@ -105,13 +119,13 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
105
119
  showsVerticalScrollIndicator={false}
106
120
  scrollEventThrottle={16}
107
121
  contentContainerStyle={[
108
- styles.scrollContent(hasHeader, resolvedPadding, extraBottomPadding),
122
+ styles.scrollContent(hasHeader, resolvedPadding, extraBottomPadding, headerAccessoryHeight),
109
123
  contentContainerStyle,
110
124
  ]}
111
125
  {...rest}
112
126
  onScroll={composedScrollHandler}
113
127
  >
114
- <View style={styles.headerWrapper(resolvedPadding)}>
128
+ <View style={styles.headerWrapper(resolvedPadding)} onLayout={handleHeaderWrapperLayout}>
115
129
  <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
116
130
  </View>
117
131
  <View style={[styles.content(horizontal), contentStyle]}>{placeholder ?? children}</View>
@@ -124,11 +138,19 @@ export const ScreenScrollView = forwardRef<ComponentRef<typeof ScrollView>, Scre
124
138
  ScreenScrollView.displayName = "Screen.ScrollView";
125
139
 
126
140
  const styles = StyleSheet.create((theme, runtime) => ({
127
- scrollContent: (hasHeader: boolean, padding: ScreenContentPadding, extraBottomPadding: number) => {
141
+ scrollContent: (
142
+ hasHeader: boolean,
143
+ padding: ScreenContentPadding,
144
+ extraBottomPadding: number,
145
+ accessoryHeight: number
146
+ ) => {
128
147
  const { x, y } = resolveScreenPadding(theme, runtime.breakpoint);
129
148
  return {
130
149
  flexGrow: 1,
131
- paddingTop: (hasHeader ? theme.layout.header.height : 0) + (hasVerticalPadding(padding) ? y : 0),
150
+ // The theme height covers the bar; an accessory pinned under it is extra.
151
+ paddingTop:
152
+ (hasHeader ? theme.layout.header.height + accessoryHeight : 0) +
153
+ (hasVerticalPadding(padding) ? y : 0),
132
154
  paddingHorizontal: hasHorizontalPadding(padding) ? x : 0,
133
155
  paddingBottom: (hasVerticalPadding(padding) ? y : 0) + extraBottomPadding,
134
156
  };