@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9530606740 → 16.0.0-alpha.9739533780

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.
@@ -123,7 +123,7 @@ export type ActionButtonsStyleConfiguration = {
123
123
  };
124
124
 
125
125
  export type HeaderConfiguration = {
126
- paddingTop?: number;
126
+ paddingTop: undefined;
127
127
  variant: HeaderVariant;
128
128
  title: TitleConfiguration;
129
129
  closeButton: CloseButtonConfiguration;
@@ -211,7 +211,6 @@ export type AudioPlayerTrackItemProps = {
211
211
  onPress?: () => void;
212
212
  onLeadingButtonPress?: () => void;
213
213
  onTrailingButtonPress?: () => void;
214
- renderHandle?: (element: React.ReactElement) => React.ReactElement;
215
214
  };
216
215
 
217
216
  // ---------------------------------------------------------------------------
@@ -705,7 +704,6 @@ type ItemButtonsProps = {
705
704
  onLeadingButtonPress?: () => void;
706
705
  onTrailingButtonPress?: () => void;
707
706
  isSelected?: boolean;
708
- renderHandle?: (element: React.ReactElement) => React.ReactElement;
709
707
  };
710
708
 
711
709
  function ItemButtons({
@@ -716,7 +714,6 @@ function ItemButtons({
716
714
  onLeadingButtonPress,
717
715
  onTrailingButtonPress,
718
716
  isSelected = false,
719
- renderHandle,
720
717
  }: ItemButtonsProps) {
721
718
  if (!leadingButton && !trailingButton) {
722
719
  return null;
@@ -736,32 +733,12 @@ function ItemButtons({
736
733
  </>
737
734
  ) : null}
738
735
  {trailingButton ? (
739
- trailingButton === "drag" ? (
740
- renderHandle ? (
741
- renderHandle(
742
- <ItemButton
743
- action={trailingButton}
744
- focused={focusedTrailingButton}
745
- onPress={onTrailingButtonPress}
746
- isSelected={isSelected}
747
- />
748
- )
749
- ) : (
750
- <ItemButton
751
- action={trailingButton}
752
- focused={focusedTrailingButton}
753
- onPress={onTrailingButtonPress}
754
- isSelected={isSelected}
755
- />
756
- )
757
- ) : (
758
- <ItemButton
759
- action={trailingButton}
760
- focused={focusedTrailingButton}
761
- onPress={onTrailingButtonPress}
762
- isSelected={isSelected}
763
- />
764
- )
736
+ <ItemButton
737
+ action={trailingButton}
738
+ focused={focusedTrailingButton}
739
+ onPress={onTrailingButtonPress}
740
+ isSelected={isSelected}
741
+ />
765
742
  ) : null}
766
743
  </View>
767
744
  );
@@ -780,7 +757,6 @@ export function AudioPlayerTrackItem({
780
757
  onPress,
781
758
  onLeadingButtonPress,
782
759
  onTrailingButtonPress,
783
- renderHandle,
784
760
  }: AudioPlayerTrackItemProps) {
785
761
  const {
786
762
  background,
@@ -831,7 +807,6 @@ export function AudioPlayerTrackItem({
831
807
  onLeadingButtonPress={onLeadingButtonPress}
832
808
  onTrailingButtonPress={onTrailingButtonPress}
833
809
  isSelected={data.isSelected}
834
- renderHandle={renderHandle}
835
810
  />
836
811
  </View>
837
812
  </ItemContainer>
@@ -23,6 +23,12 @@ type ModalComponentProps = {
23
23
  maxHeight?: number;
24
24
  dismiss: () => void;
25
25
  headerComponent?: React.ComponentType;
26
+
27
+ scrollViewContent?: {
28
+ renderItem: (item: any, index: number) => React.ReactNode;
29
+ items: any[];
30
+ } | null;
31
+
26
32
  buttonComponent?: React.ComponentType;
27
33
  iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps);
28
34
  itemProps?:
@@ -50,6 +56,9 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
50
56
  summary,
51
57
  title,
52
58
  headerComponent: ModalHeader = DefaultModalHeader,
59
+
60
+ scrollViewContent = null,
61
+
53
62
  buttonComponent: ButtonComponent = Button,
54
63
  getSelectedItemIcon = ({
55
64
  modal_bottom_sheet_item_selected_icon,
@@ -109,24 +118,31 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
109
118
  paddingBottom: paddingBottom + bottomInset,
110
119
  }}
111
120
  >
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
- )}
121
+ {scrollViewContent
122
+ ? scrollViewContent.items.map((item, index) =>
123
+ scrollViewContent.renderItem(item, index)
124
+ )
125
+ : null}
126
+
127
+ {!scrollViewContent &&
128
+ items.map((item, index) =>
129
+ item ? (
130
+ <ButtonComponent
131
+ key={index}
132
+ configuration={theme}
133
+ selectedItem={current_selection}
134
+ item={item}
135
+ onPress={handlePress}
136
+ label={theme[item?.label] ?? item?.label}
137
+ iconBaseProps={iconBaseProps}
138
+ itemBaseProps={itemBaseProps}
139
+ labelBaseProps={labelBaseProps}
140
+ selectedItemIcon={getSelectedItemIcon(theme)}
141
+ defaultItemIcon={getDefaultItemIcon(theme)}
142
+ iconPlacement={iconPlacement}
143
+ />
144
+ ) : null
145
+ )}
130
146
  </ScrollView>
131
147
  </View>
132
148
  );
@@ -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"]>(
@@ -2,13 +2,6 @@ import React, { useCallback, useState } from "react";
2
2
 
3
3
  import { BasicMeasurementsPortal as MeasurementsPortal } from "@applicaster/zapp-react-native-ui-components/Components/MeasurmentsPortal";
4
4
  import { BottomSheetModalContent } from "./BottomSheetModalContent";
5
- import { modalOrchestrator } from "@applicaster/zapp-react-native-utils/modalState";
6
- import { MultiLevelBottomSheetContent } from "./AudioPlayer/MultiLevelBottomSheetContent";
7
-
8
- // Register MultiLevelBottomSheetContent component dynamically if modalOrchestrator is defined (e.g. not mocked out in tests)
9
- if (modalOrchestrator && typeof modalOrchestrator.registerSheetContentComponent === "function") {
10
- modalOrchestrator.registerSheetContentComponent(MultiLevelBottomSheetContent);
11
- }
12
5
 
13
6
  export type PluginConfiguration = {
14
7
  [key: string]: any;
@@ -1,88 +1,7 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`OfflineHandler renders 1`] = `
4
- <View
5
- style={
6
- {
7
- "flex": 1,
8
- "position": "relative",
9
- }
10
- }
11
- >
12
- <View
13
- style={
14
- {
15
- "bottom": 0,
16
- "left": 0,
17
- "position": "absolute",
18
- "right": 0,
19
- "top": 0,
20
- }
21
- }
22
- >
23
- <View
24
- accessibilityState={
25
- {
26
- "busy": undefined,
27
- "checked": undefined,
28
- "disabled": undefined,
29
- "expanded": undefined,
30
- "selected": undefined,
31
- }
32
- }
33
- accessible={true}
34
- collapsable={false}
35
- focusable={true}
36
- onClick={[Function]}
37
- onResponderGrant={[Function]}
38
- onResponderMove={[Function]}
39
- onResponderRelease={[Function]}
40
- onResponderTerminate={[Function]}
41
- onResponderTerminationRequest={[Function]}
42
- onStartShouldSetResponder={[Function]}
43
- style={
44
- {
45
- "alignItems": "center",
46
- "backgroundColor": undefined,
47
- "flex": 1,
48
- "height": 88,
49
- "justifyContent": "center",
50
- "left": 0,
51
- "opacity": 0,
52
- "position": "absolute",
53
- "right": 0,
54
- "top": 0,
55
- "transform": [
56
- {
57
- "translateY": -88,
58
- },
59
- ],
60
- "width": "100%",
61
- }
62
- }
63
- >
64
- <View
65
- style={
66
- {
67
- "backgroundColor": undefined,
68
- "height": 44,
69
- }
70
- }
71
- />
72
- <Text
73
- style={
74
- {
75
- "color": undefined,
76
- "fontFamily": undefined,
77
- "fontSize": undefined,
78
- "letterSpacing": undefined,
79
- "lineHeight": undefined,
80
- }
81
- }
82
- >
83
- You are back online
84
- </Text>
85
- </View>
86
- </View>
87
- </View>
3
+ exports[`OfflineHandler renders its children without wrapping them in a notification view 1`] = `
4
+ <Text>
5
+ child content
6
+ </Text>
88
7
  `;
@@ -1,26 +1,26 @@
1
1
  import React from "react";
2
+ import { Text } from "react-native";
2
3
  import { render } from "@testing-library/react-native";
4
+ import { subscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
3
5
 
4
6
  jest.useFakeTimers({ legacyFakeTimers: true });
5
7
 
8
+ // `isOnline` (@applicaster/quick-brick-core/App/NetworkStatusProvider/utils)
9
+ // only treats connectionInfo as "ready" when it has NetInfoState-shaped
10
+ // isConnected/isInternetReachable/details fields, so the mock has to mirror
11
+ // that shape for online/offline to actually be distinguishable.
6
12
  let mockConnectionStatus = false;
7
- const mockPreviousValue = null;
8
13
 
9
14
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/connection", () => {
10
15
  return {
11
16
  ...jest.requireActual(
12
17
  "@applicaster/zapp-react-native-utils/reactHooks/connection"
13
18
  ),
14
- useConnectionInfo: jest.fn(() => mockConnectionStatus),
15
- };
16
- });
17
-
18
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => {
19
- return {
20
- ...jest.requireActual(
21
- "@applicaster/zapp-react-native-utils/reactHooks/utils"
22
- ),
23
- usePrevious: jest.fn(() => mockPreviousValue),
19
+ useConnectionInfo: jest.fn(() => ({
20
+ details: {},
21
+ isConnected: mockConnectionStatus,
22
+ isInternetReachable: mockConnectionStatus,
23
+ })),
24
24
  };
25
25
  });
26
26
 
@@ -54,21 +54,81 @@ jest.mock("react-native-safe-area-context", () => ({
54
54
  }));
55
55
 
56
56
  describe("OfflineHandler", () => {
57
- const { OfflineHandler, NotificationView } = require("../");
57
+ const { OfflineHandler } = require("../");
58
+
59
+ beforeEach(() => {
60
+ mockConnectionStatus = false;
61
+ });
62
+
63
+ it("renders its children without wrapping them in a notification view", () => {
64
+ const { toJSON } = render(
65
+ <OfflineHandler>
66
+ <Text>child content</Text>
67
+ </OfflineHandler>
68
+ );
58
69
 
59
- it("renders", () => {
60
- const { toJSON } = render(<OfflineHandler />);
61
70
  expect(toJSON()).toMatchSnapshot();
62
71
  });
63
72
 
64
- it("renders Notification mode if component was rendered while online", () => {
73
+ it("emits a showToast event on mount when the device starts offline", () => {
74
+ mockConnectionStatus = false;
75
+
76
+ const handleShowToast = jest.fn();
77
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
78
+
79
+ render(<OfflineHandler />);
80
+
81
+ expect(handleShowToast).toHaveBeenCalledWith(
82
+ expect.objectContaining({
83
+ id: "offline-status",
84
+ message: "No internet connection",
85
+ timeout: 5000,
86
+ }),
87
+ // the shared event bus appends a dispose fn as the last handler arg
88
+ expect.any(Function)
89
+ );
90
+
91
+ unsubscribe();
92
+ });
93
+
94
+ it("does not emit a showToast event on mount when the device starts online", () => {
65
95
  mockConnectionStatus = true;
66
96
 
67
- const { rerender, UNSAFE_getByType } = render(<OfflineHandler />);
97
+ const handleShowToast = jest.fn();
98
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
99
+
100
+ render(<OfflineHandler />);
101
+
102
+ expect(handleShowToast).not.toHaveBeenCalled();
103
+
104
+ unsubscribe();
105
+ });
106
+
107
+ it("emits a showToast event when the device comes back online", () => {
108
+ mockConnectionStatus = false;
109
+
110
+ const handleShowToast = jest.fn();
111
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
112
+
113
+ const { rerender } = render(<OfflineHandler />);
68
114
 
115
+ // ignore the offline toast emitted on mount; we only care about the
116
+ // offline -> online transition here
117
+ handleShowToast.mockClear();
118
+
119
+ mockConnectionStatus = true;
69
120
  rerender(<OfflineHandler />);
70
121
 
71
- const notificationsView = UNSAFE_getByType(NotificationView);
72
- expect(notificationsView).toBeDefined();
122
+ expect(handleShowToast).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ id: "offline-status",
125
+ message: "You are back online",
126
+ timeout: 2000,
127
+ }),
128
+ // the shared event bus appends a dispose fn as the last handler arg
129
+ expect.any(Function)
130
+ );
131
+
132
+ unsubscribe();
73
133
  });
74
134
  });
@@ -0,0 +1,123 @@
1
+ import React, { useMemo } from "react";
2
+
3
+ import {
4
+ isWeb,
5
+ platformSelect,
6
+ } from "@applicaster/zapp-react-native-utils/reactUtils";
7
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
8
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
9
+
10
+ import { usePlugins } from "@applicaster/zapp-react-native-redux";
11
+ import { textTransform } from "@applicaster/zapp-react-native-utils/cellUtils";
12
+ import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
13
+
14
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
15
+
16
+ const OFFLINE_EXPERIENCE = "offline-experience";
17
+
18
+ const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
19
+
20
+ const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
21
+
22
+ export const useNotificationHeight = () => {
23
+ const insets = useSafeAreaInsets();
24
+
25
+ const navBarHeight = platformSelect({ ios: 44, android: 56 });
26
+
27
+ const statusHeight = insets.top;
28
+ const notificationHeight = statusHeight + navBarHeight;
29
+
30
+ return { statusHeight, notificationHeight };
31
+ };
32
+
33
+ export function useNetworkStatusLocalizations() {
34
+ const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
35
+ string,
36
+ string
37
+ > | null>(null);
38
+
39
+ React.useEffect(() => {
40
+ async function loadStoredLocalizations() {
41
+ try {
42
+ const stored = await sessionStorage.getItem(
43
+ NETWORK_STATUS_LOCALIZATIONS_KEY,
44
+ THEME_STORAGE_NAMESPACE
45
+ );
46
+
47
+ if (stored) {
48
+ setStoredLocalizations(stored);
49
+ }
50
+ } catch (error) {
51
+ console.error("Error loading network status localizations", error);
52
+ }
53
+ }
54
+
55
+ loadStoredLocalizations();
56
+ }, []);
57
+
58
+ return storedLocalizations;
59
+ }
60
+
61
+ export function useOfflineHandlerProps(online: boolean) {
62
+ const theme = useTheme<BaseThemePropertiesTV>();
63
+ const storedLocalizations = useNetworkStatusLocalizations();
64
+ const plugins = usePlugins();
65
+ const offlinePlugin = findPluginByIdentifier(OFFLINE_EXPERIENCE, plugins);
66
+ const { useOfflineExperienceConfiguration } = offlinePlugin?.module ?? {};
67
+
68
+ const {
69
+ configurationFields,
70
+ localizations,
71
+ }: {
72
+ configurationFields: OfflineExperiencePluginConfiguration;
73
+ localizations: OfflineExperienceLocalizations;
74
+ } = useOfflineExperienceConfiguration?.() ?? {};
75
+
76
+ let title, subtitle, style;
77
+
78
+ if (isWeb()) {
79
+ title = online
80
+ ? (theme?.online_notification_title ??
81
+ storedLocalizations?.online_notification_title)
82
+ : (theme?.offline_notification_title ??
83
+ storedLocalizations?.offline_notification_title);
84
+
85
+ subtitle = online
86
+ ? (theme?.online_notification_subtitle ??
87
+ storedLocalizations?.online_notification_subtitle)
88
+ : (theme?.offline_notification_subtitle ??
89
+ storedLocalizations?.offline_notification_subtitle);
90
+ } else {
91
+ title = textTransform(
92
+ configurationFields.offline_toast_message_text_transform,
93
+ online
94
+ ? localizations.online_toast_message
95
+ : localizations.offline_toast_message
96
+ );
97
+
98
+ style = {
99
+ color: online
100
+ ? configurationFields.offline_toast_message_online_font_color
101
+ : configurationFields.offline_toast_message_font_color,
102
+ backgroundColor: online
103
+ ? configurationFields.offline_toast_online_background_color
104
+ : configurationFields.offline_toast_offline_background_color,
105
+ fontFamily: platformSelect({
106
+ ios: configurationFields.offline_toast_message_ios_font_family,
107
+ android: configurationFields.offline_toast_message_android_font_family,
108
+ }),
109
+ fontSize: configurationFields.offline_toast_message_font_size,
110
+ lineHeight: configurationFields.offline_toast_message_line_height,
111
+ letterSpacing: platformSelect({
112
+ ios: configurationFields.offline_toast_message_letter_spacing_ios,
113
+ android:
114
+ configurationFields.offline_toast_message_letter_spacing_android,
115
+ }),
116
+ };
117
+ }
118
+
119
+ return useMemo(
120
+ () => ({ message: title, extraMessage: subtitle, style }),
121
+ [title, subtitle, style]
122
+ );
123
+ }