@applicaster/zapp-react-native-ui-components 15.1.13-alpha.1034774223 → 15.1.13

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