@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1593888345 → 16.0.0-alpha.1701179921

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 (48) hide show
  1. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  3. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  5. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  6. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
  7. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  8. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  9. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +4 -2
  10. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  11. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +5 -2
  12. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +4 -1
  13. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -73
  14. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +65 -56
  15. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -65
  16. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +93 -46
  17. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +148 -32
  18. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  19. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  20. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  21. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +132 -6
  22. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +202 -13
  23. package/Components/ModalComponent/BottomSheetModalContent.tsx +187 -73
  24. package/Components/ModalComponent/SortableList.tsx +36 -22
  25. package/Components/ModalComponent/TextInputContent.tsx +42 -7
  26. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +111 -6
  27. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +50 -1
  28. package/Components/ModalComponent/index.tsx +7 -9
  29. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +24 -0
  30. package/Components/ModalComponent/presets/selectableCell.tsx +1 -1
  31. package/Components/ModalComponent/showTextInput.ts +11 -2
  32. package/Components/PlayerContainer/PlayerContainer.tsx +8 -1
  33. package/Components/River/ComponentsMap/ComponentsMap.tsx +10 -12
  34. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
  35. package/Components/River/__tests__/componentsMap.test.js +8 -3
  36. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  37. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  38. package/Components/Screen/index.tsx +5 -0
  39. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  40. package/Components/Transitioner/Transitioner.tsx +2 -3
  41. package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
  42. package/Components/VideoLive/LiveImageManager.ts +8 -2
  43. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  44. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  45. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  46. package/assets/empty-Info.png +0 -0
  47. package/package.json +5 -5
  48. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -1,4 +1,11 @@
1
- import React, { useCallback, useEffect, useMemo, useRef } from "react";
1
+ import React, {
2
+ memo,
3
+ useCallback,
4
+ useDeferredValue,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ } from "react";
2
9
  import {
3
10
  ActivityIndicator,
4
11
  ScrollView,
@@ -26,7 +33,6 @@ import { SortableList } from "./SortableList";
26
33
  import {
27
34
  AudioPlayerButton,
28
35
  NowPlayingHeaderSection,
29
- VARIANT_WITH_ICON,
30
36
  } from "./AudioPlayer/Components";
31
37
  import { BottomSheetDragArea } from "./BottomSheetDragArea";
32
38
  import { ModalBlocksStyleContext } from "./ModalBlocksStyleContext";
@@ -48,6 +54,10 @@ const styles = StyleSheet.create({
48
54
  fontSize: 14,
49
55
  textAlign: "center",
50
56
  },
57
+ scrollView: {
58
+ flexShrink: 1,
59
+ },
60
+ someStyles: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 },
51
61
  });
52
62
 
53
63
  type ModalComponentProps = {
@@ -81,9 +91,12 @@ type ModalComponentProps = {
81
91
  contentComponent?: React.ComponentType<any>;
82
92
  contentComponentProps?: Record<string, any>;
83
93
  styleOverrides?: ModalBlocksConfig | Record<string, any>;
94
+ measurementPhase?: boolean;
95
+ /** Copy under Next Up when the queue has no upcoming items. */
96
+ emptyMessage?: string;
84
97
  };
85
98
 
86
- export function BottomSheetModalContent(props: ModalComponentProps) {
99
+ function _BottomSheetModalContent(props: ModalComponentProps) {
87
100
  const {
88
101
  items,
89
102
  currentRoute,
@@ -103,13 +116,22 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
103
116
  contentComponent: ContentComponent,
104
117
  contentComponentProps = {},
105
118
  styleOverrides: propStyleOverrides,
119
+ measurementPhase = false,
120
+ emptyMessage,
106
121
  } = props;
107
122
 
108
123
  const route = useRef(currentRoute);
109
124
  const theme = useTheme<BaseThemePropertiesMobile>();
125
+ const bottomInset = useSafeAreaInsets().bottom;
110
126
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
111
127
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
112
128
 
129
+ const calculatedcontentContainerStyle = useMemo(() => {
130
+ return {
131
+ paddingBottom: paddingBottom + bottomInset,
132
+ };
133
+ }, [paddingBottom, bottomInset]);
134
+
113
135
  const {
114
136
  items: fetchedItems,
115
137
  showCentralLoading,
@@ -131,17 +153,81 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
131
153
  [plugins, themePluginId]
132
154
  );
133
155
 
134
- const styleOverrides = useMemo(() => {
135
- if (propStyleOverrides) {
136
- return propStyleOverrides;
137
- }
138
-
156
+ const styleOverrides = useMemo<ModalBlocksConfig | null>(() => {
139
157
  const configuration =
140
158
  plugin?.configuration || (plugin as any)?.configuration_json;
141
159
 
142
- if (!configuration || !themePluginId) return null;
160
+ const parsed =
161
+ configuration && themePluginId
162
+ ? parseModalBlocksConfiguration(configuration, themePluginId)
163
+ : null;
164
+
165
+ if (!propStyleOverrides) {
166
+ return parsed;
167
+ }
168
+
169
+ // hydrate(QueueStyles) only emits sectionHeader after the plugin class
170
+ // includes it. Until then, Studio/local keys in configuration_json still
171
+ // apply via the parser.
172
+ const hydratedSection = propStyleOverrides.sectionHeader;
173
+
174
+ const hasTitleStyle = (style?: Record<string, any>) =>
175
+ Boolean(
176
+ style &&
177
+ (style.fontColor ||
178
+ style.fontSize !== undefined ||
179
+ style.lineHeight !== undefined ||
180
+ style.fontFamily ||
181
+ style.iosFontFamily ||
182
+ style.androidFontFamily ||
183
+ style.letterSpacing !== undefined ||
184
+ style.iosLetterSpacing !== undefined ||
185
+ style.androidLetterSpacing !== undefined ||
186
+ style.textTransform ||
187
+ style.alignment)
188
+ );
189
+
190
+ const hasActionStyle = (style?: Record<string, any>) =>
191
+ Boolean(
192
+ style &&
193
+ (style.backgroundColor ||
194
+ style.fontColor ||
195
+ style.fontSize !== undefined ||
196
+ style.lineHeight !== undefined ||
197
+ style.fontFamily ||
198
+ style.iosFontFamily ||
199
+ style.androidFontFamily ||
200
+ style.letterSpacing !== undefined ||
201
+ style.iosLetterSpacing !== undefined ||
202
+ style.androidLetterSpacing !== undefined ||
203
+ style.textTransform ||
204
+ style.borderRadius !== undefined ||
205
+ style.cornerRadius !== undefined ||
206
+ style.paddingTop !== undefined ||
207
+ style.paddingRight !== undefined ||
208
+ style.paddingBottom !== undefined ||
209
+ style.paddingLeft !== undefined)
210
+ );
143
211
 
144
- return parseModalBlocksConfiguration(configuration, themePluginId);
212
+ const actionNode = hydratedSection?.actionButtons;
213
+
214
+ // An empty `default: {}` is truthy and would hide fields set on the flat
215
+ // object or on the focused bucket.
216
+ const hasHydratedSection =
217
+ hasTitleStyle(hydratedSection?.title?.default) ||
218
+ hasTitleStyle(hydratedSection?.title) ||
219
+ hasActionStyle(actionNode?.default) ||
220
+ hasActionStyle(actionNode?.focused) ||
221
+ hasActionStyle(actionNode?.focus) ||
222
+ hasActionStyle(actionNode);
223
+
224
+ return {
225
+ ...parsed,
226
+ ...propStyleOverrides,
227
+ sectionHeader: hasHydratedSection
228
+ ? hydratedSection
229
+ : parsed?.sectionHeader,
230
+ };
145
231
  }, [propStyleOverrides, plugin, themePluginId]);
146
232
 
147
233
  const pluginLocalizations = useLocalizedStrings({
@@ -156,14 +242,37 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
156
242
  ? pluginLocalizations?.[`${normalizedPluginId}_header_title`]
157
243
  : undefined;
158
244
 
159
- const createPlaylistTitle =
160
- pluginLocalizations?.create_new_playlist_title || "Create New Playlist";
245
+ const nextUpTitle =
246
+ pluginLocalizations?.next_up_title ||
247
+ (normalizedPluginId
248
+ ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`] ||
249
+ pluginLocalizations?.[`${normalizedPluginId}_label_next_up`]
250
+ : undefined) ||
251
+ "Next Up";
252
+
253
+ const nowPlayingTitle =
254
+ pluginLocalizations?.now_playing_title ||
255
+ (normalizedPluginId
256
+ ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`] ||
257
+ pluginLocalizations?.[`${normalizedPluginId}_label_now_playing`]
258
+ : undefined) ||
259
+ "Now Playing";
260
+
261
+ const clearLabel =
262
+ pluginLocalizations?.clear_label ||
263
+ (normalizedPluginId
264
+ ? pluginLocalizations?.[`${normalizedPluginId}_label_clear`]
265
+ : undefined) ||
266
+ "Clear";
161
267
 
162
268
  const resolvedTitle = props.title || localizedHeaderTitle || title;
163
269
 
164
270
  const itemsToRender = viewModel ? fetchedItems : items;
165
271
  const operations = editableCollection?.operations || [];
166
- const isSortable = hasOperation(operations, OPERATIONS.REORDER);
272
+
273
+ const isSortable = useDeferredValue(
274
+ !measurementPhase && hasOperation(operations, OPERATIONS.REORDER)
275
+ );
167
276
 
168
277
  const cell = getCell(
169
278
  viewModel ? role : undefined,
@@ -264,11 +373,22 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
264
373
  ]
265
374
  );
266
375
 
267
- const bottomInset = useSafeAreaInsets().bottom;
376
+ const sortableListRenderItem = useCallback(
377
+ ({ item, index, renderHandle }) =>
378
+ cell.renderItem({
379
+ item,
380
+ index,
381
+ onPress: onItemPress,
382
+ context: renderContext,
383
+ renderHandle,
384
+ }),
385
+ [cell, onItemPress, renderContext]
386
+ );
268
387
 
269
388
  const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
270
389
  const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
271
390
  const hasAdd = hasOperation(operations, OPERATIONS.ADD);
391
+ const isQueueEmpty = hasNowPlaying && (itemsToRender?.length ?? 0) === 0;
272
392
 
273
393
  const handleDeleteCollection = useCallback(() => {
274
394
  editableCollection?.deleteCollection?.();
@@ -277,9 +397,9 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
277
397
  const handleCreatePlaylist = useCallback(() => {
278
398
  editableCollection?.add?.({
279
399
  id: "create_new_playlist",
280
- title: createPlaylistTitle,
400
+ title: "unknown title",
281
401
  });
282
- }, [editableCollection, createPlaylistTitle]);
402
+ }, [editableCollection]);
283
403
 
284
404
  const headers = useMemo(() => {
285
405
  const list: Array<{ index: number; component: () => React.ReactElement }> =
@@ -291,6 +411,11 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
291
411
  component: () => (
292
412
  <NowPlayingHeaderSection
293
413
  onClear={hasClearAll ? handleDeleteCollection : undefined}
414
+ nextUpTitle={nextUpTitle}
415
+ nowPlayingTitle={nowPlayingTitle}
416
+ isEmpty={isQueueEmpty}
417
+ emptyMessage={emptyMessage}
418
+ clearLabel={clearLabel}
294
419
  />
295
420
  ),
296
421
  });
@@ -303,11 +428,7 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
303
428
  <View
304
429
  style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }}
305
430
  >
306
- <AudioPlayerButton
307
- configuration={VARIANT_WITH_ICON}
308
- data={{ title: createPlaylistTitle }}
309
- onPress={handleCreatePlaylist}
310
- />
431
+ <AudioPlayerButton onPress={handleCreatePlaylist} />
311
432
  </View>
312
433
  ),
313
434
  });
@@ -318,11 +439,34 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
318
439
  hasNowPlaying,
319
440
  hasClearAll,
320
441
  handleDeleteCollection,
442
+ nextUpTitle,
443
+ nowPlayingTitle,
444
+ isQueueEmpty,
445
+ emptyMessage,
446
+ clearLabel,
321
447
  hasAdd,
322
448
  handleCreatePlaylist,
323
449
  ]);
324
450
 
325
- const renderBody = () => {
451
+ const _items = useMemo(() => {
452
+ return (itemsToRender || []).map((item, index) =>
453
+ cell.renderItem({
454
+ item,
455
+ index,
456
+ onPress: onItemPress,
457
+ context: renderContext,
458
+ })
459
+ );
460
+ }, [itemsToRender, cell, onItemPress, renderContext]);
461
+
462
+ const _styles = useMemo(() => {
463
+ return {
464
+ maxHeight: props.maxHeight,
465
+ paddingTop: paddingTop,
466
+ };
467
+ }, [props.maxHeight, paddingTop]);
468
+
469
+ const renderedBody = (() => {
326
470
  if (ContentComponent) {
327
471
  const shouldWrapInScrollView =
328
472
  !contentComponentProps?.disableScrollViewWrap &&
@@ -331,9 +475,8 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
331
475
  if (shouldWrapInScrollView) {
332
476
  return (
333
477
  <ScrollView
334
- contentContainerStyle={{
335
- paddingBottom: paddingBottom + bottomInset,
336
- }}
478
+ keyboardShouldPersistTaps="handled"
479
+ contentContainerStyle={calculatedcontentContainerStyle}
337
480
  >
338
481
  <ContentComponent
339
482
  {...contentComponentProps}
@@ -346,12 +489,7 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
346
489
  }
347
490
 
348
491
  return (
349
- <View
350
- style={{
351
- flexShrink: 1,
352
- paddingBottom: paddingBottom + bottomInset,
353
- }}
354
- >
492
+ <View style={[styles.scrollView, calculatedcontentContainerStyle]}>
355
493
  <ContentComponent
356
494
  {...contentComponentProps}
357
495
  dismiss={dismiss}
@@ -395,70 +533,44 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
395
533
  <SortableList
396
534
  sortableData={itemsToRender}
397
535
  headers={headers}
398
- scrollViewStyle={{
399
- flexShrink: 1,
400
- }}
401
- contentContainerStyle={{
402
- paddingBottom: paddingBottom + bottomInset,
403
- }}
536
+ scrollViewStyle={styles.scrollView}
537
+ contentContainerStyle={calculatedcontentContainerStyle}
404
538
  onDragEnd={handleDragEnd}
405
- renderItem={({ item, index, renderHandle }) =>
406
- cell.renderItem({
407
- item,
408
- index,
409
- onPress: onItemPress,
410
- context: renderContext,
411
- renderHandle,
412
- })
413
- }
539
+ renderItem={sortableListRenderItem}
414
540
  />
415
541
  );
416
542
  }
417
543
 
418
544
  return (
419
545
  <ScrollView
546
+ keyboardShouldPersistTaps="handled"
420
547
  overScrollMode="never"
421
548
  bounces={false}
422
- contentContainerStyle={{
423
- paddingBottom: paddingBottom + bottomInset,
424
- }}
549
+ contentContainerStyle={calculatedcontentContainerStyle}
425
550
  >
426
551
  {hasNowPlaying ? (
427
552
  <NowPlayingHeaderSection
428
553
  onClear={hasClearAll ? handleDeleteCollection : undefined}
554
+ nextUpTitle={nextUpTitle}
555
+ nowPlayingTitle={nowPlayingTitle}
556
+ isEmpty={isQueueEmpty}
557
+ emptyMessage={emptyMessage}
558
+ clearLabel={clearLabel}
429
559
  />
430
560
  ) : null}
431
561
  {hasAdd ? (
432
- <View
433
- style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }}
434
- >
435
- <AudioPlayerButton
436
- configuration={VARIANT_WITH_ICON}
437
- data={{ title: createPlaylistTitle }}
438
- onPress={handleCreatePlaylist}
439
- />
562
+ <View style={styles.someStyles}>
563
+ <AudioPlayerButton onPress={handleCreatePlaylist} />
440
564
  </View>
441
565
  ) : null}
442
- {(itemsToRender || []).map((item, index) =>
443
- cell.renderItem({
444
- item,
445
- index,
446
- onPress: onItemPress,
447
- context: renderContext,
448
- })
449
- )}
566
+ {_items}
450
567
  </ScrollView>
451
568
  );
452
- };
569
+ })();
453
570
 
454
571
  return (
455
572
  <ModalBlocksStyleContext.Provider value={styleOverrides as any}>
456
- <View
457
- style={{
458
- maxHeight: props.maxHeight,
459
- paddingTop: paddingTop,
460
- }}
461
- >
573
+ <View style={_styles}>
462
574
  <BottomSheetDragArea>
463
575
  <Header
464
576
  dismiss={dismiss}
@@ -467,8 +579,10 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
467
579
  title={resolvedTitle}
468
580
  />
469
581
  </BottomSheetDragArea>
470
- {renderBody()}
582
+ {renderedBody}
471
583
  </View>
472
584
  </ModalBlocksStyleContext.Provider>
473
585
  );
474
586
  }
587
+
588
+ export const BottomSheetModalContent = memo(_BottomSheetModalContent);
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from "react";
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import Sortable from "react-native-sortables";
3
3
  import { Text, View, ViewStyle } from "react-native";
4
4
  import Animated, { useAnimatedRef } from "react-native-reanimated";
@@ -65,6 +65,9 @@ export type SortableListProps = {
65
65
  }) => void;
66
66
  };
67
67
 
68
+ const defaultKeyExtractor = (item, index) =>
69
+ `sortable-item-${item?.id ?? item?.title ?? index}`;
70
+
68
71
  export const SortableList = ({
69
72
  scrollViewStyle,
70
73
  contentContainerStyle,
@@ -73,8 +76,7 @@ export const SortableList = ({
73
76
  columns = 1,
74
77
  rowGap = 0,
75
78
  columnGap = 0,
76
- keyExtractor = (item, index) =>
77
- `sortable-item-${item?.id ?? item?.title ?? index}`,
79
+ keyExtractor = defaultKeyExtractor,
78
80
  renderItem,
79
81
  sortableData = DEFAULT_SORTABLE_DATA,
80
82
  headers = DEFAULT_HEADERS,
@@ -101,13 +103,24 @@ export const SortableList = ({
101
103
  [onDragEnd]
102
104
  );
103
105
 
104
- return (
105
- <Animated.ScrollView
106
- ref={scrollableRef}
107
- style={scrollViewStyle}
108
- contentContainerStyle={contentContainerStyle}
109
- >
110
- {(headers || []).map((header, idx) => (
106
+ const _renderItem = useCallback(
107
+ ({ item, index }) => (
108
+ <View key={keyExtractor(item, index)} style={itemStyle}>
109
+ {renderItem({
110
+ item,
111
+ index,
112
+ renderHandle: (children) => (
113
+ <Sortable.Handle>{children}</Sortable.Handle>
114
+ ),
115
+ })}
116
+ </View>
117
+ ),
118
+ [itemStyle, keyExtractor, renderItem]
119
+ );
120
+
121
+ const renderHeader = useMemo(
122
+ () =>
123
+ (headers || []).map((header, idx) => (
111
124
  <View key={`static-header-${idx}`} style={headerStyle}>
112
125
  {header.title ? (
113
126
  <SortableListHeader title={header.title} />
@@ -115,8 +128,19 @@ export const SortableList = ({
115
128
  renderHeaderComponent(header.component)
116
129
  )}
117
130
  </View>
118
- ))}
131
+ )),
132
+ [headers, headerStyle]
133
+ );
134
+
135
+ return (
136
+ <Animated.ScrollView
137
+ ref={scrollableRef}
138
+ style={scrollViewStyle}
139
+ contentContainerStyle={contentContainerStyle}
140
+ >
141
+ {renderHeader}
119
142
  <Sortable.Grid
143
+ key={`sortable-grid-${localData.length}`}
120
144
  scrollableRef={scrollableRef}
121
145
  customHandle
122
146
  columns={columns}
@@ -125,17 +149,7 @@ export const SortableList = ({
125
149
  data={localData}
126
150
  keyExtractor={keyExtractor}
127
151
  onDragEnd={handleDragEnd}
128
- renderItem={({ item, index }) => (
129
- <View key={keyExtractor(item, index)} style={itemStyle}>
130
- {renderItem({
131
- item,
132
- index,
133
- renderHandle: (children) => (
134
- <Sortable.Handle>{children}</Sortable.Handle>
135
- ),
136
- })}
137
- </View>
138
- )}
152
+ renderItem={_renderItem}
139
153
  />
140
154
  </Animated.ScrollView>
141
155
  );
@@ -9,6 +9,7 @@ import {
9
9
  AudioPlayerButton,
10
10
  VARIANT_TEXT_ONLY,
11
11
  } from "./AudioPlayer/Components/Button";
12
+ import { logger } from "@applicaster/zapp-react-native-utils/logger";
12
13
 
13
14
  export type TextInputContentProps = {
14
15
  /** Injected by the sheet host; unused for layout (header is owned by ModalComponent). */
@@ -19,16 +20,25 @@ export type TextInputContentProps = {
19
20
  inputLabel?: string;
20
21
  defaultValue?: string;
21
22
  buttonLabel: string;
23
+ validationLabel?: string;
24
+ maxLength?: number;
22
25
  actions: ActionType[];
26
+ actionContext?: Record<string, any>;
23
27
  };
24
28
 
29
+ // Spacing mirrors the `component_style_*` keys in text input component.
25
30
  const styles = StyleSheet.create({
26
31
  container: {
27
- paddingHorizontal: 20,
28
- paddingBottom: 8,
32
+ backgroundColor: "transparent", // component_style_background_color
33
+ paddingLeft: 40, // component_style_padding_left
34
+ paddingRight: 40, // component_style_padding_right
35
+ paddingTop: 0, // component_style_padding_top
36
+ paddingBottom: 0, // component_style_padding_bottom
37
+ marginTop: 20, // component_style_margin_top
38
+ marginBottom: 20, // component_style_margin_bottom
29
39
  },
30
40
  inputWrap: {
31
- marginTop: 8,
41
+ // Gap down to the submit button; owned by the modal, not the reference.
32
42
  marginBottom: 24,
33
43
  },
34
44
  });
@@ -38,11 +48,30 @@ const styles = StyleSheet.create({
38
48
  * Standard dismiss header comes from BottomSheetModalContent / ModalHeader.
39
49
  */
40
50
  export function TextInputContent(props: TextInputContentProps) {
41
- const { inputLabel, defaultValue = "", buttonLabel, actions } = props;
51
+ const {
52
+ inputLabel,
53
+ defaultValue = "",
54
+ buttonLabel,
55
+ actions,
56
+ actionContext,
57
+ maxLength = 255,
58
+ validationLabel,
59
+ } = props;
42
60
 
43
61
  const [textValue, setTextValue] = useState(defaultValue);
44
62
  const [submitting, setSubmitting] = useState(false);
45
63
 
64
+ const validationMessage =
65
+ textValue.length > maxLength
66
+ ? validationLabel || "Too many characters"
67
+ : undefined;
68
+
69
+ if (!validationLabel) {
70
+ logger.warn(
71
+ "Modal/TextInputContent: Validation label (validationLabel) is not provided, using default message 'Too many characters'."
72
+ );
73
+ }
74
+
46
75
  const onSubmit = useCallback(async () => {
47
76
  if (submitting) {
48
77
  return;
@@ -68,12 +97,17 @@ export function TextInputContent(props: TextInputContentProps) {
68
97
  return item;
69
98
  });
70
99
 
71
- await actionExecutor.handleActions(updatedActions);
100
+ if (actionContext) {
101
+ await actionExecutor.handleActions(updatedActions, actionContext);
102
+ } else {
103
+ await actionExecutor.handleActions(updatedActions);
104
+ }
105
+
72
106
  await actionExecutor.handleAction({ type: "dismissBottomSheet" });
73
107
  } finally {
74
108
  setSubmitting(false);
75
109
  }
76
- }, [submitting, actions, textValue]);
110
+ }, [submitting, actions, textValue, actionContext]);
77
111
 
78
112
  return (
79
113
  <View style={styles.container}>
@@ -85,6 +119,7 @@ export function TextInputContent(props: TextInputContentProps) {
85
119
  label: inputLabel,
86
120
  }}
87
121
  value={textValue}
122
+ validationMessage={validationMessage}
88
123
  onChangeText={setTextValue}
89
124
  onClearPress={() => setTextValue("")}
90
125
  />
@@ -93,7 +128,7 @@ export function TextInputContent(props: TextInputContentProps) {
93
128
  configuration={VARIANT_TEXT_ONLY}
94
129
  data={{ title: buttonLabel }}
95
130
  onPress={onSubmit}
96
- disabled={submitting}
131
+ disabled={!!validationMessage || submitting}
97
132
  />
98
133
  </View>
99
134
  );