react-native-terra-ui 0.4.0 → 0.5.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 (51) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/lib/module/components/header/parts/HeaderGradientOverlay.js +1 -1
  3. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -1
  4. package/lib/module/components/screen/Screen.js +3 -3
  5. package/lib/module/components/screen/Screen.js.map +1 -1
  6. package/lib/module/components/screen/ScreenFlatList.js +52 -31
  7. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  8. package/lib/module/components/screen/ScreenList.js +160 -0
  9. package/lib/module/components/screen/ScreenList.js.map +1 -0
  10. package/lib/module/components/screen/ScreenScrollView.js +26 -14
  11. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  12. package/lib/module/components/screen/index.js +1 -1
  13. package/lib/module/components/screen/index.js.map +1 -1
  14. package/lib/module/components/toast/Toast.js +2 -2
  15. package/lib/module/components/toolbar/Toolbar.js +3 -1
  16. package/lib/module/components/toolbar/Toolbar.js.map +1 -1
  17. package/lib/module/theme/tokens/primitives.js +11 -6
  18. package/lib/module/theme/tokens/primitives.js.map +1 -1
  19. package/lib/typescript/src/components/screen/Screen.d.ts +3 -2
  20. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  21. package/lib/typescript/src/components/screen/ScreenContext.d.ts +2 -2
  22. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +11 -0
  23. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  24. package/lib/typescript/src/components/screen/ScreenList.d.ts +65 -0
  25. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -0
  26. package/lib/typescript/src/components/screen/index.d.ts +2 -2
  27. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  28. package/lib/typescript/src/components/screen/types.d.ts +2 -3
  29. package/lib/typescript/src/components/screen/types.d.ts.map +1 -1
  30. package/lib/typescript/src/theme/tokens/primitives.d.ts +5 -0
  31. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  32. package/lib/typescript/src/theme/types.d.ts +1 -1
  33. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  34. package/package.json +2 -4
  35. package/src/components/header/parts/HeaderGradientOverlay.tsx +1 -1
  36. package/src/components/screen/Screen.tsx +5 -4
  37. package/src/components/screen/ScreenContext.tsx +2 -2
  38. package/src/components/screen/ScreenFlatList.tsx +52 -30
  39. package/src/components/screen/ScreenList.tsx +270 -0
  40. package/src/components/screen/ScreenScrollView.tsx +19 -10
  41. package/src/components/screen/index.ts +2 -2
  42. package/src/components/screen/types.ts +2 -3
  43. package/src/components/toast/Toast.tsx +2 -2
  44. package/src/components/toolbar/Toolbar.tsx +2 -0
  45. package/src/theme/tokens/primitives.ts +12 -6
  46. package/src/theme/types.ts +1 -1
  47. package/lib/module/components/screen/ScreenFlashList.js +0 -153
  48. package/lib/module/components/screen/ScreenFlashList.js.map +0 -1
  49. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts +0 -43
  50. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +0 -1
  51. package/src/components/screen/ScreenFlashList.tsx +0 -203
@@ -0,0 +1,270 @@
1
+ import {
2
+ type ComponentPropsWithoutRef,
3
+ type ComponentType,
4
+ type ElementType,
5
+ type ReactElement,
6
+ useMemo,
7
+ } from 'react';
8
+ import {
9
+ FlatList,
10
+ type NativeScrollEvent,
11
+ type NativeSyntheticEvent,
12
+ type StyleProp,
13
+ View,
14
+ type ViewStyle,
15
+ } from 'react-native';
16
+
17
+ import Animated, {
18
+ type AnimatedRef,
19
+ useAnimatedScrollHandler,
20
+ useComposedEventHandler,
21
+ } from 'react-native-reanimated';
22
+ import { StyleSheet } from 'react-native-unistyles';
23
+
24
+ import { resolveScreenMargin } from '#theme/screen-margin';
25
+ import { PortalHost } from '../portal';
26
+ import { BottomSafeArea } from './parts/BottomSafeArea';
27
+ import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
28
+ import { SCREEN_HEADER_PORTAL_HOST, useScreen } from './ScreenContext';
29
+ import type { ScreenMargins, ScreenScrollableProps } from './types';
30
+ import {
31
+ hasXMargin,
32
+ hasYMargin,
33
+ type ListHeaderComponent,
34
+ renderListHeader,
35
+ } from './utils';
36
+
37
+ export interface ScreenListRenderItemInfo<T> {
38
+ item: T;
39
+ index: number;
40
+ }
41
+
42
+ export type ScreenListProps<
43
+ T = unknown,
44
+ As extends ElementType = typeof FlatList,
45
+ > = ScreenScrollableProps &
46
+ Omit<
47
+ ComponentPropsWithoutRef<As>,
48
+ | 'data'
49
+ | 'renderItem'
50
+ | 'keyExtractor'
51
+ | 'style'
52
+ | 'contentContainerStyle'
53
+ | 'onScroll'
54
+ | 'ListHeaderComponent'
55
+ | 'ListFooterComponent'
56
+ | 'ListEmptyComponent'
57
+ | keyof ScreenScrollableProps
58
+ > & {
59
+ /**
60
+ * Which list component to render as. Defaults to React Native's
61
+ * `FlatList` — pass any list with a FlatList-shaped prop surface
62
+ * (`data`, `renderItem`, `ListHeaderComponent`, …), e.g. `FlashList`
63
+ * from `@shopify/flash-list`, or a custom virtualized list.
64
+ *
65
+ * Only the default `FlatList` binds the screen's shared imperative
66
+ * scroll ref — a custom `as` is responsible for its own ref if it needs
67
+ * one, since not every list exposes a Reanimated-compatible ref (e.g.
68
+ * `FlashList`'s ref exposes a different, non Reanimated-compatible
69
+ * imperative API).
70
+ */
71
+ as?: As;
72
+ data?: readonly T[] | null;
73
+ renderItem?: (info: ScreenListRenderItemInfo<T>) => ReactElement | null;
74
+ keyExtractor?: (item: T, index: number) => string;
75
+ ListHeaderComponent?: ListHeaderComponent;
76
+ ListFooterComponent?: ListHeaderComponent;
77
+ ListEmptyComponent?: ListHeaderComponent;
78
+ style?: StyleProp<ViewStyle>;
79
+ contentContainerStyle?: StyleProp<ViewStyle>;
80
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
81
+ /**
82
+ * Extra bottom padding on the list content — clearance for a tab bar or
83
+ * home indicator. Defaults to the `layout.screen.margin.y` token when
84
+ * margins are enabled, otherwise `0`.
85
+ */
86
+ bottomInset?: number;
87
+ };
88
+
89
+ /**
90
+ * Polymorphic list container — renders whichever list `as` names (defaults
91
+ * to `FlatList`), applying the same margin/header/safe-area/scroll-handler
92
+ * chrome regardless of which one it is. A screen can swap list
93
+ * implementations by changing `as`, without relearning the surrounding API.
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * <Screen.List data={items} renderItem={({ item }) => <Row item={item} />} />
98
+ * ```
99
+ *
100
+ * @example Any other list component
101
+ * ```tsx
102
+ * import { FlashList } from '@shopify/flash-list';
103
+ *
104
+ * <Screen.List
105
+ * as={FlashList}
106
+ * data={items}
107
+ * renderItem={({ item }) => <Row item={item} />}
108
+ * />
109
+ * ```
110
+ */
111
+ export function ScreenList<
112
+ T = unknown,
113
+ As extends ElementType = typeof FlatList,
114
+ >({
115
+ as,
116
+ data,
117
+ renderItem,
118
+ style,
119
+ contentContainerStyle,
120
+ ListHeaderComponent,
121
+ ListFooterComponent,
122
+ ListEmptyComponent,
123
+ horizontal = false,
124
+ loading = false,
125
+ LoadingComponent,
126
+ EmptyComponent,
127
+ margins,
128
+ bottomInset,
129
+ onScroll,
130
+ ...rest
131
+ }: ScreenListProps<T, As>) {
132
+ const isDefaultFlatList = as === undefined;
133
+ const Component = (as ?? FlatList) as ComponentType<Record<string, unknown>>;
134
+
135
+ // `Animated.createAnimatedComponent` should only run once per component,
136
+ // not on every render — `as` is arbitrary here (not a fixed module-scope
137
+ // reference like `Screen.FlatList` uses), so it's memoized on the resolved
138
+ // component instead of hoisted to module scope.
139
+ const AnimatedComponent = useMemo(
140
+ () => Animated.createAnimatedComponent(Component),
141
+ [Component]
142
+ );
143
+
144
+ const {
145
+ background,
146
+ scrollRef,
147
+ scrollHandler,
148
+ headerSnapOffsets,
149
+ margins: screenMargins,
150
+ hasHeader,
151
+ } = useScreen();
152
+
153
+ const resolvedMargins = margins ?? screenMargins;
154
+ // The placeholder rides in on `ListEmptyComponent` rather than replacing the
155
+ // list, so the header portal, scroll binding and snap offsets all stay put.
156
+ // While loading the list is fed an empty `data` so that slot is the one that
157
+ // renders.
158
+ const placeholder = renderScreenPlaceholder({
159
+ loading,
160
+ LoadingComponent,
161
+ EmptyComponent,
162
+ isEmpty: !data || data.length === 0,
163
+ });
164
+
165
+ const jsScrollHandler = useAnimatedScrollHandler(
166
+ (event) => {
167
+ if (onScroll)
168
+ onScroll(event as unknown as NativeSyntheticEvent<NativeScrollEvent>);
169
+ },
170
+ [onScroll]
171
+ );
172
+
173
+ const composedHandler = useComposedEventHandler([
174
+ scrollHandler,
175
+ jsScrollHandler,
176
+ ]);
177
+
178
+ // A caller naming its own `bottomInset` has taken the bottom over entirely,
179
+ // and a horizontal list has no bottom edge to clear.
180
+ const needsBottomSafeArea = bottomInset === undefined && !horizontal;
181
+
182
+ const footerComponent = (
183
+ <>
184
+ {renderListHeader(ListFooterComponent)}
185
+ {needsBottomSafeArea ? <BottomSafeArea /> : null}
186
+ </>
187
+ );
188
+
189
+ const headerComponent = (
190
+ <>
191
+ {/* `headerWrapper`'s negative margin cancels the list's inherited
192
+ content padding for the portaled header (`LargeTitlePortalContent`
193
+ etc. manage their own horizontal padding) — a caller's own
194
+ `ListHeaderComponent` has no such self-padding, so it stays outside
195
+ this wrapper and gets the list's normal item padding instead. */}
196
+ <View style={styles.headerWrapper(resolvedMargins)}>
197
+ <PortalHost name={SCREEN_HEADER_PORTAL_HOST} />
198
+ </View>
199
+ {renderListHeader(ListHeaderComponent)}
200
+ </>
201
+ );
202
+
203
+ return (
204
+ <AnimatedComponent
205
+ ref={
206
+ isDefaultFlatList
207
+ ? (scrollRef as unknown as AnimatedRef<never>)
208
+ : undefined
209
+ }
210
+ data={loading ? [] : data}
211
+ renderItem={renderItem}
212
+ style={[{ backgroundColor: background }, style]}
213
+ horizontal={horizontal}
214
+ contentInsetAdjustmentBehavior={hasHeader ? 'never' : undefined}
215
+ contentContainerStyle={[
216
+ styles.content(resolvedMargins, hasHeader, bottomInset),
217
+ contentContainerStyle,
218
+ ]}
219
+ ListHeaderComponent={headerComponent}
220
+ ListFooterComponent={footerComponent}
221
+ ListEmptyComponent={placeholder ?? ListEmptyComponent}
222
+ showsVerticalScrollIndicator={false}
223
+ scrollEventThrottle={16}
224
+ snapToOffsets={headerSnapOffsets}
225
+ {...rest}
226
+ onScroll={composedHandler}
227
+ />
228
+ );
229
+ }
230
+ ScreenList.displayName = 'Screen.List';
231
+
232
+ // `resolveScreenMargin` is called *inside* each dynamic function below, not
233
+ // hoisted to the factory's top level — the Babel plugin only attributes a
234
+ // runtime dependency (here, breakpoint) to a style key when the `rt`/
235
+ // `runtime` read it derives from sits inside that key's own value. Hoisting
236
+ // it would silently drop the dependency, so the margin would freeze at
237
+ // whatever breakpoint was active on mount and never update on rotation.
238
+ const styles = StyleSheet.create((theme, runtime) => ({
239
+ content: (
240
+ margins: ScreenMargins,
241
+ hasHeader: boolean,
242
+ bottomInset: number | undefined
243
+ ) => {
244
+ const { x: marginX, y: marginY } = resolveScreenMargin(
245
+ theme,
246
+ runtime.breakpoint
247
+ );
248
+ return {
249
+ flexGrow: 1,
250
+ // The header is absolutely positioned, so the list reserves its full
251
+ // height — bar plus top safe area. This assumes the scroll view does not
252
+ // also get UIKit's automatic inset adjustment: on a native tab screen
253
+ // that means `disableAutomaticContentInsets` on the tab, or the two
254
+ // insets are counted twice.
255
+ paddingTop:
256
+ (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
257
+ (hasYMargin(margins) ? marginY : 0),
258
+ paddingHorizontal: hasXMargin(margins) ? marginX : 0,
259
+ // The safe area is `BottomSafeArea`'s job; counting it here as well
260
+ // would leave a gap the height of the gesture bar.
261
+ paddingBottom: bottomInset ?? (hasYMargin(margins) ? marginY : 0),
262
+ };
263
+ },
264
+ headerWrapper: (margins: ScreenMargins) => {
265
+ const { x: marginX } = resolveScreenMargin(theme, runtime.breakpoint);
266
+ return {
267
+ marginHorizontal: hasXMargin(margins) ? -marginX : 0,
268
+ };
269
+ },
270
+ }));
@@ -172,11 +172,17 @@ export function ScreenScrollView({
172
172
  );
173
173
  }
174
174
 
175
- const styles = StyleSheet.create((theme, rt) => {
176
- const { x: marginX, y: marginY } = resolveScreenMargin(theme, rt.breakpoint);
177
-
178
- return {
179
- scrollContent: (hasHeader: boolean, margins: ScreenMargins) => ({
175
+ const styles = StyleSheet.create((theme, rt) => ({
176
+ // `resolveScreenMargin` is called *inside* each dynamic function, not
177
+ // hoisted above `return` — the Babel plugin only attributes a runtime
178
+ // dependency (here, breakpoint) to a style key when the `rt` read it
179
+ // derives from sits inside that key's own value. Hoisting it to the
180
+ // factory's top level silently drops the dependency, so the margin would
181
+ // freeze at whatever breakpoint was active on mount and never update on
182
+ // rotation.
183
+ scrollContent: (hasHeader: boolean, margins: ScreenMargins) => {
184
+ const { y: marginY } = resolveScreenMargin(theme, rt.breakpoint);
185
+ return {
180
186
  flexGrow: 1,
181
187
  // The header inset and the safe area are structural — they apply
182
188
  // whether or not margins are on; the y margin stacks on top of them.
@@ -186,12 +192,15 @@ const styles = StyleSheet.create((theme, rt) => {
186
192
  // The safe area is `BottomSafeArea`'s job; counting it here as well
187
193
  // would leave a gap the height of the gesture bar.
188
194
  paddingBottom: hasYMargin(margins) ? marginY : 0,
189
- }),
195
+ };
196
+ },
190
197
 
191
- content: (horizontal: boolean, margins: ScreenMargins) => ({
198
+ content: (horizontal: boolean, margins: ScreenMargins) => {
199
+ const { x: marginX } = resolveScreenMargin(theme, rt.breakpoint);
200
+ return {
192
201
  flexGrow: 1,
193
202
  paddingHorizontal: hasXMargin(margins) ? marginX : 0,
194
203
  flexDirection: horizontal ? 'row' : 'column',
195
- }),
196
- };
197
- });
204
+ };
205
+ },
206
+ }));
@@ -2,10 +2,10 @@ export type { ScreenHeaderProps, ScreenProps } from './Screen';
2
2
  export { Screen } from './Screen';
3
3
  export type { ScreenContextValue } from './ScreenContext';
4
4
  export { useScreen } from './ScreenContext';
5
- export type { ScreenFlashListProps } from './ScreenFlashList';
6
- export { ScreenFlashList } from './ScreenFlashList';
7
5
  export type { ScreenFlatListProps } from './ScreenFlatList';
8
6
  export { ScreenFlatList } from './ScreenFlatList';
7
+ export type { ScreenListProps } from './ScreenList';
8
+ export { ScreenList } from './ScreenList';
9
9
  export type { ScreenScrollViewProps } from './ScreenScrollView';
10
10
  export { ScreenScrollView } from './ScreenScrollView';
11
11
  export type { ScreenMargins, ScreenScrollableProps } from './types';
@@ -11,9 +11,8 @@ import type { ReactNode } from 'react';
11
11
  export type ScreenMargins = 'all' | 'x' | 'y' | 'none';
12
12
 
13
13
  /**
14
- * Props shared by `Screen.ScrollView`, `Screen.FlatList` and
15
- * `Screen.FlashList`, so a screen can swap one container for another without
16
- * relearning its API.
14
+ * Props shared by `Screen.ScrollView`, `Screen.FlatList` and `Screen.List`, so
15
+ * a screen can swap one container for another without relearning its API.
17
16
  */
18
17
  export interface ScreenScrollableProps {
19
18
  /** Lay content out along the x axis. Defaults to `false`. */
@@ -150,7 +150,7 @@ const ToastRoot = forwardRef<ComponentRef<typeof View>, ToastProps>(
150
150
  p="4"
151
151
  row
152
152
  align="center"
153
- gap="3"
153
+ gap="2"
154
154
  accessibilityRole={accessibilityRole}
155
155
  accessibilityLiveRegion="polite"
156
156
  borderWidth="hairline"
@@ -415,7 +415,7 @@ export function DefaultToast({
415
415
  {shouldRenderIcon ? (
416
416
  <Box alignSelf="start">{icon ?? <ToastIcon />}</Box>
417
417
  ) : null}
418
- <Box flex={1} gap="1">
418
+ <Box flex={1} gap="0">
419
419
  {label ? <ToastTitle>{label}</ToastTitle> : null}
420
420
  {description ? (
421
421
  <ToastDescription>{description}</ToastDescription>
@@ -310,6 +310,7 @@ const styles = StyleSheet.create((theme) => ({
310
310
  backgroundColor: resolveThemeColor(background, theme),
311
311
  borderWidth: StyleSheet.hairlineWidth,
312
312
  borderColor: theme.color['border.subtle'],
313
+ ...theme.elevation.xs,
313
314
  }),
314
315
  // Geometry is two finite axes, so it belongs in `variants` rather than in
315
316
  // conditionals: an icon-only cell is an exact square, a labelled one keeps the
@@ -333,6 +334,7 @@ const styles = StyleSheet.create((theme) => ({
333
334
  paddingHorizontal: 10,
334
335
  borderWidth: StyleSheet.hairlineWidth,
335
336
  borderColor: theme.color['border.subtle'],
337
+ ...theme.elevation.xs,
336
338
  },
337
339
  },
338
340
  },
@@ -326,20 +326,26 @@ export const primitives = {
326
326
  'elevation.none.shadowOffset.height': 0,
327
327
  'elevation.none.elevation': 0,
328
328
 
329
+ 'elevation.xs.shadowOpacity': 0.03,
330
+ 'elevation.xs.shadowRadius': 2,
331
+ 'elevation.xs.shadowOffset.width': 0,
332
+ 'elevation.xs.shadowOffset.height': 0.5,
333
+ 'elevation.xs.elevation': 1,
334
+
329
335
  'elevation.sm.shadowOpacity': 0.1,
330
- 'elevation.sm.shadowRadius': 2,
336
+ 'elevation.sm.shadowRadius': 4,
331
337
  'elevation.sm.shadowOffset.width': 0,
332
338
  'elevation.sm.shadowOffset.height': 1,
333
339
  'elevation.sm.elevation': 1,
334
340
 
335
341
  'elevation.md.shadowOpacity': 0.14,
336
- 'elevation.md.shadowRadius': 4,
342
+ 'elevation.md.shadowRadius': 6,
337
343
  'elevation.md.shadowOffset.width': 0,
338
344
  'elevation.md.shadowOffset.height': 2,
339
345
  'elevation.md.elevation': 3,
340
346
 
341
347
  'elevation.lg.shadowOpacity': 0.16,
342
- 'elevation.lg.shadowRadius': 8,
348
+ 'elevation.lg.shadowRadius': 12,
343
349
  'elevation.lg.shadowOffset.width': 0,
344
350
  'elevation.lg.shadowOffset.height': 4,
345
351
  'elevation.lg.elevation': 6,
@@ -460,11 +466,11 @@ export const primitives = {
460
466
  'layout.screen.margin.y.xs': 16,
461
467
  'layout.screen.margin.y.sm': 16,
462
468
  'layout.screen.margin.y.md': 24,
463
- 'layout.screen.margin.y.lg': 32,
464
- 'layout.screen.margin.y.xl': 48,
469
+ 'layout.screen.margin.y.lg': 24,
470
+ 'layout.screen.margin.y.xl': 24,
465
471
 
466
472
  // ── header layout ─────────────────────────────────────────────────────────
467
473
  // Header bar height (compact nav bar / collapsed large-title bar). 56dp aligns
468
474
  // to the 4dp spacing grid (= spacing.14) and a standard app-bar height.
469
- 'layout.header.height': 56,
475
+ 'layout.header.height': 54,
470
476
  };
@@ -66,7 +66,7 @@ export interface ElevationStyle {
66
66
  elevation: number;
67
67
  }
68
68
 
69
- export type ElevationKey = 'none' | 'sm' | 'md' | 'lg' | 'xl';
69
+ export type ElevationKey = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
70
70
 
71
71
  export type ElevationScale = Record<ElevationKey, ElevationStyle>;
72
72
 
@@ -1,153 +0,0 @@
1
- "use strict";
2
-
3
- import { View } from "react-native-unistyles/components/native/View";
4
- import { FlashList } from '@shopify/flash-list';
5
- import Animated, { useAnimatedScrollHandler, useComposedEventHandler } from 'react-native-reanimated';
6
- import { StyleSheet } from 'react-native-unistyles';
7
- import { resolveScreenMargin } from "../../theme/screen-margin.js";
8
- import { PortalHost } from "../portal/index.js";
9
- import { BottomSafeArea } from "./parts/BottomSafeArea.js";
10
- import { renderScreenPlaceholder } from "./parts/ScreenPlaceholder.js";
11
- import { SCREEN_HEADER_PORTAL_HOST, useScreen } from "./ScreenContext.js";
12
- import { hasXMargin, hasYMargin, renderListHeader } from "./utils.js";
13
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
14
- const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);
15
- /**
16
- * `FlashList` variant of `Screen.ScrollView`: mirrors scroll offset into the
17
- * screen's shared `scrollY`, hosts the portal target above the list, and
18
- * applies token-driven content padding.
19
- *
20
- * Requires `@shopify/flash-list` — it is a peer dependency of
21
- * `react-native-terra-ui` and must be installed in the consuming app.
22
- *
23
- * Note: unlike `Screen.FlatList`, this does not bind the screen's shared
24
- * imperative `scrollRef` — `FlashList`'s ref exposes a different (non
25
- * Reanimated-compatible) imperative API.
26
- *
27
- * Shares `loading` / `LoadingComponent` / `EmptyComponent` / `margins` /
28
- * `horizontal` with `Screen.ScrollView`, so a screen can swap one container for
29
- * another without relearning its API.
30
- *
31
- * @example
32
- * ```tsx
33
- * <Screen.FlashList data={items} renderItem={({ item }) => <Row item={item} />} />
34
- * ```
35
- *
36
- * @example Loading and empty placeholders
37
- * ```tsx
38
- * <Screen.FlashList
39
- * data={items}
40
- * loading={isLoading}
41
- * EmptyComponent={<EmptyState icon="status.info" title="Nothing yet" fill />}
42
- * renderItem={({ item }) => <Row item={item} />}
43
- * />
44
- * ```
45
- */
46
- export function ScreenFlashList({
47
- data,
48
- style,
49
- contentContainerStyle,
50
- ListHeaderComponent,
51
- ListFooterComponent,
52
- ListEmptyComponent,
53
- horizontal = false,
54
- loading = false,
55
- LoadingComponent,
56
- EmptyComponent,
57
- margins,
58
- bottomInset,
59
- onScroll,
60
- ...rest
61
- }) {
62
- if (FlashList == null) {
63
- throw new Error('[react-native-terra-ui] Screen.FlashList requires "@shopify/flash-list" to be installed in your app. Run `yarn add @shopify/flash-list` (or the npm/pnpm equivalent) and rebuild.');
64
- }
65
- const {
66
- background,
67
- scrollHandler,
68
- headerSnapOffsets,
69
- margins: screenMargins,
70
- hasHeader
71
- } = useScreen();
72
- const resolvedMargins = margins ?? screenMargins;
73
- // The placeholder rides in on `ListEmptyComponent` rather than replacing the
74
- // list, so the header portal and snap offsets stay put. While loading the
75
- // list is fed an empty `data` so that slot is the one that renders.
76
- const placeholder = renderScreenPlaceholder({
77
- loading,
78
- LoadingComponent,
79
- EmptyComponent,
80
- isEmpty: !data || data.length === 0
81
- });
82
- const jsScrollHandler = useAnimatedScrollHandler(event => {
83
- if (onScroll) onScroll(event);
84
- }, [onScroll]);
85
- const composedHandler = useComposedEventHandler([scrollHandler, jsScrollHandler]);
86
-
87
- // `AnimatedFlashList` is typed for `unknown` items, so the remaining props
88
- // are cast back to its prop type — minus the ones set explicitly below,
89
- // which the spread would otherwise re-supply.
90
- const listProps = rest;
91
-
92
- // A caller naming its own `bottomInset` has taken the bottom over entirely,
93
- // and a horizontal list has no bottom edge to clear.
94
- const needsBottomSafeArea = bottomInset === undefined && !horizontal;
95
- const footerComponent = /*#__PURE__*/_jsxs(_Fragment, {
96
- children: [renderListHeader(ListFooterComponent), needsBottomSafeArea ? /*#__PURE__*/_jsx(BottomSafeArea, {}) : null]
97
- });
98
- const headerComponent = /*#__PURE__*/_jsxs(View, {
99
- style: styles.headerWrapper(resolvedMargins),
100
- children: [/*#__PURE__*/_jsx(PortalHost, {
101
- name: SCREEN_HEADER_PORTAL_HOST
102
- }), renderListHeader(ListHeaderComponent)]
103
- });
104
- return /*#__PURE__*/_jsx(AnimatedFlashList, {
105
- data: loading ? [] : data,
106
- style: [{
107
- backgroundColor: background
108
- }, style],
109
- horizontal: horizontal,
110
- contentInsetAdjustmentBehavior: hasHeader ? 'never' : undefined,
111
- contentContainerStyle: [styles.content(resolvedMargins, hasHeader, bottomInset), contentContainerStyle],
112
- ListHeaderComponent: headerComponent,
113
- ListFooterComponent: footerComponent,
114
- ListEmptyComponent: placeholder ?? ListEmptyComponent,
115
- showsVerticalScrollIndicator: false,
116
- scrollEventThrottle: 16,
117
- snapToOffsets: headerSnapOffsets,
118
- ...listProps,
119
- onScroll: composedHandler
120
- });
121
- }
122
- const styles = StyleSheet.create((theme, runtime) => {
123
- const {
124
- x: marginX,
125
- y: marginY
126
- } = resolveScreenMargin(theme, runtime.breakpoint);
127
- return {
128
- content: (margins, hasHeader, bottomInset) => {
129
- return {
130
- flexGrow: 1,
131
- // The header is absolutely positioned, so the list reserves its full
132
- // height — bar plus top safe area. This assumes the scroll view does not
133
- // also get UIKit's automatic inset adjustment: on a native tab screen
134
- // that means `disableAutomaticContentInsets` on the tab, or the two
135
- // insets are counted twice.
136
- paddingTop: (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) + (hasYMargin(margins) ? marginY : 0),
137
- paddingHorizontal: hasXMargin(margins) ? marginX : 0,
138
- // The safe area is `BottomSafeArea`'s job; counting it here as well
139
- // would leave a gap the height of the gesture bar.
140
- paddingBottom: bottomInset ?? (hasYMargin(margins) ? marginY : 0),
141
- uni__dependencies: [0, 9]
142
- };
143
- },
144
- safeArea: {
145
- height: runtime.insets.top,
146
- uni__dependencies: [9]
147
- },
148
- headerWrapper: margins => ({
149
- marginHorizontal: hasXMargin(margins) ? -marginX : 0
150
- })
151
- };
152
- });
153
- //# sourceMappingURL=ScreenFlashList.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["FlashList","Animated","useAnimatedScrollHandler","useComposedEventHandler","StyleSheet","resolveScreenMargin","PortalHost","BottomSafeArea","renderScreenPlaceholder","SCREEN_HEADER_PORTAL_HOST","useScreen","hasXMargin","hasYMargin","renderListHeader","jsx","_jsx","Fragment","_Fragment","jsxs","_jsxs","AnimatedFlashList","createAnimatedComponent","ScreenFlashList","data","style","contentContainerStyle","ListHeaderComponent","ListFooterComponent","ListEmptyComponent","horizontal","loading","LoadingComponent","EmptyComponent","margins","bottomInset","onScroll","rest","Error","background","scrollHandler","headerSnapOffsets","screenMargins","hasHeader","resolvedMargins","placeholder","isEmpty","length","jsScrollHandler","event","composedHandler","listProps","needsBottomSafeArea","undefined","footerComponent","children","headerComponent","View","styles","headerWrapper","name","backgroundColor","contentInsetAdjustmentBehavior","content","showsVerticalScrollIndicator","scrollEventThrottle","snapToOffsets","create","theme","runtime","x","marginX","y","marginY","breakpoint","flexGrow","paddingTop","layout","header","height","insets","top","paddingHorizontal","paddingBottom","uni__dependencies","safeArea","marginHorizontal"],"sourceRoot":"../../../../src","sources":["components/screen/ScreenFlashList.tsx"],"mappings":"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","ignoreList":[]}
@@ -1,43 +0,0 @@
1
- import { type FlashListProps } from '@shopify/flash-list';
2
- import type { ScreenScrollableProps } from './types.js';
3
- export interface ScreenFlashListProps<T> extends Omit<FlashListProps<T>, 'horizontal'>, ScreenScrollableProps {
4
- /**
5
- * Extra bottom padding on the list content — clearance for a tab bar or home
6
- * indicator. Defaults to the `layout.screen.margin.y` token when margins are
7
- * enabled, otherwise `0`.
8
- */
9
- bottomInset?: number;
10
- }
11
- /**
12
- * `FlashList` variant of `Screen.ScrollView`: mirrors scroll offset into the
13
- * screen's shared `scrollY`, hosts the portal target above the list, and
14
- * applies token-driven content padding.
15
- *
16
- * Requires `@shopify/flash-list` — it is a peer dependency of
17
- * `react-native-terra-ui` and must be installed in the consuming app.
18
- *
19
- * Note: unlike `Screen.FlatList`, this does not bind the screen's shared
20
- * imperative `scrollRef` — `FlashList`'s ref exposes a different (non
21
- * Reanimated-compatible) imperative API.
22
- *
23
- * Shares `loading` / `LoadingComponent` / `EmptyComponent` / `margins` /
24
- * `horizontal` with `Screen.ScrollView`, so a screen can swap one container for
25
- * another without relearning its API.
26
- *
27
- * @example
28
- * ```tsx
29
- * <Screen.FlashList data={items} renderItem={({ item }) => <Row item={item} />} />
30
- * ```
31
- *
32
- * @example Loading and empty placeholders
33
- * ```tsx
34
- * <Screen.FlashList
35
- * data={items}
36
- * loading={isLoading}
37
- * EmptyComponent={<EmptyState icon="status.info" title="Nothing yet" fill />}
38
- * renderItem={({ item }) => <Row item={item} />}
39
- * />
40
- * ```
41
- */
42
- export declare function ScreenFlashList<T>({ data, style, contentContainerStyle, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, horizontal, loading, LoadingComponent, EmptyComponent, margins, bottomInset, onScroll, ...rest }: ScreenFlashListProps<T>): import("react").JSX.Element;
43
- //# sourceMappingURL=ScreenFlashList.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ScreenFlashList.d.ts","sourceRoot":"","sources":["../../../../../src/components/screen/ScreenFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAmBrE,OAAO,KAAK,EAAiB,qBAAqB,EAAE,MAAM,YAAS,CAAC;AAOpE,MAAM,WAAW,oBAAoB,CAAC,CAAC,CACrC,SAAQ,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,EAC3C,qBAAqB;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,EACjC,IAAI,EACJ,KAAK,EACL,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,UAAkB,EAClB,OAAe,EACf,gBAAgB,EAChB,cAAc,EACd,OAAO,EACP,WAAW,EACX,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,oBAAoB,CAAC,CAAC,CAAC,+BAkFzB"}