@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9225550512 → 16.0.0-alpha.9338540010

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 (111) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  7. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  8. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +64 -0
  9. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  10. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +18 -0
  11. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  12. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  13. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  14. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  15. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  16. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  17. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  18. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  19. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  20. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +14 -6
  21. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  22. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  23. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +1 -1
  24. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  25. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  26. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  27. package/Components/MasterCell/index.tsx +28 -4
  28. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  29. package/Components/MasterCell/utils/index.ts +18 -0
  30. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  31. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  32. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  33. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  34. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  35. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +87 -97
  36. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +87 -126
  37. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -83
  38. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +274 -124
  39. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  40. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  41. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  42. package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -3
  43. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +433 -0
  44. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +563 -0
  45. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  46. package/Components/ModalComponent/BottomSheetModalContent.tsx +440 -66
  47. package/Components/ModalComponent/Button/index.tsx +5 -3
  48. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  49. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  50. package/Components/ModalComponent/Header/index.tsx +20 -7
  51. package/Components/ModalComponent/Header/utils.ts +1 -1
  52. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  53. package/Components/ModalComponent/SortableList.tsx +156 -0
  54. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  55. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  56. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  57. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  58. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  59. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  60. package/Components/ModalComponent/__tests__/showTextInput.test.ts +120 -0
  61. package/Components/ModalComponent/index.tsx +18 -9
  62. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  63. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  64. package/Components/ModalComponent/presets/index.ts +27 -0
  65. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  66. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  67. package/Components/ModalComponent/presets/types.ts +35 -0
  68. package/Components/ModalComponent/showTextInput.ts +54 -0
  69. package/Components/ModalComponent/utils.ts +82 -21
  70. package/Components/OfflineHandler/__tests__/index.test.tsx +109 -0
  71. package/Components/OfflineHandler/hooks.ts +0 -12
  72. package/Components/OfflineHandler/index.tsx +42 -4
  73. package/Components/PlayerContainer/PlayerContainer.tsx +19 -8
  74. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  75. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
  76. package/Components/River/ComponentsMap/ComponentsMap.tsx +89 -60
  77. package/Components/River/River.tsx +4 -0
  78. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -75
  79. package/Components/River/__tests__/componentsMap.test.js +8 -3
  80. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  81. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  82. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  83. package/Components/Screen/index.tsx +5 -0
  84. package/Components/Screen/navigationHandler.ts +8 -8
  85. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  86. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  87. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  88. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  89. package/Components/Transitioner/AnimationManager.js +1 -1
  90. package/Components/Transitioner/Scene.tsx +4 -3
  91. package/Components/Transitioner/Transitioner.tsx +71 -33
  92. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  93. package/Components/VideoLive/LiveImageManager.ts +9 -3
  94. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  95. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  96. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  97. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  98. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  99. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  100. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  101. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  102. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  103. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  104. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  105. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  106. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  107. package/Hooks/useEntryActionsExpander.ts +63 -0
  108. package/assets/empty-Info.png +0 -0
  109. package/package.json +6 -5
  110. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -332
  111. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -1,16 +1,64 @@
1
- import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
- import { View, ScrollView } from "react-native";
1
+ import React, {
2
+ memo,
3
+ useCallback,
4
+ useDeferredValue,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ } from "react";
9
+ import {
10
+ ActivityIndicator,
11
+ ScrollView,
12
+ StyleSheet,
13
+ Text,
14
+ View,
15
+ } from "react-native";
3
16
  import { Button } from "./Button";
4
17
  import { ItemIconProps } from "./Button/ItemIcon";
5
18
  import { ItemProps } from "./Button/Item";
6
19
  import { ItemLabelProps } from "./Button/ItemLabel";
7
20
  import { getItemIconProps, getItemLabelProps, getItemProps } from "./utils";
8
21
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
9
-
22
+ import {
23
+ ContentViewModel,
24
+ OPERATIONS,
25
+ hasOperation,
26
+ useBottomSheetContent,
27
+ } from "@applicaster/zapp-react-native-utils/modalState";
28
+ import { getCell } from "./presets";
10
29
  import type { PluginConfiguration } from "./";
11
-
12
30
  import { ModalHeader as DefaultModalHeader } from "./Header";
13
31
  import { useSafeAreaInsets } from "react-native-safe-area-context";
32
+ import { SortableList } from "./SortableList";
33
+ import {
34
+ AudioPlayerButton,
35
+ NowPlayingHeaderSection,
36
+ } from "./AudioPlayer/Components";
37
+ import { BottomSheetDragArea } from "./BottomSheetDragArea";
38
+ import { ModalBlocksStyleContext } from "./ModalBlocksStyleContext";
39
+ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
40
+ import {
41
+ ModalBlocksConfig,
42
+ parseModalBlocksConfiguration,
43
+ } from "@applicaster/zapp-react-native-utils/configurationUtils/modalBlocksParser";
44
+ import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
45
+
46
+ const styles = StyleSheet.create({
47
+ centered: {
48
+ justifyContent: "center",
49
+ alignItems: "center",
50
+ padding: 16,
51
+ },
52
+ errorText: {
53
+ color: "#ff453a",
54
+ fontSize: 14,
55
+ textAlign: "center",
56
+ },
57
+ scrollView: {
58
+ flexShrink: 1,
59
+ },
60
+ someStyles: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 },
61
+ });
14
62
 
15
63
  type ModalComponentProps = {
16
64
  items: any[];
@@ -22,13 +70,7 @@ type ModalComponentProps = {
22
70
  title?: string;
23
71
  maxHeight?: number;
24
72
  dismiss: () => void;
25
- headerComponent?: React.ComponentType;
26
-
27
- scrollViewContent?: {
28
- renderItem: (item: any, index: number) => React.ReactNode;
29
- items: any[];
30
- } | null;
31
-
73
+ headerComponent?: React.ComponentType<any>;
32
74
  buttonComponent?: React.ComponentType;
33
75
  iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps);
34
76
  itemProps?:
@@ -37,16 +79,25 @@ type ModalComponentProps = {
37
79
  labelProps?:
38
80
  | ItemLabelProps
39
81
  | ((theme: PluginConfiguration, width: number) => ItemLabelProps);
40
- getSelectedItemIcon: (
82
+ getSelectedItemIcon?: (
41
83
  theme: PluginConfiguration
42
84
  ) => ItemIconProps["asset"] | null;
43
- getDefaultItemIcon: (
85
+ getDefaultItemIcon?: (
44
86
  theme: PluginConfiguration
45
87
  ) => ItemIconProps["asset"] | null;
46
88
  iconPlacement?: "left" | "right";
89
+ viewModel?: ContentViewModel;
90
+ /** Optional body that replaces the items list (header stays DefaultModalHeader). */
91
+ contentComponent?: React.ComponentType<any>;
92
+ contentComponentProps?: Record<string, any>;
93
+ themePluginId?: string;
94
+ styleOverrides?: ModalBlocksConfig | Record<string, any>;
95
+ measurementPhase?: boolean;
96
+ /** Copy under Next Up when the queue has no upcoming items. */
97
+ emptyMessage?: string;
47
98
  };
48
99
 
49
- export function BottomSheetModalContent(props: ModalComponentProps) {
100
+ function _BottomSheetModalContent(props: ModalComponentProps) {
50
101
  const {
51
102
  items,
52
103
  currentRoute,
@@ -55,23 +106,116 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
55
106
  dismiss,
56
107
  summary,
57
108
  title,
58
- headerComponent: ModalHeader = DefaultModalHeader,
59
-
60
- scrollViewContent = null,
61
-
109
+ headerComponent,
110
+ viewModel = null,
62
111
  buttonComponent: ButtonComponent = Button,
63
112
  getSelectedItemIcon = ({
64
113
  modal_bottom_sheet_item_selected_icon,
65
114
  }: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon,
66
115
  getDefaultItemIcon = () => null,
67
116
  iconPlacement,
117
+ contentComponent: ContentComponent,
118
+ contentComponentProps = {},
119
+ themePluginId: themePluginIdProp,
120
+ measurementPhase = false,
121
+ emptyMessage,
68
122
  } = props;
69
123
 
70
124
  const route = useRef(currentRoute);
71
125
  const theme = useTheme<BaseThemePropertiesMobile>();
126
+ const bottomInset = useSafeAreaInsets().bottom;
72
127
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
73
128
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
74
129
 
130
+ const calculatedcontentContainerStyle = useMemo(() => {
131
+ return {
132
+ paddingBottom: paddingBottom + bottomInset,
133
+ };
134
+ }, [paddingBottom, bottomInset]);
135
+
136
+ const {
137
+ items: fetchedItems,
138
+ showCentralLoading,
139
+ error,
140
+ handleItemPress,
141
+ role,
142
+ behavior,
143
+ editableCollection,
144
+ themePluginId: themePluginIdFromSheet,
145
+ } = useBottomSheetContent({
146
+ onPress,
147
+ viewModel,
148
+ });
149
+
150
+ const themePluginId = themePluginIdFromSheet || themePluginIdProp;
151
+
152
+ const plugins = usePlugins();
153
+
154
+ const plugin = useMemo(
155
+ () => plugins?.find((p) => p.identifier === themePluginId),
156
+ [plugins, themePluginId]
157
+ );
158
+
159
+ const styleOverrides = useMemo<ModalBlocksConfig | null>(() => {
160
+ const configuration =
161
+ plugin?.configuration || (plugin as any)?.configuration_json;
162
+
163
+ return configuration && themePluginId
164
+ ? parseModalBlocksConfiguration(configuration, themePluginId)
165
+ : null;
166
+ }, [plugin, themePluginId]);
167
+
168
+ const pluginLocalizations = useLocalizedStrings({
169
+ localizations:
170
+ plugin?.configuration?.localizations ||
171
+ (plugin as any)?.configuration_json?.localizations,
172
+ });
173
+
174
+ const normalizedPluginId = themePluginId?.replace(/-/g, "_");
175
+
176
+ const localizedHeaderTitle = normalizedPluginId
177
+ ? pluginLocalizations?.[`${normalizedPluginId}_header_title`]
178
+ : undefined;
179
+
180
+ const nextUpTitle =
181
+ pluginLocalizations?.next_up_title ||
182
+ (normalizedPluginId
183
+ ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`] ||
184
+ pluginLocalizations?.[`${normalizedPluginId}_label_next_up`]
185
+ : undefined) ||
186
+ "Next Up";
187
+
188
+ const nowPlayingTitle =
189
+ pluginLocalizations?.now_playing_title ||
190
+ (normalizedPluginId
191
+ ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`] ||
192
+ pluginLocalizations?.[`${normalizedPluginId}_label_now_playing`]
193
+ : undefined) ||
194
+ "Now Playing";
195
+
196
+ const clearLabel =
197
+ pluginLocalizations?.clear_label ||
198
+ (normalizedPluginId
199
+ ? pluginLocalizations?.[`${normalizedPluginId}_label_clear`]
200
+ : undefined) ||
201
+ "Clear";
202
+
203
+ const resolvedTitle = props.title || localizedHeaderTitle || title;
204
+
205
+ const itemsToRender = viewModel ? fetchedItems : items;
206
+ const operations = editableCollection?.operations || [];
207
+
208
+ const isSortable = useDeferredValue(
209
+ !measurementPhase && hasOperation(operations, OPERATIONS.REORDER)
210
+ );
211
+
212
+ const cell = getCell(
213
+ viewModel ? role : undefined,
214
+ viewModel ? behavior : undefined
215
+ );
216
+
217
+ const Header = headerComponent ?? DefaultModalHeader;
218
+
75
219
  useEffect(() => {
76
220
  if (currentRoute !== route.current) {
77
221
  props.dismiss();
@@ -83,14 +227,24 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
83
227
  }, [theme, props.iconProps]);
84
228
 
85
229
  const itemBaseProps = useMemo<ItemProps>(() => {
86
- return getItemProps(theme, props.width, props.itemProps);
87
- }, [theme, props.width, props.itemProps]);
230
+ return getItemProps(
231
+ theme,
232
+ props.width,
233
+ props.itemProps,
234
+ styleOverrides?.item
235
+ );
236
+ }, [theme, props.width, props.itemProps, styleOverrides?.item]);
88
237
 
89
238
  const labelBaseProps = useMemo<ItemLabelProps>(() => {
90
- return getItemLabelProps(theme, props.width, props.labelProps);
91
- }, [theme, props.width, props.labelProps]);
239
+ return getItemLabelProps(
240
+ theme,
241
+ props.width,
242
+ props.labelProps,
243
+ styleOverrides?.item
244
+ );
245
+ }, [theme, props.width, props.labelProps, styleOverrides?.item]);
92
246
 
93
- const handlePress = useCallback(
247
+ const handleStaticPress = useCallback(
94
248
  (item: any) => {
95
249
  onPress(item);
96
250
  dismiss();
@@ -98,52 +252,272 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
98
252
  [onPress, dismiss]
99
253
  );
100
254
 
101
- const bottomInset = useSafeAreaInsets().bottom;
255
+ const onItemPress = viewModel ? handleItemPress : handleStaticPress;
102
256
 
103
- return (
104
- <View
105
- style={{
106
- maxHeight: props.maxHeight,
107
- paddingTop: paddingTop,
108
- }}
109
- >
110
- <ModalHeader
111
- dismiss={dismiss}
112
- configuration={theme}
113
- summary={summary}
114
- title={title}
115
- />
257
+ const handleDragEnd = useCallback(
258
+ (params: any) => {
259
+ const from = params?.fromIndex ?? params?.from;
260
+ const to = params?.toIndex ?? params?.to;
261
+
262
+ const reorderedItemIds = Array.isArray(params?.data)
263
+ ? params.data
264
+ .filter((item: any) => !item?.isHeader)
265
+ .map((item: any) => item?.id)
266
+ .filter(Boolean)
267
+ : undefined;
268
+
269
+ if (
270
+ (reorderedItemIds?.length ||
271
+ (typeof from === "number" &&
272
+ typeof to === "number" &&
273
+ from !== to)) &&
274
+ editableCollection?.move
275
+ ) {
276
+ editableCollection.move(reorderedItemIds || []);
277
+ }
278
+ },
279
+ [editableCollection]
280
+ );
281
+
282
+ const renderContext = useMemo(
283
+ () => ({
284
+ theme,
285
+ ButtonComponent,
286
+ current_selection,
287
+ iconBaseProps,
288
+ itemBaseProps,
289
+ labelBaseProps,
290
+ getSelectedItemIcon,
291
+ getDefaultItemIcon,
292
+ iconPlacement,
293
+ editableCollection,
294
+ behavior,
295
+ }),
296
+ [
297
+ theme,
298
+ ButtonComponent,
299
+ current_selection,
300
+ iconBaseProps,
301
+ itemBaseProps,
302
+ labelBaseProps,
303
+ getSelectedItemIcon,
304
+ getDefaultItemIcon,
305
+ iconPlacement,
306
+ editableCollection,
307
+ behavior,
308
+ ]
309
+ );
310
+
311
+ const sortableListRenderItem = useCallback(
312
+ ({ item, index, renderHandle }) =>
313
+ cell.renderItem({
314
+ item,
315
+ index,
316
+ onPress: onItemPress,
317
+ context: renderContext,
318
+ renderHandle,
319
+ }),
320
+ [cell, onItemPress, renderContext]
321
+ );
322
+
323
+ const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
324
+ const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
325
+ const hasAdd = hasOperation(operations, OPERATIONS.ADD);
326
+ const isQueueEmpty = hasNowPlaying && (itemsToRender?.length ?? 0) === 0;
327
+
328
+ const handleDeleteCollection = useCallback(() => {
329
+ editableCollection?.deleteCollection?.();
330
+ }, [editableCollection]);
331
+
332
+ const handleCreatePlaylist = useCallback(() => {
333
+ editableCollection?.add?.({
334
+ id: "create_new_playlist",
335
+ title: "unknown title",
336
+ });
337
+ }, [editableCollection]);
338
+
339
+ const headers = useMemo(() => {
340
+ const list: Array<{ index: number; component: () => React.ReactElement }> =
341
+ [];
342
+
343
+ if (hasNowPlaying) {
344
+ list.push({
345
+ index: list.length,
346
+ component: () => (
347
+ <NowPlayingHeaderSection
348
+ onClear={hasClearAll ? handleDeleteCollection : undefined}
349
+ nextUpTitle={nextUpTitle}
350
+ nowPlayingTitle={nowPlayingTitle}
351
+ isEmpty={isQueueEmpty}
352
+ emptyMessage={emptyMessage}
353
+ clearLabel={clearLabel}
354
+ />
355
+ ),
356
+ });
357
+ }
358
+
359
+ if (hasAdd) {
360
+ list.push({
361
+ index: list.length,
362
+ component: () => (
363
+ <View
364
+ style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }}
365
+ >
366
+ <AudioPlayerButton onPress={handleCreatePlaylist} />
367
+ </View>
368
+ ),
369
+ });
370
+ }
371
+
372
+ return list.length > 0 ? list : undefined;
373
+ }, [
374
+ hasNowPlaying,
375
+ hasClearAll,
376
+ handleDeleteCollection,
377
+ nextUpTitle,
378
+ nowPlayingTitle,
379
+ isQueueEmpty,
380
+ emptyMessage,
381
+ clearLabel,
382
+ hasAdd,
383
+ handleCreatePlaylist,
384
+ ]);
385
+
386
+ const _items = useMemo(() => {
387
+ return (itemsToRender || []).map((item, index) =>
388
+ cell.renderItem({
389
+ item,
390
+ index,
391
+ onPress: onItemPress,
392
+ context: renderContext,
393
+ })
394
+ );
395
+ }, [itemsToRender, cell, onItemPress, renderContext]);
396
+
397
+ const _styles = useMemo(() => {
398
+ return {
399
+ maxHeight: props.maxHeight,
400
+ paddingTop: paddingTop,
401
+ };
402
+ }, [props.maxHeight, paddingTop]);
403
+
404
+ const renderedBody = (() => {
405
+ if (ContentComponent) {
406
+ const shouldWrapInScrollView =
407
+ !contentComponentProps?.disableScrollViewWrap &&
408
+ !(ContentComponent as any)?.disableScrollViewWrap;
409
+
410
+ if (shouldWrapInScrollView) {
411
+ return (
412
+ <ScrollView
413
+ keyboardShouldPersistTaps="handled"
414
+ contentContainerStyle={calculatedcontentContainerStyle}
415
+ >
416
+ <ContentComponent
417
+ {...contentComponentProps}
418
+ dismiss={dismiss}
419
+ width={props.width}
420
+ maxHeight={props.maxHeight}
421
+ />
422
+ </ScrollView>
423
+ );
424
+ }
425
+
426
+ return (
427
+ <View style={[styles.scrollView, calculatedcontentContainerStyle]}>
428
+ <ContentComponent
429
+ {...contentComponentProps}
430
+ dismiss={dismiss}
431
+ width={props.width}
432
+ maxHeight={props.maxHeight}
433
+ />
434
+ </View>
435
+ );
436
+ }
437
+
438
+ if (showCentralLoading) {
439
+ const loaderColor =
440
+ (theme as any)?.modal_bottom_sheet_activity_indicator_color ||
441
+ (theme as any)?.cell_title_color ||
442
+ "#ffffff";
443
+
444
+ return (
445
+ <View
446
+ style={[
447
+ styles.centered,
448
+ { backgroundColor: theme.modal_bottom_sheet_background_color },
449
+ ]}
450
+ >
451
+ <ActivityIndicator color={loaderColor} />
452
+ </View>
453
+ );
454
+ }
455
+
456
+ if (error) {
457
+ return (
458
+ <View style={styles.centered}>
459
+ <Text style={styles.errorText}>
460
+ {typeof error === "string" ? error : error.message}
461
+ </Text>
462
+ </View>
463
+ );
464
+ }
465
+
466
+ if (isSortable) {
467
+ return (
468
+ <SortableList
469
+ sortableData={itemsToRender}
470
+ headers={headers}
471
+ scrollViewStyle={styles.scrollView}
472
+ contentContainerStyle={calculatedcontentContainerStyle}
473
+ onDragEnd={handleDragEnd}
474
+ renderItem={sortableListRenderItem}
475
+ />
476
+ );
477
+ }
478
+
479
+ return (
116
480
  <ScrollView
117
- contentContainerStyle={{
118
- paddingBottom: paddingBottom + bottomInset,
119
- }}
481
+ keyboardShouldPersistTaps="handled"
482
+ overScrollMode="never"
483
+ bounces={false}
484
+ contentContainerStyle={calculatedcontentContainerStyle}
120
485
  >
121
- {scrollViewContent
122
- ? scrollViewContent.items.map((item, index) =>
123
- scrollViewContent.renderItem(item, index)
124
- )
125
- : null}
126
-
127
- {!scrollViewContent &&
128
- items.map((item, index) =>
129
- item ? (
130
- <ButtonComponent
131
- key={index}
132
- configuration={theme}
133
- selectedItem={current_selection}
134
- item={item}
135
- onPress={handlePress}
136
- label={theme[item?.label] ?? item?.label}
137
- iconBaseProps={iconBaseProps}
138
- itemBaseProps={itemBaseProps}
139
- labelBaseProps={labelBaseProps}
140
- selectedItemIcon={getSelectedItemIcon(theme)}
141
- defaultItemIcon={getDefaultItemIcon(theme)}
142
- iconPlacement={iconPlacement}
143
- />
144
- ) : null
145
- )}
486
+ {hasNowPlaying ? (
487
+ <NowPlayingHeaderSection
488
+ onClear={hasClearAll ? handleDeleteCollection : undefined}
489
+ nextUpTitle={nextUpTitle}
490
+ nowPlayingTitle={nowPlayingTitle}
491
+ isEmpty={isQueueEmpty}
492
+ emptyMessage={emptyMessage}
493
+ clearLabel={clearLabel}
494
+ />
495
+ ) : null}
496
+ {hasAdd ? (
497
+ <View style={styles.someStyles}>
498
+ <AudioPlayerButton onPress={handleCreatePlaylist} />
499
+ </View>
500
+ ) : null}
501
+ {_items}
146
502
  </ScrollView>
147
- </View>
503
+ );
504
+ })();
505
+
506
+ return (
507
+ <ModalBlocksStyleContext.Provider value={styleOverrides as any}>
508
+ <View style={_styles}>
509
+ <BottomSheetDragArea>
510
+ <Header
511
+ dismiss={dismiss}
512
+ configuration={theme}
513
+ summary={summary}
514
+ title={resolvedTitle}
515
+ />
516
+ </BottomSheetDragArea>
517
+ {renderedBody}
518
+ </View>
519
+ </ModalBlocksStyleContext.Provider>
148
520
  );
149
521
  }
522
+
523
+ export const BottomSheetModalContent = memo(_BottomSheetModalContent);
@@ -43,11 +43,13 @@ export function Button({
43
43
  }: ButtonProps) {
44
44
  const [focused, setFocused] = useState(false);
45
45
 
46
- const selected = selectedItem && item.value === selectedItem?.value;
46
+ const selected = selectedItem && item?.value === selectedItem?.value;
47
47
 
48
48
  const itemIconPropsAssets = useMemo<ItemIconProps["asset"]>(
49
- () => configuration[item.asset] ?? item.asset ?? defaultItemIcon,
50
- [item.asset, defaultItemIcon]
49
+ () =>
50
+ (item ? (configuration[item.asset] ?? item.asset) : undefined) ??
51
+ defaultItemIcon,
52
+ [item?.asset, defaultItemIcon]
51
53
  );
52
54
 
53
55
  const selectedItemIconPropsAssets = useMemo<ItemIconProps["asset"]>(
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { TouchableOpacity } from "react-native";
3
3
  import { QBImage } from "@applicaster/zapp-react-native-ui-components/Components";
4
+ import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
4
5
  // @ts-ignore
5
6
  import { defaultCloseAsset } from "./assets";
6
7
 
@@ -40,9 +41,20 @@ export function CloseButton(props: CloseButtonProps) {
40
41
 
41
42
  if (!enabled) return null;
42
43
 
44
+ // Expose the close button as its own accessible control so screen readers
45
+ // (e.g. iOS VoiceOver) can focus and activate it. The image-only button
46
+ // otherwise has no label/role, leaving it unreachable/unannounced. Uses the
47
+ // framework's localized "close" button config (label + hint + button role).
48
+ // Computed each render (not memoized) so runtime localization updates via
49
+ // AccessibilityManager.updateLocalizations() are picked up on re-render.
50
+ const accessibilityProps =
51
+ AccessibilityManager.getInstance().getButtonAccessibilityProps("close");
52
+
43
53
  return (
44
54
  <TouchableOpacity
45
55
  activeOpacity={1}
56
+ testID="closeButton"
57
+ {...accessibilityProps}
46
58
  style={{
47
59
  marginBottom,
48
60
  marginTop,
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react-native";
3
+ import { CloseButton, CloseButtonProps } from "../CloseButton";
4
+
5
+ jest.mock("@applicaster/zapp-react-native-ui-components/Components", () => {
6
+ const { Image } = require("react-native");
7
+
8
+ return { QBImage: Image };
9
+ });
10
+
11
+ const mockGetButtonA11yProps = jest.fn(() => ({
12
+ accessible: true,
13
+ accessibilityLabel: "Close",
14
+ accessibilityHint: "Closes the sheet",
15
+ accessibilityRole: "button",
16
+ }));
17
+
18
+ jest.mock(
19
+ "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager",
20
+ () => ({
21
+ AccessibilityManager: {
22
+ getInstance: () => ({
23
+ getButtonAccessibilityProps: mockGetButtonA11yProps,
24
+ }),
25
+ },
26
+ })
27
+ );
28
+
29
+ const baseProps: CloseButtonProps = {
30
+ enabled: true,
31
+ close: jest.fn(),
32
+ asset: "close.png",
33
+ assetFocused: "close_focused.png",
34
+ height: 24,
35
+ width: 24,
36
+ marginTop: 0,
37
+ marginBottom: 0,
38
+ marginLeft: 0,
39
+ marginRight: 0,
40
+ };
41
+
42
+ describe("ModalComponent Header CloseButton", () => {
43
+ beforeEach(() => {
44
+ jest.clearAllMocks();
45
+ });
46
+
47
+ it("renders as an accessible button reachable by screen readers", () => {
48
+ render(<CloseButton {...baseProps} />);
49
+
50
+ const button = screen.getByTestId("closeButton");
51
+
52
+ expect(button.props.accessible).toBe(true);
53
+ expect(button.props.accessibilityRole).toBe("button");
54
+ expect(button.props.accessibilityLabel).toBe("Close");
55
+ expect(mockGetButtonA11yProps).toHaveBeenCalledWith("close");
56
+ });
57
+
58
+ it("invokes close on press", () => {
59
+ const close = jest.fn();
60
+ render(<CloseButton {...baseProps} close={close} />);
61
+
62
+ fireEvent.press(screen.getByTestId("closeButton"));
63
+
64
+ expect(close).toHaveBeenCalledTimes(1);
65
+ });
66
+
67
+ it("renders nothing when disabled", () => {
68
+ render(<CloseButton {...baseProps} enabled={false} />);
69
+
70
+ expect(screen.queryByTestId("closeButton")).toBeNull();
71
+ });
72
+ });