@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9235607812 → 16.0.0-alpha.9338540010

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 (33) hide show
  1. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +12 -3
  2. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +1 -1
  3. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  4. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +2 -2
  5. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +2 -1
  6. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  7. package/Components/MasterCell/index.tsx +28 -4
  8. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  9. package/Components/MasterCell/utils/index.ts +18 -0
  10. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +22 -1
  11. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +52 -1
  12. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +77 -0
  13. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +58 -16
  14. package/Components/ModalComponent/BottomSheetModalContent.tsx +9 -74
  15. package/Components/ModalComponent/__tests__/showTextInput.test.ts +25 -0
  16. package/Components/ModalComponent/showTextInput.ts +2 -0
  17. package/Components/River/ComponentsMap/ComponentsMap.tsx +4 -1
  18. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  19. package/Components/Transitioner/AnimationManager.js +1 -1
  20. package/Components/Transitioner/Scene.tsx +4 -3
  21. package/Components/Transitioner/Transitioner.tsx +69 -30
  22. package/Components/Transitioner/__tests__/transitioner.test.js +308 -20
  23. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  24. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  25. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  26. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  27. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  28. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  29. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  30. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  31. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  32. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  33. package/package.json +5 -5
@@ -90,6 +90,7 @@ type ModalComponentProps = {
90
90
  /** Optional body that replaces the items list (header stays DefaultModalHeader). */
91
91
  contentComponent?: React.ComponentType<any>;
92
92
  contentComponentProps?: Record<string, any>;
93
+ themePluginId?: string;
93
94
  styleOverrides?: ModalBlocksConfig | Record<string, any>;
94
95
  measurementPhase?: boolean;
95
96
  /** Copy under Next Up when the queue has no upcoming items. */
@@ -115,7 +116,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
115
116
  iconPlacement,
116
117
  contentComponent: ContentComponent,
117
118
  contentComponentProps = {},
118
- styleOverrides: propStyleOverrides,
119
+ themePluginId: themePluginIdProp,
119
120
  measurementPhase = false,
120
121
  emptyMessage,
121
122
  } = props;
@@ -140,12 +141,14 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
140
141
  role,
141
142
  behavior,
142
143
  editableCollection,
143
- themePluginId,
144
+ themePluginId: themePluginIdFromSheet,
144
145
  } = useBottomSheetContent({
145
146
  onPress,
146
147
  viewModel,
147
148
  });
148
149
 
150
+ const themePluginId = themePluginIdFromSheet || themePluginIdProp;
151
+
149
152
  const plugins = usePlugins();
150
153
 
151
154
  const plugin = useMemo(
@@ -157,78 +160,10 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
157
160
  const configuration =
158
161
  plugin?.configuration || (plugin as any)?.configuration_json;
159
162
 
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]);
163
+ return configuration && themePluginId
164
+ ? parseModalBlocksConfiguration(configuration, themePluginId)
165
+ : null;
166
+ }, [plugin, themePluginId]);
232
167
 
233
168
  const pluginLocalizations = useLocalizedStrings({
234
169
  localizations:
@@ -92,4 +92,29 @@ describe("showTextInputAction", () => {
92
92
  expect(result).toBe(ActionResult.Error);
93
93
  expect(actionExecutor.handleAction).not.toHaveBeenCalled();
94
94
  });
95
+
96
+ it("forwards themePluginId so the sheet can read plugin button styles", async () => {
97
+ const action = {
98
+ type: "showTextInput",
99
+ options: {
100
+ headerTitle: "Create New Playlist",
101
+ inputLabel: "Name your playlist",
102
+ buttonLabel: "Create",
103
+ themePluginId: "playlist-experience",
104
+ actions: [{ type: "sendCloudEvent", options: {} }],
105
+ },
106
+ };
107
+
108
+ await showTextInputAction(action as any);
109
+
110
+ expect(actionExecutor.handleAction).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ type: "openBottomSheet",
113
+ options: expect.objectContaining({
114
+ themePluginId: "playlist-experience",
115
+ }),
116
+ }),
117
+ undefined
118
+ );
119
+ });
95
120
  });
@@ -21,6 +21,7 @@ export async function showTextInputAction(
21
21
  validationLabel,
22
22
  actions,
23
23
  maxLength,
24
+ themePluginId,
24
25
  } = action.options || {};
25
26
 
26
27
  if (!headerTitle || !buttonLabel || !actions || !actions.length) {
@@ -34,6 +35,7 @@ export async function showTextInputAction(
34
35
  header: {
35
36
  title: headerTitle,
36
37
  },
38
+ ...(themePluginId ? { themePluginId } : {}),
37
39
  content: {
38
40
  component: TextInputContent,
39
41
  props: {
@@ -118,7 +118,10 @@ function ComponentsMapComponent(props: Props) {
118
118
  };
119
119
 
120
120
  return (
121
- <ScreenLoadingMeasurementsListItemWrapper index={index}>
121
+ <ScreenLoadingMeasurementsListItemWrapper
122
+ index={index}
123
+ testID={`component-${item.id}`}
124
+ >
122
125
  <RiverItem {...riverItemProps} />
123
126
  </ScreenLoadingMeasurementsListItemWrapper>
124
127
  );
@@ -213,6 +213,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
213
213
  <View
214
214
  onLayout={[Function]}
215
215
  style={{}}
216
+ testID="component-A1"
216
217
  >
217
218
  <View />
218
219
  </View>
@@ -225,6 +226,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
225
226
  <View
226
227
  onLayout={[Function]}
227
228
  style={{}}
229
+ testID="component-A2"
228
230
  />
229
231
  </View>
230
232
  <View
@@ -66,7 +66,7 @@ export class AnimationManager {
66
66
  *
67
67
  * @param {object} to contains style to animate and scenes index
68
68
  * @param {string} action push|back
69
- * @param {number} stepsBack number of steps back
69
+ * @param {number} [stepsBack] number of steps back, used by the back action only
70
70
  */
71
71
  stateForAction({ to, scenes }, action, stepsBack) {
72
72
  let rootIndex = to.index;
@@ -17,13 +17,14 @@ const fullWidthDimensions = {
17
17
  type Props = {
18
18
  children: any;
19
19
  style: any;
20
- pointerEvents: ViewProps["pointerEvents"];
21
- overlayStyle: ViewStyle;
22
- animating: boolean;
20
+ pointerEvents?: ViewProps["pointerEvents"];
21
+ overlayStyle?: ViewStyle;
22
+ animating?: boolean;
23
23
  contentStyle: ViewStyle;
24
24
  pathname: string;
25
25
  isActive: boolean;
26
26
  screenData: NavigationScreenData;
27
+ screenUniqueId?: string;
27
28
  };
28
29
 
29
30
  export function CurrentScreenContextProvider({
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { View } from "react-native";
3
- import { append, compose, identity, tail, take, update } from "ramda";
3
+ import { append, compose, identity, tail, update } from "ramda";
4
4
 
5
5
  import { AnimationManager } from "./AnimationManager";
6
6
  import { Scene } from "./Scene";
@@ -18,11 +18,23 @@ export const NAV_ACTION_REPLACE = "REPLACE";
18
18
 
19
19
  export const NAV_ACTION_BACK = "POP";
20
20
 
21
- type SceneProps = {
22
- children: React.ReactNode;
23
- overlayStyle: any;
24
- style: any;
25
- animating: boolean;
21
+ const RIVER_SEGMENT_REGEX = /river\//g;
22
+
23
+ // Number of "river/" segments in the route, i.e. its depth in the navigation stack
24
+ function countRiverSegments(route?: string): number {
25
+ return (route?.match(RIVER_SEGMENT_REGEX) || []).length;
26
+ }
27
+
28
+ // The screen element rendered for the current route
29
+ type SceneChild = React.ReactElement<{
30
+ pathname: string;
31
+ screenData: NavigationScreenData;
32
+ }>;
33
+
34
+ type TransitionerScene = {
35
+ children: SceneChild;
36
+ key: string;
37
+ screenUniqueId: string;
26
38
  };
27
39
 
28
40
  /**
@@ -31,7 +43,7 @@ type SceneProps = {
31
43
  * @param {Object} children
32
44
  * @returns { children: Object, key: string }
33
45
  */
34
- function prepareScene(children) {
46
+ function prepareScene(children: SceneChild): TransitionerScene {
35
47
  return {
36
48
  children,
37
49
  key: children.props.pathname,
@@ -45,7 +57,10 @@ function prepareScene(children) {
45
57
  * @param {any} props Component's props
46
58
  * @param {any} state Component's state
47
59
  */
48
- function scenesForBackTransition(props: SceneProps, { scenes }) {
60
+ function scenesForBackTransition(
61
+ props: Pick<Props, "children">,
62
+ { scenes }: Pick<State, "scenes">
63
+ ): TransitionerScene[] {
49
64
  return scenes.length > 1
50
65
  ? identity(scenes)
51
66
  : [prepareScene(props.children), scenes[0]];
@@ -53,7 +68,8 @@ function scenesForBackTransition(props: SceneProps, { scenes }) {
53
68
 
54
69
  type Props = {
55
70
  transitionConfig: any;
56
- children: React.ReactNode;
71
+ isRTL?: boolean;
72
+ children: SceneChild;
57
73
  navigator: {
58
74
  previousAction: string;
59
75
  currentRoute: string;
@@ -78,13 +94,12 @@ type Props = {
78
94
  };
79
95
 
80
96
  type State = {
81
- animatedValue: any;
82
97
  animating: boolean;
83
98
  duration: number;
84
99
  easing: any;
85
100
  from: any;
86
101
  to: any;
87
- scenes: any;
102
+ scenes: TransitionerScene[];
88
103
  };
89
104
 
90
105
  const styles = {
@@ -92,11 +107,18 @@ const styles = {
92
107
  } as const;
93
108
 
94
109
  export class TransitionerComponent extends React.PureComponent<Props, State> {
110
+ private animationManager: AnimationManager;
111
+
112
+ private transitionId: number;
113
+
95
114
  constructor(props: Props) {
96
115
  super(props);
97
116
 
98
117
  this.animationManager = new AnimationManager(props);
99
118
 
119
+ // Bumped by every animated transition; end callbacks of older ones are ignored
120
+ this.transitionId = 0;
121
+
100
122
  this.state = {
101
123
  scenes: [prepareScene(props.children)], // starting with index: 0
102
124
  ...this.animationManager.initialState(),
@@ -113,28 +135,36 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
113
135
  * and trigger push animation with callbacks.
114
136
  */
115
137
  pushTransitionStart() {
138
+ const transitionId = ++this.transitionId;
139
+
116
140
  this.setState(
117
141
  {
118
- scenes: append(prepareScene(this.props.children), this.state.scenes),
142
+ // Append right after the visible scene: stateForAction(PUSH) animates to.index + 1
143
+ scenes: [
144
+ ...this.state.scenes.slice(0, this.state.to.index + 1),
145
+ prepareScene(this.props.children),
146
+ ],
119
147
  ...this.animationManager.stateForAction(this.state, NAV_ACTION_PUSH),
120
148
  },
121
149
  () => {
122
- this.animationManager.animate(this.state, this.pushTransitionEnd);
150
+ this.animationManager.animate(this.state, () =>
151
+ this.pushTransitionEnd(transitionId)
152
+ );
123
153
  }
124
154
  );
125
155
  }
126
156
 
127
157
  /**
128
158
  * Trigger back animation.
129
- * The last scene in the array is removed in the "backTransitionEnd" callback,
159
+ * The scenes after the target one are removed in the "backTransitionEnd" callback,
130
160
  * only after the animation is complete.
131
161
  */
132
162
  backTransitionStart(previousRoute: string) {
163
+ const transitionId = ++this.transitionId;
133
164
  const currentRoute = this.props.navigator?.currentRoute;
134
165
 
135
166
  const diffLengthRoutes =
136
- (previousRoute?.match(/river\//g) || []).length -
137
- (currentRoute?.match(/river\//g) || []).length;
167
+ countRiverSegments(previousRoute) - countRiverSegments(currentRoute);
138
168
 
139
169
  const stepsBack =
140
170
  diffLengthRoutes && diffLengthRoutes > 0 ? diffLengthRoutes : 1;
@@ -150,30 +180,32 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
150
180
  },
151
181
  () => {
152
182
  this.animationManager.animate(this.state, () =>
153
- this.backTransitionEnd(stepsBack)
183
+ this.backTransitionEnd(transitionId)
154
184
  );
155
185
  }
156
186
  );
157
187
  }
158
188
 
159
- shouldSkipAnimationForRoute(route) {
189
+ shouldSkipAnimationForRoute(route: string) {
160
190
  // We are skipping animations for some routes, likes hooks & the player mainly
161
191
 
162
192
  return route.includes("/playable") || route.includes("/hook");
163
193
  }
164
194
 
165
- shouldResetStateForRoute(route) {
195
+ shouldResetStateForRoute(route: string) {
166
196
  // We are resetting the transitioner state for root routes
167
197
 
168
- return (route?.match(/river\//g) || []).length === 1;
198
+ return countRiverSegments(route) === 1;
169
199
  }
170
200
 
171
- transitionWithoutAnimation(resetState) {
172
- const { scenes } = this.state;
201
+ transitionWithoutAnimation(resetState?: boolean) {
202
+ // Scenes after the visible one are only leaving (back animation in flight),
203
+ // so the new scene replaces the one being backed to
204
+ const scenes = this.state.scenes.slice(0, this.state.to.index + 1);
173
205
  const newScene = prepareScene(this.props.children);
174
206
  const isLastSceneHook = scenes[scenes.length - 1].key.startsWith("/hook");
175
207
 
176
- let newState;
208
+ let newState: Pick<State, "scenes"> | Pick<State, "scenes" | "to">;
177
209
 
178
210
  if (resetState) {
179
211
  newState = {
@@ -263,20 +295,27 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
263
295
  }
264
296
  }
265
297
 
266
- // Sets state.animating to false
267
- pushTransitionEnd() {
298
+ // Sets state.animating to false,
299
+ // unless a newer transition has started since this one
300
+ pushTransitionEnd(transitionId: number) {
301
+ if (transitionId !== this.transitionId) return;
302
+
268
303
  this.setState({
269
304
  animating: false,
270
305
  });
271
306
  }
272
307
 
273
308
  // Sets state.animating to false
274
- // and remove last scene from scenes stack
275
- backTransitionEnd(stepsBack) {
276
- this.setState({
309
+ // and drops every scene after the target one. Reads the latest state,
310
+ // so it's correct however many backs were merged into this animation.
311
+ // Ignored if a newer transition has started since this one
312
+ backTransitionEnd(transitionId: number) {
313
+ if (transitionId !== this.transitionId) return;
314
+
315
+ this.setState(({ scenes, to }) => ({
277
316
  animating: false,
278
- scenes: take(this.state.scenes.length - stepsBack, this.state.scenes),
279
- });
317
+ scenes: scenes.slice(0, to.index + 1),
318
+ }));
280
319
  }
281
320
 
282
321
  // On initial state, both "to" and "from" point to a single scene