@applicaster/zapp-react-native-ui-components 15.0.0-rc.129 → 15.0.0-rc.131

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 (30) hide show
  1. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  2. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  3. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  5. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  6. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  7. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  8. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  9. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  10. package/Components/MasterCell/DefaultComponents/PressableView.tsx +196 -0
  11. package/Components/MasterCell/DefaultComponents/index.ts +2 -0
  12. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +46 -0
  13. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +126 -0
  14. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
  15. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  16. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  17. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
  18. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +191 -0
  19. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
  20. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +222 -0
  21. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  22. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +104 -0
  23. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  24. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +73 -0
  25. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +86 -0
  26. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +35 -52
  27. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +35 -171
  28. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +36 -145
  29. package/Components/MasterCell/elementMapper.tsx +1 -0
  30. package/package.json +5 -5
@@ -0,0 +1,196 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { TouchableOpacity } from "react-native";
3
+
4
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+
6
+ import { masterCellLogger } from "../logger";
7
+ import {
8
+ buildLegacySelection,
9
+ resolveIsActive,
10
+ resolveLabelText,
11
+ } from "./mobile/MobileActionButtons/helpers";
12
+
13
+ type ChildElementProps = {
14
+ children?: React.ReactNode;
15
+ mobileActionRole?: string;
16
+ state?: string;
17
+ [key: string]: unknown;
18
+ };
19
+
20
+ type Props = {
21
+ children?: React.ReactNode;
22
+ item: ZappEntry | ZappFeed;
23
+ action?: {
24
+ identifier?: string;
25
+ };
26
+ style?: Record<string, unknown>;
27
+ focusedStyles?: Record<string, unknown>;
28
+ testID?: string;
29
+ accessibilityLabel?: string;
30
+ accessibilityHint?: string;
31
+ };
32
+
33
+ const isValidElement = (
34
+ child: React.ReactNode
35
+ ): child is React.ReactElement<ChildElementProps> =>
36
+ React.isValidElement(child);
37
+
38
+ export function PressableView({
39
+ children,
40
+ item,
41
+ action,
42
+ style = {},
43
+ focusedStyles = {},
44
+ testID,
45
+ accessibilityLabel,
46
+ accessibilityHint,
47
+ }: Props) {
48
+ const actionContext = useActions(action?.identifier);
49
+
50
+ const actionDisabled =
51
+ typeof actionContext?.isActionAvailable === "function" &&
52
+ !actionContext.isActionAvailable(item);
53
+
54
+ const supportsEntryState =
55
+ typeof actionContext?.initialEntryState === "function" && !actionDisabled;
56
+
57
+ const [actionState, setActionState] = useState(() =>
58
+ supportsEntryState ? actionContext.initialEntryState(item) : null
59
+ );
60
+
61
+ useEffect(() => {
62
+ if (supportsEntryState) {
63
+ setActionState(actionContext.initialEntryState(item));
64
+ }
65
+ }, [supportsEntryState, item, actionContext]);
66
+
67
+ useEffect(() => {
68
+ if (typeof actionContext?.addListener === "function") {
69
+ return actionContext.addListener(String(item?.id), (nextState) => {
70
+ setActionState(nextState);
71
+ });
72
+ }
73
+
74
+ if (typeof actionContext?.addListeners === "function") {
75
+ return actionContext.addListeners(({ entryState, entry }) => {
76
+ if (entry?.id === item?.id) {
77
+ setActionState(entryState);
78
+ }
79
+ });
80
+ }
81
+
82
+ return undefined;
83
+ }, [actionContext, item?.id]);
84
+
85
+ const legacySelected = useMemo(() => {
86
+ if (!actionContext || supportsEntryState) {
87
+ return false;
88
+ }
89
+
90
+ return buildLegacySelection(item, actionContext);
91
+ }, [actionContext, supportsEntryState, item]);
92
+
93
+ const isActive = resolveIsActive(actionState, legacySelected);
94
+
95
+ const labelText = supportsEntryState
96
+ ? resolveLabelText(actionState?.label)
97
+ : "";
98
+
99
+ const shouldRenderAsset = Boolean(
100
+ supportsEntryState ? actionState?.asset : false
101
+ );
102
+
103
+ const shouldRenderLabel = Boolean(labelText);
104
+
105
+ const cloneChildrenWithState = (nodes?: React.ReactNode): React.ReactNode => {
106
+ return React.Children.map(nodes, (child) => {
107
+ if (!isValidElement(child)) {
108
+ return child;
109
+ }
110
+
111
+ const role = child.props.mobileActionRole;
112
+ const nextChildren = cloneChildrenWithState(child.props.children);
113
+
114
+ if (role === "asset" && !shouldRenderAsset) {
115
+ return null;
116
+ }
117
+
118
+ if (role === "label" && !shouldRenderLabel) {
119
+ return null;
120
+ }
121
+
122
+ if (
123
+ role === "label_container" &&
124
+ React.Children.count(nextChildren) === 0
125
+ ) {
126
+ return null;
127
+ }
128
+
129
+ const nextProps: Partial<ChildElementProps> = {};
130
+
131
+ if (role === "asset") {
132
+ nextProps.state = isActive ? "active" : "inactive";
133
+ }
134
+
135
+ if (role === "label") {
136
+ nextProps.state = isActive ? "focused" : "default";
137
+ }
138
+
139
+ if (nextChildren !== child.props.children) {
140
+ nextProps.children = nextChildren;
141
+ }
142
+
143
+ return React.cloneElement(child, nextProps);
144
+ });
145
+ };
146
+
147
+ const onPress = useCallback(async () => {
148
+ if (!actionContext) {
149
+ return;
150
+ }
151
+
152
+ if (supportsEntryState) {
153
+ return actionContext.invokeAction(item, {
154
+ updateState: setActionState,
155
+ });
156
+ }
157
+
158
+ const favouritesAction = legacySelected
159
+ ? actionContext.removeFavourite
160
+ : actionContext.addFavourite;
161
+
162
+ const toggleAction = actionContext?.invokeAction ?? favouritesAction;
163
+
164
+ return toggleAction?.(item);
165
+ }, [actionContext, supportsEntryState, item, legacySelected]);
166
+
167
+ if (!actionContext) {
168
+ masterCellLogger.warning(
169
+ `You're missing an action plugin(${action?.identifier}) required by your mobile action button.`
170
+ );
171
+
172
+ return null;
173
+ }
174
+
175
+ if (actionDisabled) {
176
+ return null;
177
+ }
178
+
179
+ if (!shouldRenderAsset && !shouldRenderLabel) {
180
+ return null;
181
+ }
182
+
183
+ return (
184
+ <TouchableOpacity
185
+ activeOpacity={1}
186
+ onPress={onPress}
187
+ testID={testID || `${item?.id}`}
188
+ accessibilityLabel={accessibilityLabel || `${item?.id}`}
189
+ accessibilityHint={accessibilityHint}
190
+ accessible={!!(testID || accessibilityLabel)}
191
+ style={isActive ? { ...style, ...focusedStyles } : style}
192
+ >
193
+ {cloneChildrenWithState(children)}
194
+ </TouchableOpacity>
195
+ );
196
+ }
@@ -16,12 +16,14 @@ import { CellBadge } from "./CellBadge";
16
16
  import { TvActionButtons } from "./tv/TvActionButtons";
17
17
  import { ButtonContainerView } from "./tv/ButtonContainerView";
18
18
  import { ImageBorderContainer } from "./ImageBorderContainer";
19
+ import { PressableView } from "./PressableView";
19
20
 
20
21
  export const defaultComponents = {
21
22
  View,
22
23
  CollapsibleTextContainer,
23
24
  Text,
24
25
  Button,
26
+ PressableView,
25
27
  Image,
26
28
  PureImage,
27
29
  ButtonContainerView,
@@ -0,0 +1,46 @@
1
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
2
+
3
+ type Props = {
4
+ prefix: string;
5
+ value: Function;
6
+ actionIdentifier: string;
7
+ testID?: string;
8
+ };
9
+
10
+ export const Asset = ({ prefix, value, actionIdentifier, testID }: Props) => {
11
+ if (!value(`${prefix}_asset_enabled`)) {
12
+ return null;
13
+ }
14
+
15
+ return {
16
+ type: "Image",
17
+ style: {
18
+ width: toNumberWithDefaultZero(value(`${prefix}_asset_width`)),
19
+ height: toNumberWithDefaultZero(value(`${prefix}_asset_height`)),
20
+ marginTop: toNumberWithDefaultZero(value(`${prefix}_asset_margin_top`)),
21
+ marginRight: toNumberWithDefaultZero(
22
+ value(`${prefix}_asset_margin_right`)
23
+ ),
24
+ marginBottom: toNumberWithDefaultZero(
25
+ value(`${prefix}_asset_margin_bottom`)
26
+ ),
27
+ marginLeft: toNumberWithDefaultZero(value(`${prefix}_asset_margin_left`)),
28
+ backgroundColor: "transparent",
29
+ },
30
+ data: [
31
+ {
32
+ func: (entry) => entry,
33
+ args: [],
34
+ propName: "entry",
35
+ },
36
+ ],
37
+ additionalProps: {
38
+ source: {
39
+ context: actionIdentifier,
40
+ },
41
+ state: "inactive",
42
+ mobileActionRole: "asset",
43
+ testID: testID ? `${testID}-asset` : undefined,
44
+ },
45
+ };
46
+ };
@@ -0,0 +1,126 @@
1
+ import {
2
+ toNumberWithDefault,
3
+ toNumberWithDefaultZero,
4
+ } from "@applicaster/zapp-react-native-utils/numberUtils";
5
+ import { compact } from "@applicaster/zapp-react-native-utils/cellUtils";
6
+
7
+ import { Asset } from "./Asset";
8
+ import { Spacer } from "./Spacer";
9
+ import { TextLabelsContainer } from "./TextLabelsContainer";
10
+ import { getContentDirection, getContentsAlignment } from "./helpers";
11
+
12
+ const displayModeStyle = ({ value, prefix }) => {
13
+ const mode = value(`${prefix}_display_mode`) || "dynamic";
14
+
15
+ if (mode === "fixed") {
16
+ return {
17
+ width: toNumberWithDefault(140, value(`${prefix}_width`)),
18
+ };
19
+ }
20
+
21
+ if (mode === "fill") {
22
+ return {
23
+ flex: 1,
24
+ };
25
+ }
26
+
27
+ return {};
28
+ };
29
+
30
+ type Props = {
31
+ index: number;
32
+ value: Function;
33
+ stylePrefix: string;
34
+ specificPrefix: string;
35
+ spacingStyle: Record<string, unknown>;
36
+ };
37
+
38
+ export const Button = ({
39
+ index,
40
+ value,
41
+ stylePrefix,
42
+ specificPrefix,
43
+ spacingStyle,
44
+ }: Props) => {
45
+ if (!value(`${specificPrefix}_button_enabled`)) {
46
+ return null;
47
+ }
48
+
49
+ const testID = `mobile_action_button_${index + 1}`;
50
+ const actionIdentifier = value(`${specificPrefix}_assign_action`);
51
+ const assetAlignment = value(`${stylePrefix}_asset_alignment`) || "left";
52
+
53
+ const contentsAlignment =
54
+ value(`${stylePrefix}_contents_alignment`) || "center";
55
+
56
+ return {
57
+ type: "PressableView",
58
+ style: {
59
+ flexDirection: getContentDirection(assetAlignment),
60
+ alignItems: "center",
61
+ justifyContent: getContentsAlignment(contentsAlignment),
62
+
63
+ marginTop: toNumberWithDefaultZero(value(`${stylePrefix}_margin_top`)),
64
+ marginRight: toNumberWithDefaultZero(
65
+ value(`${stylePrefix}_margin_right`)
66
+ ),
67
+ marginBottom: toNumberWithDefaultZero(
68
+ value(`${stylePrefix}_margin_bottom`)
69
+ ),
70
+ marginLeft: toNumberWithDefaultZero(value(`${stylePrefix}_margin_left`)),
71
+
72
+ paddingTop: toNumberWithDefaultZero(value(`${stylePrefix}_padding_top`)),
73
+ paddingRight: toNumberWithDefaultZero(
74
+ value(`${stylePrefix}_padding_right`)
75
+ ),
76
+ paddingBottom: toNumberWithDefaultZero(
77
+ value(`${stylePrefix}_padding_bottom`)
78
+ ),
79
+ paddingLeft: toNumberWithDefaultZero(
80
+ value(`${stylePrefix}_padding_left`)
81
+ ),
82
+
83
+ borderWidth: toNumberWithDefaultZero(value(`${stylePrefix}_border_size`)),
84
+ borderRadius: toNumberWithDefaultZero(
85
+ value(`${stylePrefix}_corner_radius`)
86
+ ),
87
+ borderColor: value(`${stylePrefix}_border_color`),
88
+ backgroundColor: value(`${stylePrefix}_background_color`),
89
+
90
+ ...displayModeStyle({ value, prefix: stylePrefix }),
91
+ ...spacingStyle,
92
+ },
93
+ additionalProps: {
94
+ action: {
95
+ identifier: actionIdentifier,
96
+ },
97
+ focusedStyles: {
98
+ backgroundColor: value(`${stylePrefix}_focused_background_color`),
99
+ borderColor: value(`${stylePrefix}_focused_border_color`),
100
+ },
101
+ testID,
102
+ },
103
+ elements: compact([
104
+ Asset({
105
+ prefix: stylePrefix,
106
+ value,
107
+ actionIdentifier,
108
+ testID,
109
+ }),
110
+ Spacer(),
111
+ TextLabelsContainer({
112
+ prefix: stylePrefix,
113
+ value,
114
+ actionIdentifier,
115
+ testID,
116
+ }),
117
+ ]),
118
+ data: [
119
+ {
120
+ func: (x) => x,
121
+ args: [],
122
+ propName: "item",
123
+ },
124
+ ],
125
+ };
126
+ };
@@ -0,0 +1,23 @@
1
+ type Props = {
2
+ style: Record<string, unknown>;
3
+ contentStyle: Record<string, unknown>;
4
+ elements: Array<Record<string, unknown>>;
5
+ };
6
+
7
+ export const ButtonContainerView = ({
8
+ style,
9
+ contentStyle,
10
+ elements,
11
+ }: Props) => {
12
+ return {
13
+ type: "View",
14
+ style,
15
+ elements: [
16
+ {
17
+ type: "View",
18
+ style: contentStyle,
19
+ elements,
20
+ },
21
+ ],
22
+ };
23
+ };
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ enabled?: boolean;
3
+ };
4
+
5
+ export const Spacer = ({ enabled = false }: Props = {}) => {
6
+ if (!enabled) {
7
+ return null;
8
+ }
9
+
10
+ return {
11
+ type: "View",
12
+ style: {
13
+ flex: 1,
14
+ },
15
+ };
16
+ };
@@ -0,0 +1,67 @@
1
+ import { Platform } from "react-native";
2
+
3
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
4
+
5
+ type Props = {
6
+ prefix: string;
7
+ value: Function;
8
+ actionIdentifier: string;
9
+ testID?: string;
10
+ };
11
+
12
+ export const TextLabel = ({
13
+ prefix,
14
+ value,
15
+ actionIdentifier,
16
+ testID,
17
+ }: Props) => {
18
+ if (!value(`${prefix}_label_enabled`)) {
19
+ return null;
20
+ }
21
+
22
+ return {
23
+ type: "Text",
24
+ style: {
25
+ color: value(`${prefix}_font_color`),
26
+ fontSize: toNumberWithDefaultZero(value(`${prefix}_font_size`)),
27
+ lineHeight: toNumberWithDefaultZero(value(`${prefix}_line_height`)),
28
+ marginTop: toNumberWithDefaultZero(value(`${prefix}_margin_top`)),
29
+ marginRight: toNumberWithDefaultZero(value(`${prefix}_margin_right`)),
30
+ marginBottom: toNumberWithDefaultZero(value(`${prefix}_margin_bottom`)),
31
+ marginLeft: toNumberWithDefaultZero(value(`${prefix}_margin_left`)),
32
+ fontFamily:
33
+ Platform.OS === "ios"
34
+ ? value(`${prefix}_ios_font_family`)
35
+ : value(`${prefix}_android_font_family`),
36
+ letterSpacing: toNumberWithDefaultZero(
37
+ Platform.OS === "ios"
38
+ ? value(`${prefix}_ios_letter_spacing`)
39
+ : value(`${prefix}_android_letter_spacing`)
40
+ ),
41
+ },
42
+ data: [
43
+ {
44
+ func: (entry) => entry,
45
+ args: [],
46
+ propName: "entry",
47
+ },
48
+ ],
49
+ additionalProps: {
50
+ label: {
51
+ context: actionIdentifier,
52
+ name: "label_1",
53
+ },
54
+ normalStyles: {
55
+ color: value(`${prefix}_font_color`),
56
+ },
57
+ focusedStyles: {
58
+ color: value(`${prefix}_focused_font_color`),
59
+ },
60
+ state: "default",
61
+ mobileActionRole: "label",
62
+ transformText: value(`${prefix}_text_transform`) || "default",
63
+ numberOfLines: value(`${prefix}_number_of_lines`),
64
+ testID: testID ? `${testID}-label` : undefined,
65
+ },
66
+ };
67
+ };
@@ -0,0 +1,32 @@
1
+ import { compact } from "@applicaster/zapp-react-native-utils/cellUtils";
2
+
3
+ import { TextLabel } from "./TextLabel";
4
+
5
+ type Props = {
6
+ prefix: string;
7
+ value: Function;
8
+ actionIdentifier: string;
9
+ testID?: string;
10
+ };
11
+
12
+ export const TextLabelsContainer = ({
13
+ prefix,
14
+ value,
15
+ actionIdentifier,
16
+ testID,
17
+ }: Props) => {
18
+ return {
19
+ type: "View",
20
+ additionalProps: {
21
+ mobileActionRole: "label_container",
22
+ },
23
+ elements: compact([
24
+ TextLabel({
25
+ prefix,
26
+ value,
27
+ actionIdentifier,
28
+ testID,
29
+ }),
30
+ ]),
31
+ };
32
+ };