@applicaster/zapp-react-native-ui-components 16.0.0-rc.11 → 16.0.0-rc.111

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 (156) 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/CellRendererResolver/index.ts +2 -2
  7. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  8. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  9. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  10. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  11. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  26. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  31. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  34. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  35. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  36. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  37. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
  38. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  39. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  40. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  41. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  51. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  52. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  53. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  54. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  55. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  56. package/Components/MasterCell/dataAdapter.ts +15 -5
  57. package/Components/MasterCell/hooks/index.ts +2 -0
  58. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  59. package/Components/MasterCell/index.tsx +30 -5
  60. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  61. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  62. package/Components/MasterCell/utils/index.ts +85 -46
  63. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  64. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  65. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  66. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  67. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  68. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +772 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +728 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +1028 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  73. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  74. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  75. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  76. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +393 -0
  77. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +523 -0
  78. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  79. package/Components/ModalComponent/BottomSheetModalContent.tsx +436 -50
  80. package/Components/ModalComponent/Button/index.tsx +5 -3
  81. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  82. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  83. package/Components/ModalComponent/Header/index.tsx +20 -7
  84. package/Components/ModalComponent/Header/utils.ts +1 -1
  85. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  86. package/Components/ModalComponent/SortableList.tsx +156 -0
  87. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  88. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  89. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  90. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  91. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  92. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  93. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  94. package/Components/ModalComponent/index.tsx +18 -9
  95. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  96. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  97. package/Components/ModalComponent/presets/index.ts +27 -0
  98. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  99. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  100. package/Components/ModalComponent/presets/types.ts +35 -0
  101. package/Components/ModalComponent/showTextInput.ts +52 -0
  102. package/Components/ModalComponent/utils.ts +82 -21
  103. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  104. package/Components/OfflineHandler/__tests__/index.test.tsx +186 -17
  105. package/Components/OfflineHandler/hooks.ts +111 -0
  106. package/Components/OfflineHandler/index.tsx +91 -75
  107. package/Components/OfflineHandler/utils/index.ts +1 -13
  108. package/Components/PlayerContainer/PlayerContainer.tsx +33 -18
  109. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  110. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  111. package/Components/PreloaderWrapper/index.tsx +1 -1
  112. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -61
  113. package/Components/River/River.tsx +4 -0
  114. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -89
  115. package/Components/River/__tests__/componentsMap.test.js +8 -3
  116. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  117. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  118. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  119. package/Components/Screen/index.tsx +13 -4
  120. package/Components/Screen/navigationHandler.ts +8 -8
  121. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  122. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  123. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  124. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  125. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  126. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  127. package/Components/TopCutoffOverlay/index.tsx +2 -2
  128. package/Components/Transitioner/AnimationManager.js +1 -1
  129. package/Components/Transitioner/Scene.tsx +4 -3
  130. package/Components/Transitioner/Transitioner.tsx +98 -49
  131. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  132. package/Components/VideoLive/LiveImageManager.ts +9 -3
  133. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  134. package/Components/VideoModal/utils.ts +6 -1
  135. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  136. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  137. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  138. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  139. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  140. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  141. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  142. package/Helpers/index.js +0 -39
  143. package/Hooks/useEntryActionsExpander.ts +63 -0
  144. package/assets/empty-Info.png +0 -0
  145. package/package.json +6 -5
  146. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  147. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  148. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  149. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  150. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  151. package/Components/Screen/utils.ts +0 -16
  152. package/Helpers/Analytics/index.js +0 -95
  153. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  154. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  155. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  156. /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,7 +70,7 @@ type ModalComponentProps = {
22
70
  title?: string;
23
71
  maxHeight?: number;
24
72
  dismiss: () => void;
25
- headerComponent?: React.ComponentType;
73
+ headerComponent?: React.ComponentType<any>;
26
74
  buttonComponent?: React.ComponentType;
27
75
  iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps);
28
76
  itemProps?:
@@ -31,16 +79,24 @@ type ModalComponentProps = {
31
79
  labelProps?:
32
80
  | ItemLabelProps
33
81
  | ((theme: PluginConfiguration, width: number) => ItemLabelProps);
34
- getSelectedItemIcon: (
82
+ getSelectedItemIcon?: (
35
83
  theme: PluginConfiguration
36
84
  ) => ItemIconProps["asset"] | null;
37
- getDefaultItemIcon: (
85
+ getDefaultItemIcon?: (
38
86
  theme: PluginConfiguration
39
87
  ) => ItemIconProps["asset"] | null;
40
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
+ styleOverrides?: ModalBlocksConfig | Record<string, any>;
94
+ measurementPhase?: boolean;
95
+ /** Copy under Next Up when the queue has no upcoming items. */
96
+ emptyMessage?: string;
41
97
  };
42
98
 
43
- export function BottomSheetModalContent(props: ModalComponentProps) {
99
+ function _BottomSheetModalContent(props: ModalComponentProps) {
44
100
  const {
45
101
  items,
46
102
  currentRoute,
@@ -49,20 +105,113 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
49
105
  dismiss,
50
106
  summary,
51
107
  title,
52
- headerComponent: ModalHeader = DefaultModalHeader,
108
+ headerComponent,
109
+ viewModel = null,
53
110
  buttonComponent: ButtonComponent = Button,
54
111
  getSelectedItemIcon = ({
55
112
  modal_bottom_sheet_item_selected_icon,
56
113
  }: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon,
57
114
  getDefaultItemIcon = () => null,
58
115
  iconPlacement,
116
+ contentComponent: ContentComponent,
117
+ contentComponentProps = {},
118
+ measurementPhase = false,
119
+ emptyMessage,
59
120
  } = props;
60
121
 
61
122
  const route = useRef(currentRoute);
62
123
  const theme = useTheme<BaseThemePropertiesMobile>();
124
+ const bottomInset = useSafeAreaInsets().bottom;
63
125
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
64
126
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
65
127
 
128
+ const calculatedcontentContainerStyle = useMemo(() => {
129
+ return {
130
+ paddingBottom: paddingBottom + bottomInset,
131
+ };
132
+ }, [paddingBottom, bottomInset]);
133
+
134
+ const {
135
+ items: fetchedItems,
136
+ showCentralLoading,
137
+ error,
138
+ handleItemPress,
139
+ role,
140
+ behavior,
141
+ editableCollection,
142
+ themePluginId,
143
+ } = useBottomSheetContent({
144
+ onPress,
145
+ viewModel,
146
+ });
147
+
148
+ const plugins = usePlugins();
149
+
150
+ const plugin = useMemo(
151
+ () => plugins?.find((p) => p.identifier === themePluginId),
152
+ [plugins, themePluginId]
153
+ );
154
+
155
+ const styleOverrides = useMemo<ModalBlocksConfig | null>(() => {
156
+ const configuration =
157
+ plugin?.configuration || (plugin as any)?.configuration_json;
158
+
159
+ return configuration && themePluginId
160
+ ? parseModalBlocksConfiguration(configuration, themePluginId)
161
+ : null;
162
+ }, [plugin, themePluginId]);
163
+
164
+ const pluginLocalizations = useLocalizedStrings({
165
+ localizations:
166
+ plugin?.configuration?.localizations ||
167
+ (plugin as any)?.configuration_json?.localizations,
168
+ });
169
+
170
+ const normalizedPluginId = themePluginId?.replace(/-/g, "_");
171
+
172
+ const localizedHeaderTitle = normalizedPluginId
173
+ ? pluginLocalizations?.[`${normalizedPluginId}_header_title`]
174
+ : undefined;
175
+
176
+ const nextUpTitle =
177
+ pluginLocalizations?.next_up_title ||
178
+ (normalizedPluginId
179
+ ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`] ||
180
+ pluginLocalizations?.[`${normalizedPluginId}_label_next_up`]
181
+ : undefined) ||
182
+ "Next Up";
183
+
184
+ const nowPlayingTitle =
185
+ pluginLocalizations?.now_playing_title ||
186
+ (normalizedPluginId
187
+ ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`] ||
188
+ pluginLocalizations?.[`${normalizedPluginId}_label_now_playing`]
189
+ : undefined) ||
190
+ "Now Playing";
191
+
192
+ const clearLabel =
193
+ pluginLocalizations?.clear_label ||
194
+ (normalizedPluginId
195
+ ? pluginLocalizations?.[`${normalizedPluginId}_label_clear`]
196
+ : undefined) ||
197
+ "Clear";
198
+
199
+ const resolvedTitle = props.title || localizedHeaderTitle || title;
200
+
201
+ const itemsToRender = viewModel ? fetchedItems : items;
202
+ const operations = editableCollection?.operations || [];
203
+
204
+ const isSortable = useDeferredValue(
205
+ !measurementPhase && hasOperation(operations, OPERATIONS.REORDER)
206
+ );
207
+
208
+ const cell = getCell(
209
+ viewModel ? role : undefined,
210
+ viewModel ? behavior : undefined
211
+ );
212
+
213
+ const Header = headerComponent ?? DefaultModalHeader;
214
+
66
215
  useEffect(() => {
67
216
  if (currentRoute !== route.current) {
68
217
  props.dismiss();
@@ -74,14 +223,24 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
74
223
  }, [theme, props.iconProps]);
75
224
 
76
225
  const itemBaseProps = useMemo<ItemProps>(() => {
77
- return getItemProps(theme, props.width, props.itemProps);
78
- }, [theme, props.width, props.itemProps]);
226
+ return getItemProps(
227
+ theme,
228
+ props.width,
229
+ props.itemProps,
230
+ styleOverrides?.item
231
+ );
232
+ }, [theme, props.width, props.itemProps, styleOverrides?.item]);
79
233
 
80
234
  const labelBaseProps = useMemo<ItemLabelProps>(() => {
81
- return getItemLabelProps(theme, props.width, props.labelProps);
82
- }, [theme, props.width, props.labelProps]);
235
+ return getItemLabelProps(
236
+ theme,
237
+ props.width,
238
+ props.labelProps,
239
+ styleOverrides?.item
240
+ );
241
+ }, [theme, props.width, props.labelProps, styleOverrides?.item]);
83
242
 
84
- const handlePress = useCallback(
243
+ const handleStaticPress = useCallback(
85
244
  (item: any) => {
86
245
  onPress(item);
87
246
  dismiss();
@@ -89,45 +248,272 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
89
248
  [onPress, dismiss]
90
249
  );
91
250
 
92
- const bottomInset = useSafeAreaInsets().bottom;
251
+ const onItemPress = viewModel ? handleItemPress : handleStaticPress;
93
252
 
94
- return (
95
- <View
96
- style={{
97
- maxHeight: props.maxHeight,
98
- paddingTop: paddingTop,
99
- }}
100
- >
101
- <ModalHeader
102
- dismiss={dismiss}
103
- configuration={theme}
104
- summary={summary}
105
- title={title}
106
- />
253
+ const handleDragEnd = useCallback(
254
+ (params: any) => {
255
+ const from = params?.fromIndex ?? params?.from;
256
+ const to = params?.toIndex ?? params?.to;
257
+
258
+ const reorderedItemIds = Array.isArray(params?.data)
259
+ ? params.data
260
+ .filter((item: any) => !item?.isHeader)
261
+ .map((item: any) => item?.id)
262
+ .filter(Boolean)
263
+ : undefined;
264
+
265
+ if (
266
+ (reorderedItemIds?.length ||
267
+ (typeof from === "number" &&
268
+ typeof to === "number" &&
269
+ from !== to)) &&
270
+ editableCollection?.move
271
+ ) {
272
+ editableCollection.move(reorderedItemIds || []);
273
+ }
274
+ },
275
+ [editableCollection]
276
+ );
277
+
278
+ const renderContext = useMemo(
279
+ () => ({
280
+ theme,
281
+ ButtonComponent,
282
+ current_selection,
283
+ iconBaseProps,
284
+ itemBaseProps,
285
+ labelBaseProps,
286
+ getSelectedItemIcon,
287
+ getDefaultItemIcon,
288
+ iconPlacement,
289
+ editableCollection,
290
+ behavior,
291
+ }),
292
+ [
293
+ theme,
294
+ ButtonComponent,
295
+ current_selection,
296
+ iconBaseProps,
297
+ itemBaseProps,
298
+ labelBaseProps,
299
+ getSelectedItemIcon,
300
+ getDefaultItemIcon,
301
+ iconPlacement,
302
+ editableCollection,
303
+ behavior,
304
+ ]
305
+ );
306
+
307
+ const sortableListRenderItem = useCallback(
308
+ ({ item, index, renderHandle }) =>
309
+ cell.renderItem({
310
+ item,
311
+ index,
312
+ onPress: onItemPress,
313
+ context: renderContext,
314
+ renderHandle,
315
+ }),
316
+ [cell, onItemPress, renderContext]
317
+ );
318
+
319
+ const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
320
+ const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
321
+ const hasAdd = hasOperation(operations, OPERATIONS.ADD);
322
+ const isQueueEmpty = hasNowPlaying && (itemsToRender?.length ?? 0) === 0;
323
+
324
+ const handleDeleteCollection = useCallback(() => {
325
+ editableCollection?.deleteCollection?.();
326
+ }, [editableCollection]);
327
+
328
+ const handleCreatePlaylist = useCallback(() => {
329
+ editableCollection?.add?.({
330
+ id: "create_new_playlist",
331
+ title: "unknown title",
332
+ });
333
+ }, [editableCollection]);
334
+
335
+ const headers = useMemo(() => {
336
+ const list: Array<{ index: number; component: () => React.ReactElement }> =
337
+ [];
338
+
339
+ if (hasNowPlaying) {
340
+ list.push({
341
+ index: list.length,
342
+ component: () => (
343
+ <NowPlayingHeaderSection
344
+ onClear={hasClearAll ? handleDeleteCollection : undefined}
345
+ nextUpTitle={nextUpTitle}
346
+ nowPlayingTitle={nowPlayingTitle}
347
+ isEmpty={isQueueEmpty}
348
+ emptyMessage={emptyMessage}
349
+ clearLabel={clearLabel}
350
+ />
351
+ ),
352
+ });
353
+ }
354
+
355
+ if (hasAdd) {
356
+ list.push({
357
+ index: list.length,
358
+ component: () => (
359
+ <View
360
+ style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }}
361
+ >
362
+ <AudioPlayerButton onPress={handleCreatePlaylist} />
363
+ </View>
364
+ ),
365
+ });
366
+ }
367
+
368
+ return list.length > 0 ? list : undefined;
369
+ }, [
370
+ hasNowPlaying,
371
+ hasClearAll,
372
+ handleDeleteCollection,
373
+ nextUpTitle,
374
+ nowPlayingTitle,
375
+ isQueueEmpty,
376
+ emptyMessage,
377
+ clearLabel,
378
+ hasAdd,
379
+ handleCreatePlaylist,
380
+ ]);
381
+
382
+ const _items = useMemo(() => {
383
+ return (itemsToRender || []).map((item, index) =>
384
+ cell.renderItem({
385
+ item,
386
+ index,
387
+ onPress: onItemPress,
388
+ context: renderContext,
389
+ })
390
+ );
391
+ }, [itemsToRender, cell, onItemPress, renderContext]);
392
+
393
+ const _styles = useMemo(() => {
394
+ return {
395
+ maxHeight: props.maxHeight,
396
+ paddingTop: paddingTop,
397
+ };
398
+ }, [props.maxHeight, paddingTop]);
399
+
400
+ const renderedBody = (() => {
401
+ if (ContentComponent) {
402
+ const shouldWrapInScrollView =
403
+ !contentComponentProps?.disableScrollViewWrap &&
404
+ !(ContentComponent as any)?.disableScrollViewWrap;
405
+
406
+ if (shouldWrapInScrollView) {
407
+ return (
408
+ <ScrollView
409
+ keyboardShouldPersistTaps="handled"
410
+ contentContainerStyle={calculatedcontentContainerStyle}
411
+ >
412
+ <ContentComponent
413
+ {...contentComponentProps}
414
+ dismiss={dismiss}
415
+ width={props.width}
416
+ maxHeight={props.maxHeight}
417
+ />
418
+ </ScrollView>
419
+ );
420
+ }
421
+
422
+ return (
423
+ <View style={[styles.scrollView, calculatedcontentContainerStyle]}>
424
+ <ContentComponent
425
+ {...contentComponentProps}
426
+ dismiss={dismiss}
427
+ width={props.width}
428
+ maxHeight={props.maxHeight}
429
+ />
430
+ </View>
431
+ );
432
+ }
433
+
434
+ if (showCentralLoading) {
435
+ const loaderColor =
436
+ (theme as any)?.modal_bottom_sheet_activity_indicator_color ||
437
+ (theme as any)?.cell_title_color ||
438
+ "#ffffff";
439
+
440
+ return (
441
+ <View
442
+ style={[
443
+ styles.centered,
444
+ { backgroundColor: theme.modal_bottom_sheet_background_color },
445
+ ]}
446
+ >
447
+ <ActivityIndicator color={loaderColor} />
448
+ </View>
449
+ );
450
+ }
451
+
452
+ if (error) {
453
+ return (
454
+ <View style={styles.centered}>
455
+ <Text style={styles.errorText}>
456
+ {typeof error === "string" ? error : error.message}
457
+ </Text>
458
+ </View>
459
+ );
460
+ }
461
+
462
+ if (isSortable) {
463
+ return (
464
+ <SortableList
465
+ sortableData={itemsToRender}
466
+ headers={headers}
467
+ scrollViewStyle={styles.scrollView}
468
+ contentContainerStyle={calculatedcontentContainerStyle}
469
+ onDragEnd={handleDragEnd}
470
+ renderItem={sortableListRenderItem}
471
+ />
472
+ );
473
+ }
474
+
475
+ return (
107
476
  <ScrollView
108
- contentContainerStyle={{
109
- paddingBottom: paddingBottom + bottomInset,
110
- }}
477
+ keyboardShouldPersistTaps="handled"
478
+ overScrollMode="never"
479
+ bounces={false}
480
+ contentContainerStyle={calculatedcontentContainerStyle}
111
481
  >
112
- {items.map((item, index) =>
113
- item ? (
114
- <ButtonComponent
115
- key={index}
116
- configuration={theme}
117
- selectedItem={current_selection}
118
- item={item}
119
- onPress={handlePress}
120
- label={theme[item?.label] ?? item?.label}
121
- iconBaseProps={iconBaseProps}
122
- itemBaseProps={itemBaseProps}
123
- labelBaseProps={labelBaseProps}
124
- selectedItemIcon={getSelectedItemIcon(theme)}
125
- defaultItemIcon={getDefaultItemIcon(theme)}
126
- iconPlacement={iconPlacement}
127
- />
128
- ) : null
129
- )}
482
+ {hasNowPlaying ? (
483
+ <NowPlayingHeaderSection
484
+ onClear={hasClearAll ? handleDeleteCollection : undefined}
485
+ nextUpTitle={nextUpTitle}
486
+ nowPlayingTitle={nowPlayingTitle}
487
+ isEmpty={isQueueEmpty}
488
+ emptyMessage={emptyMessage}
489
+ clearLabel={clearLabel}
490
+ />
491
+ ) : null}
492
+ {hasAdd ? (
493
+ <View style={styles.someStyles}>
494
+ <AudioPlayerButton onPress={handleCreatePlaylist} />
495
+ </View>
496
+ ) : null}
497
+ {_items}
130
498
  </ScrollView>
131
- </View>
499
+ );
500
+ })();
501
+
502
+ return (
503
+ <ModalBlocksStyleContext.Provider value={styleOverrides as any}>
504
+ <View style={_styles}>
505
+ <BottomSheetDragArea>
506
+ <Header
507
+ dismiss={dismiss}
508
+ configuration={theme}
509
+ summary={summary}
510
+ title={resolvedTitle}
511
+ />
512
+ </BottomSheetDragArea>
513
+ {renderedBody}
514
+ </View>
515
+ </ModalBlocksStyleContext.Provider>
132
516
  );
133
517
  }
518
+
519
+ 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
+ });