@applicaster/zapp-react-native-ui-components 16.0.0-rc.12 → 16.0.0-rc.121

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 (166) 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 +1 -1
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  8. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  9. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  10. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  11. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  15. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  16. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  17. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  18. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  19. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  20. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  22. package/Components/GeneralContentScreen/index.ts +2 -0
  23. package/Components/Layout/FullWidthRow.tsx +38 -0
  24. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  30. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  34. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  35. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  36. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +14 -6
  37. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  38. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  39. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  40. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +34 -21
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  52. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  53. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  54. package/Components/MasterCell/__tests__/masterCell.test.js +60 -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 +57 -8
  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/__tests__/withoutFocusedState.test.js +20 -0
  63. package/Components/MasterCell/utils/index.ts +103 -46
  64. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  65. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  66. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  67. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  68. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +772 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +728 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +1028 -0
  73. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  74. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  75. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  76. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  77. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +433 -0
  78. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +563 -0
  79. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  80. package/Components/ModalComponent/BottomSheetModalContent.tsx +440 -50
  81. package/Components/ModalComponent/Button/index.tsx +5 -3
  82. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  83. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  84. package/Components/ModalComponent/Header/index.tsx +20 -7
  85. package/Components/ModalComponent/Header/utils.ts +1 -1
  86. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  87. package/Components/ModalComponent/SortableList.tsx +156 -0
  88. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  89. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  90. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  91. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  92. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  93. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  94. package/Components/ModalComponent/__tests__/showTextInput.test.ts +120 -0
  95. package/Components/ModalComponent/index.tsx +18 -9
  96. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  97. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  98. package/Components/ModalComponent/presets/index.ts +27 -0
  99. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  100. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  101. package/Components/ModalComponent/presets/types.ts +35 -0
  102. package/Components/ModalComponent/showTextInput.ts +54 -0
  103. package/Components/ModalComponent/utils.ts +82 -21
  104. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  105. package/Components/OfflineHandler/__tests__/index.test.tsx +186 -17
  106. package/Components/OfflineHandler/hooks.ts +111 -0
  107. package/Components/OfflineHandler/index.tsx +91 -75
  108. package/Components/OfflineHandler/utils/index.ts +1 -13
  109. package/Components/PlayerContainer/PlayerContainer.tsx +28 -11
  110. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  111. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  112. package/Components/PreloaderWrapper/index.tsx +1 -1
  113. package/Components/River/ComponentsMap/ComponentsMap.tsx +91 -62
  114. package/Components/River/River.tsx +4 -0
  115. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +142 -89
  116. package/Components/River/__tests__/componentsMap.test.js +8 -3
  117. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  118. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  119. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  120. package/Components/Screen/index.tsx +13 -4
  121. package/Components/Screen/navigationHandler.ts +8 -8
  122. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  123. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  124. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  125. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  126. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  127. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  128. package/Components/TopCutoffOverlay/index.tsx +2 -2
  129. package/Components/Transitioner/AnimationManager.js +1 -1
  130. package/Components/Transitioner/Scene.tsx +4 -3
  131. package/Components/Transitioner/Transitioner.tsx +98 -49
  132. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  133. package/Components/VideoLive/LiveImageManager.ts +9 -3
  134. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  135. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  136. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  137. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  138. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  139. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  140. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  141. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  142. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  143. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  144. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  145. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  146. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  147. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  148. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  149. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  150. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  151. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  152. package/Helpers/index.js +0 -39
  153. package/Hooks/useEntryActionsExpander.ts +63 -0
  154. package/assets/empty-Info.png +0 -0
  155. package/package.json +6 -5
  156. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  157. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  158. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  159. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  160. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  161. package/Components/Screen/utils.ts +0 -16
  162. package/Helpers/Analytics/index.js +0 -95
  163. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  164. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  165. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  166. /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,25 @@ 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
+ 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;
41
98
  };
42
99
 
43
- export function BottomSheetModalContent(props: ModalComponentProps) {
100
+ function _BottomSheetModalContent(props: ModalComponentProps) {
44
101
  const {
45
102
  items,
46
103
  currentRoute,
@@ -49,20 +106,116 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
49
106
  dismiss,
50
107
  summary,
51
108
  title,
52
- headerComponent: ModalHeader = DefaultModalHeader,
109
+ headerComponent,
110
+ viewModel = null,
53
111
  buttonComponent: ButtonComponent = Button,
54
112
  getSelectedItemIcon = ({
55
113
  modal_bottom_sheet_item_selected_icon,
56
114
  }: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon,
57
115
  getDefaultItemIcon = () => null,
58
116
  iconPlacement,
117
+ contentComponent: ContentComponent,
118
+ contentComponentProps = {},
119
+ themePluginId: themePluginIdProp,
120
+ measurementPhase = false,
121
+ emptyMessage,
59
122
  } = props;
60
123
 
61
124
  const route = useRef(currentRoute);
62
125
  const theme = useTheme<BaseThemePropertiesMobile>();
126
+ const bottomInset = useSafeAreaInsets().bottom;
63
127
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
64
128
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
65
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
+
66
219
  useEffect(() => {
67
220
  if (currentRoute !== route.current) {
68
221
  props.dismiss();
@@ -74,14 +227,24 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
74
227
  }, [theme, props.iconProps]);
75
228
 
76
229
  const itemBaseProps = useMemo<ItemProps>(() => {
77
- return getItemProps(theme, props.width, props.itemProps);
78
- }, [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]);
79
237
 
80
238
  const labelBaseProps = useMemo<ItemLabelProps>(() => {
81
- return getItemLabelProps(theme, props.width, props.labelProps);
82
- }, [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]);
83
246
 
84
- const handlePress = useCallback(
247
+ const handleStaticPress = useCallback(
85
248
  (item: any) => {
86
249
  onPress(item);
87
250
  dismiss();
@@ -89,45 +252,272 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
89
252
  [onPress, dismiss]
90
253
  );
91
254
 
92
- const bottomInset = useSafeAreaInsets().bottom;
255
+ const onItemPress = viewModel ? handleItemPress : handleStaticPress;
93
256
 
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
- />
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 (
107
480
  <ScrollView
108
- contentContainerStyle={{
109
- paddingBottom: paddingBottom + bottomInset,
110
- }}
481
+ keyboardShouldPersistTaps="handled"
482
+ overScrollMode="never"
483
+ bounces={false}
484
+ contentContainerStyle={calculatedcontentContainerStyle}
111
485
  >
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
- )}
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}
130
502
  </ScrollView>
131
- </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>
132
520
  );
133
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
+ });