@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1453660071 → 16.0.0-alpha.1454930193

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