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

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