@applicaster/zapp-react-native-ui-components 16.0.0-rc.9 → 16.0.0-rc.91

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