@applicaster/zapp-react-native-ui-components 12.0.0-alpha.4220424021 → 12.0.0-alpha.4223432180

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 (32) hide show
  1. package/Components/Cell/Cell.tsx +8 -1
  2. package/Components/Focusable/__tests__/index.android.test.tsx +72 -0
  3. package/Components/Focusable/index.android.tsx +15 -9
  4. package/Components/FocusableList/FocusableChild.ts +2 -4
  5. package/Components/FocusableList/index.tsx +11 -12
  6. package/Components/FocusableScrollView/index.tsx +1 -1
  7. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +12 -0
  8. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +6 -5
  9. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +2 -2
  10. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +1 -1
  11. package/Components/PlayerContainer/PlayerContainer.tsx +14 -8
  12. package/Components/Transitioner/Transitioner.tsx +13 -3
  13. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +5 -0
  14. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +2 -1
  15. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +11 -3
  16. package/Components/VideoModal/PlayerDetails.tsx +16 -2
  17. package/Components/VideoModal/PlayerWrapper.tsx +40 -10
  18. package/Components/VideoModal/VideoModal.tsx +33 -27
  19. package/Components/VideoModal/__tests__/PlayerDetails.test.tsx +7 -0
  20. package/Components/VideoModal/__tests__/__snapshots__/PlayerDetails.test.tsx.snap +2 -0
  21. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +56 -10
  22. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +4 -1
  23. package/Components/ZappFrameworkComponents/BarView/Buttons/BackButton.tsx +7 -2
  24. package/Components/ZappFrameworkComponents/BarView/Buttons/CloseButton.tsx +7 -2
  25. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +87 -0
  26. package/Components/ZappFrameworkComponents/Buttons/ActionButton.tsx +9 -4
  27. package/Contexts/CachedDimensionsContext/index.ts +13 -9
  28. package/Contexts/ConfirmDialogState/index.ts +1 -1
  29. package/Contexts/ScreenContext/index.tsx +6 -1
  30. package/const.ts +6 -0
  31. package/index.js +3 -0
  32. package/package.json +5 -5
@@ -276,13 +276,20 @@ export class CellComponent extends React.Component<Props, State> {
276
276
  skipFocusManagerRegistration={skipFocusManagerRegistration}
277
277
  >
278
278
  {(focused, event) => {
279
+ const isFocused = this.isCellFocused(focused);
280
+
281
+ if (isFocused) {
282
+ const accessibilityManager = AccessibilityManager.getInstance();
283
+ accessibilityManager.readText({ text: item.title });
284
+ }
285
+
279
286
  return (
280
287
  <FocusableCell
281
288
  {...{
282
289
  index,
283
290
  CellRenderer,
284
291
  item,
285
- focused: this.isCellFocused(focused),
292
+ focused: isFocused,
286
293
  scrollTo: this.scrollToIndex(event),
287
294
  behavior,
288
295
  }}
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ import { render, act } from "@testing-library/react-native";
3
+ import { Touchable } from "../../Touchable";
4
+
5
+ const mockUseIsRTL = jest.fn();
6
+
7
+ jest.mock("@applicaster/zapp-react-native-utils/localizationUtils", () => ({
8
+ useIsRTL: mockUseIsRTL,
9
+ }));
10
+
11
+ const mockUseFocusManager = jest.fn();
12
+
13
+ jest.mock("@applicaster/zapp-react-native-utils/focusManager", () => ({
14
+ useFocusManager: mockUseFocusManager,
15
+ }));
16
+
17
+ const mockUsePrevious = jest.fn();
18
+
19
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => ({
20
+ usePrevious: mockUsePrevious,
21
+ }));
22
+
23
+ const { Focusable } = require("../index.android");
24
+
25
+ describe("Focusable", () => {
26
+ const mockFocusManager = {
27
+ isFocused: jest.fn(),
28
+ onDisableFocusChange: jest.fn(),
29
+ registerFocusable: jest.fn(),
30
+ focusedId: "test-id",
31
+ };
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ mockUseIsRTL.mockReturnValue(false);
36
+ mockUseFocusManager.mockReturnValue(mockFocusManager);
37
+ mockUsePrevious.mockReturnValue(false);
38
+ });
39
+
40
+ it("updates disableFocus state when disableFocus prop changes", () => {
41
+ const { rerender } = render(
42
+ <Focusable id="test-id" disableFocus={false}>
43
+ <Touchable testID="touchable" />
44
+ </Focusable>
45
+ );
46
+
47
+ rerender(
48
+ <Focusable id="test-id" disableFocus={true}>
49
+ <Touchable testID="touchable" />
50
+ </Focusable>
51
+ );
52
+
53
+ expect(mockFocusManager.onDisableFocusChange).toHaveBeenCalledWith(
54
+ "test-id"
55
+ );
56
+ });
57
+
58
+ it("calls focusManager.registerFocusable on mount and unregisters on unmount", () => {
59
+ const unregister = jest.fn();
60
+ mockFocusManager.registerFocusable.mockReturnValue(unregister);
61
+
62
+ const { unmount } = render(
63
+ <Focusable id="test-id">
64
+ <Touchable testID="touchable" />
65
+ </Focusable>
66
+ );
67
+
68
+ expect(mockFocusManager.registerFocusable).toHaveBeenCalled();
69
+ act(() => unmount());
70
+ expect(unregister).toHaveBeenCalled();
71
+ });
72
+ });
@@ -128,7 +128,10 @@ function FocusableComponent(props: Props, forwardedRef) {
128
128
 
129
129
  const _onBlur = React.useCallback(
130
130
  (ref, options = {}) => {
131
- setFocused(false);
131
+ if (ref.props.id !== focusManager.focusedId) {
132
+ setFocused(false);
133
+ }
134
+
132
135
  onBlur?.(ref, options);
133
136
  },
134
137
  [onBlur]
@@ -148,15 +151,14 @@ function FocusableComponent(props: Props, forwardedRef) {
148
151
  [isRTL, nextFocusDown, nextFocusUp, nextFocusLeft, nextFocusRight]
149
152
  );
150
153
 
151
- const contextState = React.useMemo(
152
- () => ({
154
+ const contextState = React.useMemo(() => {
155
+ return {
153
156
  focused,
154
157
  ...parentFocus,
155
158
  ref: touchableRef,
156
159
  setIsFocusable,
157
- }),
158
- [parentFocus, focused]
159
- );
160
+ };
161
+ }, [parentFocus, focused]);
160
162
 
161
163
  return (
162
164
  <Touchable
@@ -173,9 +175,13 @@ function FocusableComponent(props: Props, forwardedRef) {
173
175
  {...parentFocus}
174
176
  >
175
177
  <FocusableContext.Provider value={contextState}>
176
- {typeof children === "function"
177
- ? children(focused, parentFocus, touchableRef)
178
- : children}
178
+ {React.useMemo(
179
+ () =>
180
+ typeof children === "function"
181
+ ? children(focused, parentFocus, touchableRef)
182
+ : children,
183
+ [children, focused, parentFocus]
184
+ )}
179
185
  </FocusableContext.Provider>
180
186
  </Touchable>
181
187
  );
@@ -1,7 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { isEqual } from "@applicaster/zapp-react-native-utils/equalUtils";
3
3
  import { useFocusable } from "../Focusable/index.android";
4
- import { useCellState } from "./hooks/useCellState.android";
5
4
 
6
5
  /**
7
6
  * returns parent focus from prop or from context
@@ -30,15 +29,14 @@ const useParentFocus = (
30
29
  );
31
30
 
32
31
  function FocusableChildComponent(props) {
33
- const { setIsFocusable, ...focusableContext } = useFocusable();
32
+ const { setIsFocusable, focused, ...focusableContext } = useFocusable();
33
+
34
34
  const parentFocus = useParentFocus(props, focusableContext);
35
35
 
36
36
  const onLoadFailed = React.useCallback(() => {
37
37
  setIsFocusable(false);
38
38
  }, []);
39
39
 
40
- const focused = useCellState(props.id);
41
-
42
40
  return props.renderItem({
43
41
  ...props,
44
42
  focused,
@@ -88,6 +88,7 @@ function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
88
88
  data = [],
89
89
  // eslint-disable-next-line unused-imports/no-unused-vars
90
90
  onAllComponentsLoaded, // TODO: re-implement it
91
+ omitPropsPropagation = [],
91
92
  } = props;
92
93
 
93
94
  useCheckItemIdsForUnique({ componentId: props.id, items: data });
@@ -252,9 +253,9 @@ function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
252
253
  ]
253
254
  );
254
255
 
255
- const flatListProps = React.useMemo(
256
- () =>
257
- R.omit([
256
+ const getFlatListProps = React.useCallback(
257
+ R.omit(
258
+ [
258
259
  "onListElementFocus",
259
260
  "onListElementPress",
260
261
  "onListElementPressOut",
@@ -267,17 +268,17 @@ function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
267
268
  "focusableItemProps",
268
269
  "renderItem",
269
270
  "extraChildrenData",
270
- "extraData",
271
- "keyExtractor",
272
271
  "onAllComponentsLoaded",
273
272
  "focused",
274
273
  "id",
275
274
  "initialFocusDirection",
276
275
  "withStateMemory",
277
276
  "useSequentialLoading",
278
- ...(props.omitPropsPropagation || []),
279
- ])(props),
280
- [props]
277
+ ...omitPropsPropagation,
278
+ ],
279
+ R.__
280
+ ),
281
+ [omitPropsPropagation]
281
282
  );
282
283
 
283
284
  const onEndReached = React.useCallback(
@@ -289,7 +290,7 @@ function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
289
290
 
290
291
  const initialNumToRender = toFiniteNumberWithDefault(
291
292
  10,
292
- flatListProps.initialNumToRender
293
+ props.initialNumToRender
293
294
  );
294
295
 
295
296
  return (
@@ -298,12 +299,10 @@ function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
298
299
  <FlatList
299
300
  focusable={false}
300
301
  scrollEnabled={false}
301
- extraData={props.extraData}
302
302
  ref={ref}
303
- {...flatListProps}
303
+ {...getFlatListProps(props)}
304
304
  renderItem={renderItem}
305
305
  onEndReached={onEndReached}
306
- keyExtractor={props.keyExtractor}
307
306
  initialNumToRender={initialNumToRender}
308
307
  />
309
308
  </ChildrenFocusDeactivatorView>
@@ -284,7 +284,7 @@ function FocusableScrollViewComponent(props: Props, ref) {
284
284
  };
285
285
 
286
286
  return (
287
- <View onLayout={onItemLayout}>
287
+ <View key={id} onLayout={onItemLayout}>
288
288
  <Focusable
289
289
  {...nextFocus}
290
290
  onFocus={(element, direction) =>
@@ -5,6 +5,7 @@ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/acti
5
5
  import * as R from "ramda";
6
6
  import { getXray } from "@applicaster/zapp-react-native-utils/logger";
7
7
  import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
8
+ import { useAccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
8
9
 
9
10
  const { Logger } = getXray();
10
11
 
@@ -44,6 +45,11 @@ export function FocusableView({ style, children, item, ...otherProps }: Props) {
44
45
 
45
46
  const actionContext = useActions(pluginIdentifier);
46
47
 
48
+ const accessibilityManager = useAccessibilityManager({});
49
+
50
+ const { ttsLabel = "" } =
51
+ actionContext?.initialEntryState(item)?.getAccessibility?.(item) || {};
52
+
47
53
  const onPress = (event) => {
48
54
  event?.preventDefault?.();
49
55
 
@@ -88,6 +94,12 @@ export function FocusableView({ style, children, item, ...otherProps }: Props) {
88
94
  mouse: focusable.mouse,
89
95
  });
90
96
  });
97
+
98
+ if (ttsLabel) {
99
+ accessibilityManager.readText({
100
+ text: ttsLabel,
101
+ });
102
+ }
91
103
  };
92
104
 
93
105
  const handleBlur = (_focusable, _direction) => {
@@ -38,21 +38,22 @@ function Image({
38
38
 
39
39
  const shouldRenderImg = source && !error;
40
40
 
41
+ // Default placeholder image fallback is empty string because it won't rerender
42
+ // in case of undefined or {uri: undefined} source
43
+ // https://github.com/facebook/react-native/issues/9195
41
44
  const defaultPlaceHolderImage = React.useMemo(
42
45
  () => ({ uri: placeholderImage || "" }),
43
46
  [source, error]
44
47
  );
45
48
 
46
- const _source = shouldRenderImg
47
- ? withDimensions(source)
48
- : defaultPlaceHolderImage;
49
-
50
49
  return (
51
50
  // @ts-ignore
52
51
  <MemoizedImage
53
52
  style={style as ImageStyle}
54
53
  onError={() => setErrorState(true)}
55
- source={_source?.uri ? _source : undefined}
54
+ source={
55
+ shouldRenderImg ? withDimensions(source) : defaultPlaceHolderImage
56
+ }
56
57
  {...R.omit(["source"], otherProps)}
57
58
  />
58
59
  );
@@ -31,10 +31,10 @@ function Image({
31
31
 
32
32
  return (
33
33
  <RnImage
34
- defaultSource={placeholderImage || ""}
34
+ defaultSource={placeholderImage || null}
35
35
  style={style}
36
36
  source={updatedSource}
37
- {...R.omit(["source"], otherProps)}
37
+ {...R.omit(["source", "placeholderImage"], otherProps)}
38
38
  />
39
39
  );
40
40
  }
@@ -39,6 +39,6 @@ describe("image with no source", () => {
39
39
  );
40
40
 
41
41
  const testInstance = testRenderer.root;
42
- expect(testInstance.findByType(Image).props.source).toBeUndefined();
42
+ expect(testInstance.findByType(Image).props.source).toEqual({ uri: "" });
43
43
  });
44
44
  });
@@ -362,13 +362,15 @@ const PlayerContainerComponent = (props: Props) => {
362
362
  const toggleFullscreen = (event) => {
363
363
  const { fullscreen = false } = event || {};
364
364
 
365
- if (fullscreen) {
366
- playerEvent("onFullscreenPlayerWillPresent");
367
- navigator.fullscreenVideoModal();
368
- } else {
369
- playerEvent("onFullscreenPlayerWillDismiss");
370
- navigator.maximiseVideoModal();
371
- }
365
+ setTimeout(() => {
366
+ if (fullscreen) {
367
+ navigator.fullscreenVideoModal();
368
+ playerEvent("onFullscreenPlayerWillPresent");
369
+ } else {
370
+ navigator.maximiseVideoModal();
371
+ playerEvent("onFullscreenPlayerWillDismiss");
372
+ }
373
+ });
372
374
  };
373
375
 
374
376
  const onCellTap = (entry) => {
@@ -463,6 +465,10 @@ const PlayerContainerComponent = (props: Props) => {
463
465
 
464
466
  if (mode === VideoModalMode.FULLSCREEN) {
465
467
  toggleFullscreen({ fullscreen: false });
468
+
469
+ if (!isModal) {
470
+ playerManager.closeNativePlayer();
471
+ }
466
472
  }
467
473
 
468
474
  return !!(
@@ -519,7 +525,7 @@ const PlayerContainerComponent = (props: Props) => {
519
525
  }
520
526
 
521
527
  return () => {
522
- playerEvent("player_did_close");
528
+ playerEvent("player_did_close", { item });
523
529
 
524
530
  if (!isModal) {
525
531
  showNavBar(true);
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { View } from "react-native";
3
- import { update, compose, tail, append, take, identity } from "ramda";
3
+ import { update, compose, tail, append, take, identity, isNil } from "ramda";
4
4
 
5
5
  import { AnimationManager } from "./AnimationManager";
6
6
  import { Scene } from "./Scene";
@@ -50,6 +50,10 @@ function scenesForBackTransition(props: SceneProps, { scenes }) {
50
50
  : [prepareScene(props.children), scenes[0]];
51
51
  }
52
52
 
53
+ const isModalClosed = (navigator) => {
54
+ return !!isNil(navigator?.modalData);
55
+ };
56
+
53
57
  type Props = {
54
58
  transitionConfig: any;
55
59
  children: React.ReactNode;
@@ -313,7 +317,10 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
313
317
  <Scene
314
318
  key={scenes[from.index].key}
315
319
  pathname={scenes[from.index].key}
316
- isActive={scenes[from.index].key === this.props.navigator.currentRoute}
320
+ isActive={
321
+ scenes[from.index].key === this.props.navigator.currentRoute &&
322
+ isModalClosed(this.props.navigator)
323
+ }
317
324
  style={from.style}
318
325
  pointerEvents="none"
319
326
  overlayStyle={from.overlayStyle}
@@ -329,7 +336,10 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
329
336
  const toScene = scenes[to.index] && (
330
337
  <Scene
331
338
  pathname={scenes[to.index].key}
332
- isActive={scenes[to.index].key === this.props.navigator.currentRoute}
339
+ isActive={
340
+ scenes[to.index].key === this.props.navigator.currentRoute &&
341
+ isModalClosed(this.props.navigator)
342
+ }
333
343
  key={scenes[to.index].key}
334
344
  style={to.style}
335
345
  overlayStyle={to.overlayStyle}
@@ -31,6 +31,11 @@ exports[`PlayerLiveImageComponent should render correctly with default props 1`]
31
31
  playerId="player1"
32
32
  resizeMode="cover"
33
33
  screenConfig={Object {}}
34
+ source={
35
+ Object {
36
+ "uri": "",
37
+ }
38
+ }
34
39
  style={Object {}}
35
40
  />
36
41
  </View>
@@ -223,7 +223,8 @@ export const AnimatedScrollModalComponent = ({ children }: Props) => {
223
223
  (videoModalState.mode === "MAXIMIZED" &&
224
224
  videoModalState.previousMode === "MINIMIZED" &&
225
225
  previousItemId === videoModalState.item?.id) ||
226
- previousItemId !== videoModalState.item?.id
226
+ (previousItemId !== videoModalState.item?.id &&
227
+ videoModalState.mode !== "FULLSCREEN")
227
228
  ) {
228
229
  setPlayerAnimationState(PlayerAnimationStateEnum.maximaze);
229
230
  setTimeout(() => resetPlayerAnimationState());
@@ -60,7 +60,10 @@ export const AnimationView = ({
60
60
  const {
61
61
  width: screenWidth,
62
62
  deviceInfo: { isTablet, isTabletLandscape },
63
- } = useDimensions("window", { deviceInfo: true });
63
+ } = useDimensions("window", {
64
+ deviceInfo: true,
65
+ updateForInactiveScreens: true,
66
+ });
64
67
 
65
68
  const { videoModalState } = useNavigation();
66
69
  const insets = useSafeAreaInsets();
@@ -301,13 +304,18 @@ export const AnimationView = ({
301
304
  typeof additionalData.height === "number" && additionalData.height;
302
305
  }
303
306
 
304
- if (defaultValue.x === 0 && width && height) {
307
+ if (
308
+ videoModalState.mode === "MAXIMIZED" &&
309
+ defaultValue.x !== screenWidth &&
310
+ width &&
311
+ height
312
+ ) {
305
313
  const value = { x: width, y: height };
306
314
  setDefaultValue(value);
307
315
  animatedValue.setValue(value);
308
316
  }
309
317
  }
310
- }, [additionalData, screenWidth]);
318
+ }, [screenWidth]);
311
319
 
312
320
  React.useEffect(() => {
313
321
  if (
@@ -1,9 +1,17 @@
1
1
  import React, { useEffect, useRef } from "react";
2
- import { StyleProp, ViewStyle, Animated, Easing } from "react-native";
2
+ import {
3
+ StyleProp,
4
+ ViewStyle,
5
+ Animated,
6
+ Easing,
7
+ Dimensions,
8
+ } from "react-native";
3
9
  import { useTargetScreenData } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
4
10
  import { ComponentsMap } from "@applicaster/zapp-react-native-ui-components/Components/River/ComponentsMap";
5
11
  import { useSafeAreaInsets } from "react-native-safe-area-context";
6
12
 
13
+ const { width: SCREEN_WIDTH } = Dimensions.get("screen");
14
+
7
15
  type Configuration = {
8
16
  [key: string]: any;
9
17
  modal_background_color?: string;
@@ -15,6 +23,7 @@ type Props = {
15
23
  configuration: Configuration;
16
24
  isTabletLandscape: boolean;
17
25
  isAudioPlayer?: boolean;
26
+ isTablet: boolean;
18
27
  };
19
28
 
20
29
  const containerStyle = ({
@@ -31,6 +40,7 @@ export const PlayerDetails = ({
31
40
  configuration,
32
41
  isTabletLandscape,
33
42
  isAudioPlayer,
43
+ isTablet,
34
44
  }: Props) => {
35
45
  const screenData = useTargetScreenData(entry);
36
46
  const insets = useSafeAreaInsets();
@@ -77,7 +87,11 @@ export const PlayerDetails = ({
77
87
  transform: [{ translateY }],
78
88
  opacity,
79
89
  },
80
- // isAudioPlayer && audioMarginStyle,
90
+ {
91
+ // workaround for avoid wrong text-height after going back to portrait rotation
92
+ // we don't see this view in landscape mode, so we are able to use fixed width from portrait mode
93
+ width: isTablet ? undefined : SCREEN_WIDTH,
94
+ },
81
95
  ]}
82
96
  >
83
97
  {screenData && (
@@ -7,7 +7,6 @@ import {
7
7
  ViewStyle,
8
8
  } from "react-native";
9
9
  import { Edge, SafeAreaView } from "react-native-safe-area-context";
10
-
11
10
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
12
11
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
13
12
  import { PlayerDetails } from "./PlayerDetails";
@@ -23,6 +22,8 @@ import {
23
22
  ComponentAnimationType,
24
23
  } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
25
24
 
25
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("screen");
26
+
26
27
  type DimensionsT = {
27
28
  width: number | string;
28
29
  height: number | string | undefined;
@@ -69,10 +70,8 @@ const directionStyles = (isTabletLandscape: boolean): ViewStyle => {
69
70
  };
70
71
 
71
72
  const getScreenAspectRatio = () => {
72
- const { width, height } = Dimensions.get("screen");
73
-
74
- const longEdge = Math.max(width, height);
75
- const shortEdge = Math.min(width, height);
73
+ const longEdge = Math.max(SCREEN_WIDTH, SCREEN_HEIGHT);
74
+ const shortEdge = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
76
75
 
77
76
  return longEdge / shortEdge;
78
77
  };
@@ -125,10 +124,37 @@ const getTabletWidth = (
125
124
  return Number(width) - sidebarWidth;
126
125
  };
127
126
 
128
- export const PlayerWrapper = (props: Props) => {
127
+ const getStyles = (baseStyle, isMobile, isPortrait) => {
128
+ if (isMobile && isPortrait) {
129
+ return { width: SCREEN_WIDTH, height: SCREEN_HEIGHT };
130
+ }
131
+
132
+ if (isMobile && !isPortrait) {
133
+ return { width: SCREEN_HEIGHT, height: SCREEN_WIDTH };
134
+ }
135
+
136
+ return baseStyle;
137
+ };
138
+
139
+ const showDetails = (isMobile: boolean, docked: boolean): boolean => {
140
+ // for mobile we always show details. Mounting of it very heavy operation.
141
+ if (isMobile) {
142
+ return true;
143
+ }
144
+
145
+ // workaround for tablets we need to mount/unmount in order to avoid PlayerDetails content dissappearing
146
+ // FIXME - find out the reason of dissapearing of content
147
+ if (docked) {
148
+ return false;
149
+ }
150
+
151
+ return true;
152
+ };
153
+
154
+ const PlayerWrapperComponent = (props: Props) => {
129
155
  const {
130
156
  entry,
131
- style,
157
+ style: baseStyle,
132
158
  containerStyle,
133
159
  inline,
134
160
  docked,
@@ -143,6 +169,8 @@ export const PlayerWrapper = (props: Props) => {
143
169
 
144
170
  const isInlineModal = inline && isModal;
145
171
 
172
+ const style = getStyles(baseStyle, !isTablet, isInlineModal);
173
+
146
174
  const isTabletLandscape = !isTV() && isTablet && !isTabletPortrait;
147
175
 
148
176
  const tabletWidth = getTabletWidth(configuration, style);
@@ -224,8 +252,9 @@ export const PlayerWrapper = (props: Props) => {
224
252
  </AnimatedVideoPlayerComponent>
225
253
  </AnimationComponent>
226
254
  </View>
255
+
227
256
  <AnimatedScrollModal>
228
- {isInlineModal && !docked ? (
257
+ {showDetails(!isTablet, docked) && (
229
258
  <AnimationComponent
230
259
  animationType={ComponentAnimationType.componentFade}
231
260
  style={defaultStyles.flex}
@@ -235,10 +264,9 @@ export const PlayerWrapper = (props: Props) => {
235
264
  style={defaultStyles.playerDetails}
236
265
  entry={entry}
237
266
  isTabletLandscape={isTabletLandscape}
267
+ isTablet={isTablet}
238
268
  />
239
269
  </AnimationComponent>
240
- ) : (
241
- <View style={defaultStyles.playerDetails} />
242
270
  )}
243
271
  </AnimatedScrollModal>
244
272
  </ScreenContextProvider>
@@ -249,3 +277,5 @@ export const PlayerWrapper = (props: Props) => {
249
277
  </PathnameContext.Provider>
250
278
  );
251
279
  };
280
+
281
+ export const PlayerWrapper = PlayerWrapperComponent;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import * as R from "ramda";
3
- import { StyleSheet, View } from "react-native";
3
+ import { StyleSheet, View, StatusBar } from "react-native";
4
4
 
5
5
  import { HandlePlayable } from "@applicaster/zapp-react-native-ui-components/Components/HandlePlayable";
6
6
  import {
@@ -129,32 +129,38 @@ const VideoModalComponent = () => {
129
129
  const pathname = `${ROUTE_TYPES.VIDEO_MODAL}/${item?.id}`;
130
130
 
131
131
  return (
132
- <PathnameContext.Provider value={pathname}>
133
- <ScreenContextProvider pathname={pathname}>
134
- {itemIdHooksFinished === item?.id ? (
135
- <AnimatedPlayerModalWrapper
136
- style={[
137
- styles.container,
138
- {
139
- backgroundColor,
140
- },
141
- ]}
142
- >
143
- <HandlePlayable
144
- item={item}
145
- isModal={mode !== "PIP"}
146
- modalHeight={modalSize.height}
147
- modalWidth={modalSize.width}
148
- mode={mode}
149
- />
150
- </AnimatedPlayerModalWrapper>
151
- ) : (
152
- <View style={styles.loaderContainer}>
153
- <Spinner />
154
- </View>
155
- )}
156
- </ScreenContextProvider>
157
- </PathnameContext.Provider>
132
+ <>
133
+ {/* TODO: Workaround to avoid unnecessary UI effect when switching from portrait to fullscreen */}
134
+ <StatusBar hidden={false} />
135
+ {/* TODO: Workaround to avoid unnecessary UI effect when switching from portrait to fullscreen */}
136
+
137
+ <PathnameContext.Provider value={pathname}>
138
+ <ScreenContextProvider pathname={pathname}>
139
+ {itemIdHooksFinished === item?.id ? (
140
+ <AnimatedPlayerModalWrapper
141
+ style={[
142
+ styles.container,
143
+ {
144
+ backgroundColor,
145
+ },
146
+ ]}
147
+ >
148
+ <HandlePlayable
149
+ item={item}
150
+ isModal={mode !== "PIP"}
151
+ modalHeight={modalSize.height}
152
+ modalWidth={modalSize.width}
153
+ mode={mode}
154
+ />
155
+ </AnimatedPlayerModalWrapper>
156
+ ) : (
157
+ <View style={styles.loaderContainer}>
158
+ <Spinner />
159
+ </View>
160
+ )}
161
+ </ScreenContextProvider>
162
+ </PathnameContext.Provider>
163
+ </>
158
164
  );
159
165
  };
160
166
 
@@ -8,6 +8,13 @@ const props = {
8
8
  configuration: { modal_background_color: "transparent" },
9
9
  };
10
10
 
11
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/layout", () => ({
12
+ useDimensions: jest.fn(() => ({
13
+ width: 20,
14
+ height: 30,
15
+ })),
16
+ }));
17
+
11
18
  jest.mock("react-native-safe-area-context", () => ({
12
19
  useSafeAreaInsets: jest.fn(() => ({
13
20
  top: 20,
@@ -9,6 +9,7 @@ exports[`PlayerDetails renders properly 1`] = `
9
9
  "flex": 1,
10
10
  "marginTop": -8,
11
11
  "paddingTop": -8,
12
+ "width": 750,
12
13
  }
13
14
  }
14
15
  >
@@ -29,6 +30,7 @@ exports[`PlayerDetails renders properly on tablet in landscape orientation 1`] =
29
30
  "flex": 1,
30
31
  "marginTop": -20,
31
32
  "paddingTop": 40,
33
+ "width": 750,
32
34
  }
33
35
  }
34
36
  >
@@ -27,8 +27,8 @@ exports[`PlayerWrapper renders inline 1`] = `
27
27
  "backgroundColor": "transparent",
28
28
  },
29
29
  Object {
30
- "height": 800,
31
- "width": 300,
30
+ "height": 1334,
31
+ "width": 750,
32
32
  },
33
33
  Object {},
34
34
  ]
@@ -91,6 +91,7 @@ exports[`PlayerWrapper renders inline 1`] = `
91
91
  "id": "test",
92
92
  }
93
93
  }
94
+ isTablet={false}
94
95
  isTabletLandscape={false}
95
96
  style={
96
97
  Object {
@@ -132,8 +133,8 @@ exports[`PlayerWrapper renders inline and docked 1`] = `
132
133
  "backgroundColor": "transparent",
133
134
  },
134
135
  Object {
135
- "height": undefined,
136
- "width": 300,
136
+ "height": 1334,
137
+ "width": 750,
137
138
  },
138
139
  Object {},
139
140
  ]
@@ -177,13 +178,35 @@ exports[`PlayerWrapper renders inline and docked 1`] = `
177
178
  testID="test-player-container"
178
179
  />
179
180
  <View
181
+ animationType="componentFade"
180
182
  style={
181
183
  Object {
182
184
  "flex": 1,
183
- "paddingTop": 20,
184
185
  }
185
186
  }
186
- />
187
+ >
188
+ <View
189
+ configuration={
190
+ Object {
191
+ "tablet_landscape_player_container_background_color": "red",
192
+ "tablet_landscape_sidebar_width": "35%",
193
+ }
194
+ }
195
+ entry={
196
+ Object {
197
+ "id": "test",
198
+ }
199
+ }
200
+ isTablet={false}
201
+ isTabletLandscape={false}
202
+ style={
203
+ Object {
204
+ "flex": 1,
205
+ "paddingTop": 20,
206
+ }
207
+ }
208
+ />
209
+ </View>
187
210
  </View>
188
211
  </RNCSafeAreaView>
189
212
  </RNCSafeAreaProvider>
@@ -296,6 +319,7 @@ exports[`PlayerWrapper renders inline on tablet in landscape orientation 1`] = `
296
319
  "id": "test",
297
320
  }
298
321
  }
322
+ isTablet={true}
299
323
  isTabletLandscape={true}
300
324
  style={
301
325
  Object {
@@ -337,8 +361,8 @@ exports[`PlayerWrapper renders properly 1`] = `
337
361
  "backgroundColor": "transparent",
338
362
  },
339
363
  Object {
340
- "height": 800,
341
- "width": 300,
364
+ "height": 750,
365
+ "width": 1334,
342
366
  },
343
367
  Object {},
344
368
  ]
@@ -382,13 +406,35 @@ exports[`PlayerWrapper renders properly 1`] = `
382
406
  testID="test-player-container"
383
407
  />
384
408
  <View
409
+ animationType="componentFade"
385
410
  style={
386
411
  Object {
387
412
  "flex": 1,
388
- "paddingTop": 20,
389
413
  }
390
414
  }
391
- />
415
+ >
416
+ <View
417
+ configuration={
418
+ Object {
419
+ "tablet_landscape_player_container_background_color": "red",
420
+ "tablet_landscape_sidebar_width": "35%",
421
+ }
422
+ }
423
+ entry={
424
+ Object {
425
+ "id": "test",
426
+ }
427
+ }
428
+ isTablet={false}
429
+ isTabletLandscape={false}
430
+ style={
431
+ Object {
432
+ "flex": 1,
433
+ "paddingTop": 20,
434
+ }
435
+ }
436
+ />
437
+ </View>
392
438
  </View>
393
439
  </RNCSafeAreaView>
394
440
  </RNCSafeAreaProvider>
@@ -45,7 +45,10 @@ const BarView = ({ screenStyles, barState = defaultState }: Props) => {
45
45
  );
46
46
 
47
47
  return (
48
- <SafeAreaView style={[style.view, isRTL ? style.rtlStyle : {}]}>
48
+ <SafeAreaView
49
+ style={[style.view, isRTL ? style.rtlStyle : {}]}
50
+ testID="BarView-safeAreaView"
51
+ >
49
52
  {backButton}
50
53
  <BarTitle title={barState.title} screenStyles={screenStyles} />
51
54
  {closeButton}
@@ -11,12 +11,16 @@ const renderEmptyView = () => {
11
11
  return <View style={emptyViewStyle} />;
12
12
  };
13
13
 
14
- type FloatingButtonProps = {
14
+ type FloatingButtonProps = Testable & {
15
15
  onPress: () => void;
16
16
  screenStyles: any;
17
17
  };
18
18
 
19
- const BackButton = ({ onPress, screenStyles }: FloatingButtonProps) => {
19
+ const BackButton = ({
20
+ onPress,
21
+ screenStyles,
22
+ testID = "backButton",
23
+ }: FloatingButtonProps) => {
20
24
  const containerStyle: any = {};
21
25
 
22
26
  if (!onPress) {
@@ -25,6 +29,7 @@ const BackButton = ({ onPress, screenStyles }: FloatingButtonProps) => {
25
29
 
26
30
  return (
27
31
  <ActionButton
32
+ testID={`${testID}-actionButton`}
28
33
  onPress={onPress}
29
34
  buttonStyles={containerStyle}
30
35
  imageUrl={screenStyles.back_button_image}
@@ -11,12 +11,16 @@ const renderEmptyView = () => {
11
11
  return <View style={emptyViewStyle} />;
12
12
  };
13
13
 
14
- type CloseButtonProps = {
14
+ type CloseButtonProps = Testable & {
15
15
  onPress: () => void;
16
16
  screenStyles?: any;
17
17
  };
18
18
 
19
- const CloseButton = ({ onPress, screenStyles }: CloseButtonProps) => {
19
+ const CloseButton = ({
20
+ onPress,
21
+ screenStyles,
22
+ testID = "closeButton",
23
+ }: CloseButtonProps) => {
20
24
  const containerStyle: any = {};
21
25
 
22
26
  if (!onPress) {
@@ -25,6 +29,7 @@ const CloseButton = ({ onPress, screenStyles }: CloseButtonProps) => {
25
29
 
26
30
  return (
27
31
  <ActionButton
32
+ testID={`${testID}-actionButton`}
28
33
  onPress={onPress}
29
34
  buttonStyles={containerStyle}
30
35
  imageUrl={screenStyles.close_button_image}
@@ -0,0 +1,87 @@
1
+ import React from "react";
2
+ import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
3
+ import { fireEvent } from "@testing-library/react-native";
4
+
5
+ const mockUseIsRTL = jest.fn(() => true);
6
+
7
+ jest.mock("@applicaster/zapp-react-native-utils/localizationUtils", () => ({
8
+ useIsRTL: mockUseIsRTL,
9
+ }));
10
+
11
+ const BarView = require("../BarView").default;
12
+
13
+ describe("BarView", () => {
14
+ const screenStyles = {
15
+ title_font_color: "#000",
16
+ title_font_android: "Roboto",
17
+ title_font_ios: "San Francisco",
18
+ title_font_size: "16",
19
+ background_color: "#fff",
20
+ };
21
+
22
+ const barState = {
23
+ backHandler: jest.fn(),
24
+ closeHandler: jest.fn(),
25
+ title: "Test Title",
26
+ };
27
+
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ });
31
+
32
+ it("renders without crashing", () => {
33
+ mockUseIsRTL.mockReturnValue(false);
34
+
35
+ const { getByText } = renderWithProviders(
36
+ <BarView screenStyles={screenStyles} barState={barState} />
37
+ );
38
+
39
+ expect(getByText("Test Title")).toBeTruthy();
40
+ });
41
+
42
+ it("applies RTL styles when isRTL is true", () => {
43
+ mockUseIsRTL.mockReturnValue(true);
44
+
45
+ const { getByTestId } = renderWithProviders(
46
+ <BarView screenStyles={screenStyles} barState={barState} />
47
+ );
48
+
49
+ expect(getByTestId("BarView-safeAreaView").props.style).toContainEqual({
50
+ transform: [{ scaleX: -1 }],
51
+ });
52
+ });
53
+
54
+ it("does not apply RTL styles when isRTL is false", () => {
55
+ mockUseIsRTL.mockReturnValue(false);
56
+
57
+ const { getByTestId } = renderWithProviders(
58
+ <BarView screenStyles={screenStyles} barState={barState} />
59
+ );
60
+
61
+ expect(getByTestId("BarView-safeAreaView").props.style).not.toContainEqual({
62
+ transform: [{ scaleX: -1 }],
63
+ });
64
+ });
65
+
66
+ it("calls closeHandler when CloseButton is pressed", () => {
67
+ mockUseIsRTL.mockReturnValue(false);
68
+
69
+ const { getByTestId } = renderWithProviders(
70
+ <BarView screenStyles={screenStyles} barState={barState} />
71
+ );
72
+
73
+ fireEvent.press(getByTestId("closeButton-actionButton-touchable"));
74
+ expect(barState.closeHandler).toHaveBeenCalled();
75
+ });
76
+
77
+ it("calls backHandler when BackButton is pressed", () => {
78
+ mockUseIsRTL.mockReturnValue(false);
79
+
80
+ const { getByTestId } = renderWithProviders(
81
+ <BarView screenStyles={screenStyles} barState={barState} />
82
+ );
83
+
84
+ fireEvent.press(getByTestId("backButton-actionButton-touchable"));
85
+ expect(barState.backHandler).toHaveBeenCalled();
86
+ });
87
+ });
@@ -28,7 +28,7 @@ styles: {
28
28
  }
29
29
  */
30
30
 
31
- type ActionButtonProps = {
31
+ type ActionButtonProps = Testable & {
32
32
  onPress: () => void;
33
33
  buttonStyles: any;
34
34
  imageUrl: string;
@@ -46,11 +46,16 @@ const ActionButton = ({
46
46
  buttonStyles,
47
47
  imageUrl,
48
48
  imageStyle = closeButtonStyle,
49
+ testID,
49
50
  }: ActionButtonProps) => {
50
51
  return (
51
- <View style={buttonStyles}>
52
- <TouchableOpacity onPress={onPress}>
53
- <Image source={{ uri: imageUrl }} style={imageStyle} />
52
+ <View style={buttonStyles} testID={testID}>
53
+ <TouchableOpacity onPress={onPress} testID={`${testID}-touchable`}>
54
+ <Image
55
+ source={{ uri: imageUrl }}
56
+ style={imageStyle}
57
+ testID={`${testID}-image`}
58
+ />
54
59
  </TouchableOpacity>
55
60
  </View>
56
61
  );
@@ -1,5 +1,6 @@
1
1
  import { create, useStore } from "zustand";
2
2
  import { produce } from "immer";
3
+ import { useCallback, useMemo } from "react";
3
4
 
4
5
  type Dimensions = {
5
6
  width: Option<number>;
@@ -31,16 +32,19 @@ export const useCachedDimensions = (id: string) => {
31
32
  dimensionsStore
32
33
  ) as DimensionsState;
33
34
 
34
- const getCachedDimensions = () => {
35
+ const getCachedDimensions = useCallback(() => {
35
36
  return dimensions[id] || initialDimensions();
36
- };
37
+ }, [dimensions?.[id], id]);
37
38
 
38
- const handleSetCachedDimensions = (newDimensions) => {
39
+ const handleSetCachedDimensions = useCallback((newDimensions) => {
39
40
  setCachedDimensions(id, newDimensions);
40
- };
41
-
42
- return {
43
- getCachedDimensions,
44
- setCachedDimensions: handleSetCachedDimensions,
45
- };
41
+ }, []);
42
+
43
+ return useMemo(
44
+ () => ({
45
+ getCachedDimensions,
46
+ setCachedDimensions: handleSetCachedDimensions,
47
+ }),
48
+ [getCachedDimensions, handleSetCachedDimensions]
49
+ );
46
50
  };
@@ -2,7 +2,7 @@ import { createStore } from "zustand/vanilla";
2
2
  import { useStore } from "zustand";
3
3
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
4
4
 
5
- interface ConfirmationDialogState {
5
+ export interface ConfirmationDialogState {
6
6
  isDialogVisible: boolean;
7
7
  messages: string;
8
8
  confirmAction: () => void;
@@ -177,7 +177,12 @@ export function withScreenContext(Component: React.ComponentType<any>) {
177
177
  return function WithScreenContextWrapper(props) {
178
178
  return (
179
179
  <ScreenContext.Consumer>
180
- {(value) => <Component {...props} screenContext={value} />}
180
+ {useCallback(
181
+ (value) => (
182
+ <Component {...props} screenContext={value} />
183
+ ),
184
+ [Component, props]
185
+ )}
181
186
  </ScreenContext.Consumer>
182
187
  );
183
188
  };
package/const.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ViewStyle } from "react-native";
2
+
1
3
  export const tvPluginsWithCellRenderer = [
2
4
  "group-qb",
3
5
  "gallery-qb",
@@ -5,3 +7,7 @@ export const tvPluginsWithCellRenderer = [
5
7
  "hero-qb",
6
8
  "list-qb",
7
9
  ];
10
+
11
+ // Can't be used in StyleSheet.create, need to be inline/defined as a constant
12
+ // https://github.com/necolas/react-native-web/issues/1427
13
+ export const flatListOverflowStyle: ViewStyle = { overflow: "visible" };
package/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  import * as QuickBrickComponents from "./Components";
2
+ import { flatListOverflowStyle } from "./const";
3
+
4
+ export { flatListOverflowStyle };
2
5
 
3
6
  export default QuickBrickComponents;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "12.0.0-alpha.4220424021",
3
+ "version": "12.0.0-alpha.4223432180",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -34,10 +34,10 @@
34
34
  "redux-mock-store": "^1.5.3"
35
35
  },
36
36
  "dependencies": {
37
- "@applicaster/applicaster-types": "12.0.0-alpha.4220424021",
38
- "@applicaster/zapp-react-native-bridge": "12.0.0-alpha.4220424021",
39
- "@applicaster/zapp-react-native-redux": "12.0.0-alpha.4220424021",
40
- "@applicaster/zapp-react-native-utils": "12.0.0-alpha.4220424021",
37
+ "@applicaster/applicaster-types": "12.0.0-alpha.4223432180",
38
+ "@applicaster/zapp-react-native-bridge": "12.0.0-alpha.4223432180",
39
+ "@applicaster/zapp-react-native-redux": "12.0.0-alpha.4223432180",
40
+ "@applicaster/zapp-react-native-utils": "12.0.0-alpha.4223432180",
41
41
  "promise": "^8.3.0",
42
42
  "react-router-native": "^5.1.2",
43
43
  "url": "^0.11.0",