@applicaster/zapp-react-native-ui-components 15.0.0-alpha.3514407021 → 15.0.0-alpha.3564837831

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 (89) hide show
  1. package/Components/AnimatedInOut/index.tsx +69 -26
  2. package/Components/BaseFocusable/index.ios.ts +12 -2
  3. package/Components/Cell/Cell.tsx +8 -3
  4. package/Components/Cell/FocusableWrapper.tsx +3 -0
  5. package/Components/Cell/TvOSCellComponent.tsx +26 -5
  6. package/Components/Focusable/Focusable.tsx +4 -2
  7. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  8. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  9. package/Components/FocusableGroup/FocusableTvOS.tsx +55 -1
  10. package/Components/FocusableGroup/hooks/__tests__/useIsFocusEnabled.test.ts +113 -0
  11. package/Components/FocusableGroup/hooks/index.ts +1 -0
  12. package/Components/FocusableGroup/hooks/useIsFocusEnabled.ts +68 -0
  13. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +19 -3
  14. package/Components/HandlePlayable/HandlePlayable.tsx +17 -65
  15. package/Components/HandlePlayable/const.ts +3 -0
  16. package/Components/HandlePlayable/utils.ts +74 -0
  17. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  18. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  19. package/Components/Layout/TV/index.tsx +3 -4
  20. package/Components/Layout/TV/index.web.tsx +3 -4
  21. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  22. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +16 -1
  23. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +30 -2
  24. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  25. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  26. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  27. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  28. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +10 -6
  29. package/Components/MasterCell/DefaultComponents/Text/index.tsx +8 -8
  30. package/Components/MasterCell/index.tsx +2 -0
  31. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  32. package/Components/MasterCell/utils/index.ts +61 -31
  33. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  34. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  35. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  36. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  37. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  38. package/Components/PlayerContainer/PlayerContainer.tsx +51 -55
  39. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
  40. package/Components/PlayerImageBackground/index.tsx +3 -22
  41. package/Components/River/TV/River.tsx +31 -14
  42. package/Components/River/TV/index.tsx +8 -4
  43. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  44. package/Components/River/TV/utils/index.ts +4 -0
  45. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  46. package/Components/Screen/TV/index.web.tsx +4 -2
  47. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  48. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  49. package/Components/Screen/hooks.ts +2 -3
  50. package/Components/Screen/index.tsx +2 -3
  51. package/Components/Screen/navigationHandler.ts +49 -24
  52. package/Components/Screen/orientationHandler.ts +3 -3
  53. package/Components/ScreenResolver/index.tsx +13 -7
  54. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  55. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  56. package/Components/Tabs/TV/Tabs.tsx +20 -3
  57. package/Components/Transitioner/Scene.tsx +15 -2
  58. package/Components/Transitioner/index.js +3 -3
  59. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
  60. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
  61. package/Components/VideoModal/ModalAnimation/index.ts +2 -13
  62. package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
  63. package/Components/VideoModal/PlayerWrapper.tsx +14 -88
  64. package/Components/VideoModal/VideoModal.tsx +1 -5
  65. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  66. package/Components/VideoModal/hooks/useModalSize.ts +10 -5
  67. package/Components/VideoModal/playerWrapperStyle.ts +70 -0
  68. package/Components/VideoModal/playerWrapperUtils.ts +91 -0
  69. package/Components/VideoModal/utils.ts +19 -9
  70. package/Contexts/AboveTabsScreenContext/index.tsx +33 -0
  71. package/Decorators/Analytics/index.tsx +6 -5
  72. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  73. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  74. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  75. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  76. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  77. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  78. package/Helpers/DataSourceHelper/index.ts +19 -0
  79. package/index.d.ts +7 -0
  80. package/package.json +6 -5
  81. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  82. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
  83. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -417
  84. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
  85. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
  86. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
  87. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
  88. package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
  89. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -6,10 +6,6 @@ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
6
6
 
7
7
  type AnimatedInterpolatedStyle = any;
8
8
 
9
- // type AnimatedInterpolatedStyle =
10
- // | Animated.AnimatedInterpolation
11
- // | [{ [Key: string]: Animated.AnimatedInterpolation }];
12
-
13
9
  type AnimationConfig = {
14
10
  duration: number;
15
11
  easing: EasingFunction;
@@ -45,32 +41,57 @@ export function AnimatedInOut({
45
41
  children,
46
42
  }: Props) {
47
43
  const [animatedValue] = React.useState(new Animated.Value(visible ? 1 : 0));
48
- const [animating, setAnimating] = React.useState(undefined);
44
+ const animationRef = React.useRef<Animated.CompositeAnimation | null>(null);
45
+ const delayTimerRef = React.useRef<NodeJS.Timeout | null>(null);
49
46
 
50
47
  const previousVisible = usePrevious(toBooleanWithDefaultFalse(visible));
51
48
 
52
- function startAnimation(toValue, config) {
53
- if (animating) {
54
- animating.reset();
55
- }
56
-
57
- const { duration, easing, delay = 0, onAnimationEnd = noop } = config;
49
+ const startAnimation = React.useCallback(
50
+ (toValue: number, config: AnimationConfig) => {
51
+ if (delayTimerRef.current) {
52
+ clearTimeout(delayTimerRef.current);
53
+ delayTimerRef.current = null;
54
+ }
55
+
56
+ if (animationRef.current) {
57
+ animationRef.current.stop();
58
+ animationRef.current = null;
59
+ }
60
+
61
+ const { duration, easing, delay = 0, onAnimationEnd = noop } = config;
62
+
63
+ const runAnimation = () => {
64
+ animationRef.current = Animated.timing(animatedValue, {
65
+ duration,
66
+ toValue,
67
+ easing,
68
+ useNativeDriver: true,
69
+ });
70
+
71
+ animationRef.current.start(({ finished }) => {
72
+ if (finished) {
73
+ animationRef.current = null;
74
+ onAnimationEnd();
75
+ }
76
+ });
77
+ };
78
+
79
+ if (delay > 0) {
80
+ delayTimerRef.current = setTimeout(runAnimation, delay);
81
+ } else {
82
+ runAnimation();
83
+ }
84
+ },
85
+ [animatedValue]
86
+ );
58
87
 
59
- const compositeAnimation = Animated.timing(animatedValue, {
60
- duration,
61
- toValue,
62
- easing,
63
- delay,
64
- useNativeDriver: true,
65
- }).start(() => {
66
- setAnimating(undefined);
67
- onAnimationEnd();
68
- });
88
+ React.useEffect(() => {
89
+ if (previousVisible === undefined) {
90
+ animatedValue.setValue(visible ? 1 : 0);
69
91
 
70
- setAnimating(compositeAnimation);
71
- }
92
+ return;
93
+ }
72
94
 
73
- React.useEffect(() => {
74
95
  if (!previousVisible && visible) {
75
96
  startAnimation(1.0, getAnimation(animationConfig, "in"));
76
97
  }
@@ -78,7 +99,29 @@ export function AnimatedInOut({
78
99
  if (previousVisible && !visible) {
79
100
  startAnimation(0.0, getAnimation(animationConfig, "out"));
80
101
  }
81
- }, [visible, previousVisible]);
102
+ }, [
103
+ visible,
104
+ previousVisible,
105
+ animatedValue,
106
+ startAnimation,
107
+ animationConfig,
108
+ ]);
109
+
110
+ React.useEffect(() => {
111
+ return () => {
112
+ if (delayTimerRef.current) {
113
+ clearTimeout(delayTimerRef.current);
114
+ delayTimerRef.current = null;
115
+ }
116
+
117
+ if (animationRef.current) {
118
+ animationRef.current.stop();
119
+ animationRef.current = null;
120
+ }
121
+
122
+ animatedValue.stopAnimation();
123
+ };
124
+ }, [animatedValue]);
82
125
 
83
126
  const styles = visible
84
127
  ? getAnimation(animationConfig, "in").styles
@@ -86,7 +129,7 @@ export function AnimatedInOut({
86
129
 
87
130
  return (
88
131
  <Animated.View
89
- renderToHardwareTextureAndroid={animating}
132
+ renderToHardwareTextureAndroid={!!animationRef.current}
90
133
  style={[styles(animatedValue), staticStyles]}
91
134
  >
92
135
  {children}
@@ -22,6 +22,7 @@ type Props = {
22
22
  onFocus?: FocusManager.FocusEventCB;
23
23
  onBlur?: FocusManager.FocusEventCB;
24
24
  selected?: boolean;
25
+ skipFocusManagerRegistration?: boolean;
25
26
  };
26
27
 
27
28
  export class BaseFocusable<
@@ -61,10 +62,14 @@ export class BaseFocusable<
61
62
  }
62
63
 
63
64
  componentDidMount() {
64
- const { id } = this.props;
65
+ const { id, skipFocusManagerRegistration } = this.props;
65
66
  const component = this;
66
67
  this.node = this.ref.current;
67
68
 
69
+ if (skipFocusManagerRegistration) {
70
+ return;
71
+ }
72
+
68
73
  focusManager.register({
69
74
  id,
70
75
  component: component,
@@ -118,7 +123,12 @@ export class BaseFocusable<
118
123
 
119
124
  componentWillUnmount() {
120
125
  this._isMounted = false;
121
- const { id } = this.props;
126
+ const { id, skipFocusManagerRegistration } = this.props;
127
+
128
+ if (skipFocusManagerRegistration) {
129
+ return;
130
+ }
131
+
122
132
  focusManager.unregister(id, { group: this.isGroup || false });
123
133
  }
124
134
 
@@ -208,14 +208,14 @@ export class CellComponent extends React.Component<Props, State> {
208
208
  this.accessibilityManager.readText({
209
209
  text: " ",
210
210
  });
211
- } else {
211
+ } else if (this.state.cellFocused) {
212
212
  this.accessibilityManager.readText({
213
213
  text: `${positionLabel}`,
214
214
  });
215
215
  }
216
216
  }
217
217
 
218
- componentDidUpdate(prevProps: Readonly<Props>) {
218
+ componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
219
219
  if (prevProps.item !== this.props.item) {
220
220
  this.setState({
221
221
  hasFocusableInside: this.props.CellRenderer.hasFocusableInside?.(
@@ -224,7 +224,12 @@ export class CellComponent extends React.Component<Props, State> {
224
224
  });
225
225
  }
226
226
 
227
- this.handleAccessibilityFocus(this.props.index, this.props.dataLength);
227
+ if (
228
+ prevState.cellFocused !== this.state.cellFocused ||
229
+ this.state.hasFocusableInside
230
+ ) {
231
+ this.handleAccessibilityFocus(this.props.index, this.props.dataLength);
232
+ }
228
233
  }
229
234
 
230
235
  render() {
@@ -10,6 +10,7 @@ type Props = {
10
10
  children: (focused: boolean) => React.ReactNode;
11
11
  onFocus: (arg1: any, index?: number) => void;
12
12
  onBlur: Callback;
13
+ skipFocusManagerRegistration?: boolean;
13
14
  };
14
15
 
15
16
  export const FocusableWrapper = ({
@@ -20,6 +21,7 @@ export const FocusableWrapper = ({
20
21
  applyWrapper,
21
22
  onFocus,
22
23
  onBlur,
24
+ skipFocusManagerRegistration,
23
25
  }: Props) => {
24
26
  if (applyWrapper) {
25
27
  return (
@@ -34,6 +36,7 @@ export const FocusableWrapper = ({
34
36
  // @ts-ignore
35
37
  offsetUpdater={noop}
36
38
  isFocusable
39
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
37
40
  >
38
41
  {(focused) => children(focused)}
39
42
  </Focusable>
@@ -17,6 +17,7 @@ import { CellWithFocusable } from "./CellWithFocusable";
17
17
  import { FocusableWrapper } from "./FocusableWrapper";
18
18
 
19
19
  import { focusableButtonsRegistration$ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
20
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
20
21
 
21
22
  type Props = {
22
23
  item: ZappEntry;
@@ -37,6 +38,10 @@ type Props = {
37
38
  component: {
38
39
  id: number | string;
39
40
  component_type: string;
41
+ styles?: {
42
+ component_margin_top?: number;
43
+ component_padding_top?: number;
44
+ };
40
45
  };
41
46
  selected: boolean;
42
47
  CellRenderer: React.FunctionComponent<any> & {
@@ -75,6 +80,7 @@ type Props = {
75
80
  componentsMapOffset: number;
76
81
  applyFocusableWrapper: boolean;
77
82
  hasFocusableInside: boolean;
83
+ skipFocusManagerRegistration?: boolean;
78
84
  };
79
85
 
80
86
  type State = {
@@ -201,14 +207,25 @@ class TvOSCell extends React.Component<Props, State> {
201
207
  ) {
202
208
  const { headerOffset } = getHeaderOffset();
203
209
 
204
- const extraAnchorPointYOffset =
205
- screenLayout?.extraAnchorPointYOffset || 0;
210
+ const extraAnchorPointYOffset = toNumberWithDefaultZero(
211
+ screenLayout?.extraAnchorPointYOffset
212
+ );
213
+
214
+ const componentMarginTop = toNumberWithDefaultZero(
215
+ component?.styles?.component_margin_top
216
+ );
217
+
218
+ const componentPaddingTop = toNumberWithDefaultZero(
219
+ component?.styles?.component_padding_top
220
+ );
206
221
 
207
222
  const totalOffset =
208
223
  headerOffset +
209
- (componentAnchorPointY || 0) +
210
- extraAnchorPointYOffset -
211
- componentsMapOffset || 0;
224
+ toNumberWithDefaultZero(componentAnchorPointY) +
225
+ extraAnchorPointYOffset -
226
+ toNumberWithDefaultZero(componentsMapOffset) +
227
+ componentMarginTop +
228
+ componentPaddingTop;
212
229
 
213
230
  mainOffsetUpdater?.(
214
231
  { tag: this.target },
@@ -250,6 +267,7 @@ class TvOSCell extends React.Component<Props, State> {
250
267
  behavior,
251
268
  applyFocusableWrapper,
252
269
  hasFocusableInside,
270
+ skipFocusManagerRegistration,
253
271
  } = this.props;
254
272
 
255
273
  const { id } = item;
@@ -275,6 +293,7 @@ class TvOSCell extends React.Component<Props, State> {
275
293
  onFocus={handleFocus}
276
294
  onBlur={onBlur || this.onBlur}
277
295
  applyWrapper={applyFocusableWrapper}
296
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
278
297
  >
279
298
  {(focused) => (
280
299
  <CellWithFocusable
@@ -289,6 +308,7 @@ class TvOSCell extends React.Component<Props, State> {
289
308
  focused={focused || this.props.focused}
290
309
  behavior={behavior}
291
310
  isFocusable={isFocusable}
311
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
292
312
  />
293
313
  )}
294
314
  </FocusableWrapper>
@@ -311,6 +331,7 @@ class TvOSCell extends React.Component<Props, State> {
311
331
  offsetUpdater={offsetUpdater}
312
332
  style={baseCellStyles}
313
333
  isFocusable={isFocusable}
334
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
314
335
  >
315
336
  {(focused) => (
316
337
  <FocusableCell
@@ -8,6 +8,8 @@ import { withFocusableContext } from "../../Contexts/FocusableGroupContext/withF
8
8
  import { StyleSheet, ViewStyle } from "react-native";
9
9
  import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
10
10
 
11
+ import { isSearchInputId } from "@applicaster/zapp-react-native-utils/searchUtils";
12
+
11
13
  type Props = {
12
14
  initialFocus?: boolean;
13
15
  id: string;
@@ -106,7 +108,7 @@ class Focusable extends BaseFocusable<Props> {
106
108
  onMouseEnter() {
107
109
  const { id } = this.props;
108
110
 
109
- if (id !== "search_input_group_id") {
111
+ if (!isSearchInputId(id)) {
110
112
  this.mouse = true;
111
113
  this.props?.handleFocus?.({ mouse: true });
112
114
 
@@ -120,7 +122,7 @@ class Focusable extends BaseFocusable<Props> {
120
122
  onMouseLeave() {
121
123
  const { id } = this.props;
122
124
 
123
- if (id !== "search_input_group_id") {
125
+ if (!isSearchInputId(id)) {
124
126
  this.mouse = false;
125
127
  this.blur(null);
126
128
  }
@@ -10,8 +10,12 @@ import {
10
10
  forceFocusableFocus,
11
11
  } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
12
12
  import { findNodeHandle, ViewStyle } from "react-native";
13
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
13
14
 
14
- function noop() {}
15
+ import {
16
+ emitDidFocused,
17
+ emitNativeRegistered,
18
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
15
19
 
16
20
  type Props = {
17
21
  id: string;
@@ -39,6 +43,7 @@ type Props = {
39
43
  hasReceivedFocus: () => void;
40
44
  offsetUpdater: (arg1: string, arg2: number) => number;
41
45
  style: ViewStyle;
46
+ skipFocusManagerRegistration?: boolean;
42
47
  };
43
48
 
44
49
  export class Focusable extends BaseFocusable<Props> {
@@ -53,6 +58,7 @@ export class Focusable extends BaseFocusable<Props> {
53
58
  this.nextFocusableReactTags = {};
54
59
  this.preferredFocus = this.preferredFocus.bind(this);
55
60
  this.measureView = this.measureView.bind(this);
61
+ this.onRegistered = this.onRegistered.bind(this);
56
62
  }
57
63
 
58
64
  /**
@@ -84,6 +90,9 @@ export class Focusable extends BaseFocusable<Props> {
84
90
  });
85
91
  }
86
92
 
93
+ const id: string = nativeEvent.itemID;
94
+ emitDidFocused(id);
95
+
87
96
  onFocus(nativeEvent);
88
97
  }
89
98
 
@@ -169,6 +178,13 @@ export class Focusable extends BaseFocusable<Props> {
169
178
  });
170
179
  }
171
180
 
181
+ onRegistered({ nativeEvent }) {
182
+ const groupId = nativeEvent?.groupId;
183
+ const id = nativeEvent?.itemId;
184
+
185
+ emitNativeRegistered({ id, groupId, isGroup: false });
186
+ }
187
+
172
188
  render() {
173
189
  const {
174
190
  children,
@@ -203,6 +219,7 @@ export class Focusable extends BaseFocusable<Props> {
203
219
  focusable={isFocusable}
204
220
  {...this.nextFocusableReactTags}
205
221
  {...otherProps}
222
+ onRegistered={this.onRegistered}
206
223
  >
207
224
  {typeof children === "function" ? children(focused) : children}
208
225
  </FocusableItemNative>
@@ -5,6 +5,7 @@ exports[`FocusableTvOS should render correctly 1`] = `
5
5
  groupId={null}
6
6
  itemId={null}
7
7
  onLayout={[Function]}
8
+ onRegistered={[Function]}
8
9
  onViewBlur={[Function]}
9
10
  onViewFocus={[Function]}
10
11
  onViewPress={[Function]}
@@ -1,7 +1,15 @@
1
1
  import * as React from "react";
2
+ import { compose } from "@applicaster/zapp-react-native-utils/utils";
2
3
  import { FocusableGroupNative } from "@applicaster/zapp-react-native-ui-components/Components/NativeFocusables";
3
4
  import { BaseFocusable } from "@applicaster/zapp-react-native-ui-components/Components/BaseFocusable";
4
5
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
6
+ import { LayoutContext } from "@applicaster/zapp-react-native-tvos-app/Context/LayoutContext";
7
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
8
+ import { isScreenPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
9
+ import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
10
+ import { withAboveTabsScreenContextConsumer } from "@applicaster/zapp-react-native-ui-components/Contexts/AboveTabsScreenContext";
11
+
12
+ import { useIsFocusEnabled } from "./hooks";
5
13
 
6
14
  const { log_verbose } = createLogger({
7
15
  subsystem: "General",
@@ -33,7 +41,16 @@ type Props = {
33
41
  screenData: { screenId: string; parentScreenId: string };
34
42
  };
35
43
 
36
- export class FocusableGroup extends BaseFocusable<Props> {
44
+ class FocusableGroupComponent extends BaseFocusable<Props> {
45
+ public readonly isGroup: boolean = true;
46
+
47
+ onRegistered = ({ nativeEvent }) => {
48
+ const groupId = nativeEvent?.groupId;
49
+ const id = nativeEvent?.itemId;
50
+
51
+ emitNativeRegistered({ id, groupId, isGroup: true });
52
+ };
53
+
37
54
  render() {
38
55
  const {
39
56
  children,
@@ -66,9 +83,46 @@ export class FocusableGroup extends BaseFocusable<Props> {
66
83
  onGroupBlur={onGroupBlur}
67
84
  style={style}
68
85
  {...otherProps}
86
+ onRegistered={this.onRegistered}
69
87
  >
70
88
  {children}
71
89
  </FocusableGroupNative>
72
90
  );
73
91
  }
74
92
  }
93
+
94
+ export const withFocusDisabledHOC = (Component) => {
95
+ return function WithFocusDisabledHOC(props) {
96
+ // @ts-ignore
97
+ const { screenFocusBlocked } = React.useContext(LayoutContext.ReactContext);
98
+
99
+ const { pathname } = useRoute();
100
+
101
+ const isPlayerPresented = isScreenPlayable(pathname);
102
+
103
+ const blockScreenFocus = isPlayerPresented === false && screenFocusBlocked;
104
+
105
+ return (
106
+ <Component
107
+ {...props}
108
+ isFocusDisabled={blockScreenFocus || props.isFocusDisabled}
109
+ />
110
+ );
111
+ };
112
+ };
113
+
114
+ const withAboveTabsScreenHOC = (Component) => {
115
+ return function WithAboveTabsScreenHOC(props) {
116
+ const { aboveTabsScreen } = props;
117
+
118
+ const isFocusEnabled = useIsFocusEnabled(aboveTabsScreen);
119
+
120
+ return <Component {...props} isFocusDisabled={!isFocusEnabled} />;
121
+ };
122
+ };
123
+
124
+ export const FocusableGroup = compose(
125
+ withAboveTabsScreenContextConsumer,
126
+ withAboveTabsScreenHOC,
127
+ withFocusDisabledHOC
128
+ )(FocusableGroupComponent);
@@ -0,0 +1,113 @@
1
+ import { act, renderHook } from "@testing-library/react-native";
2
+
3
+ import { useIsFocusEnabled } from "../useIsFocusEnabled";
4
+
5
+ // ----------------- MOCKS -----------------
6
+ jest.mock(
7
+ "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios",
8
+ () => ({
9
+ focusManager: {
10
+ isFocusOnMenu: jest.fn(),
11
+ isFocusOnTabsScreen: jest.fn(),
12
+ },
13
+ })
14
+ );
15
+
16
+ jest.mock(
17
+ "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios",
18
+ () => {
19
+ const { Subject } = require("rxjs");
20
+
21
+ return {
22
+ willFocused$: new Subject<void>(),
23
+ didFocused$: new Subject<void>(),
24
+ TabsScreenScreenSelectorContainerRegistry: {
25
+ observable$: new Subject<string>(),
26
+ },
27
+ };
28
+ }
29
+ );
30
+
31
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
32
+ import {
33
+ willFocused$,
34
+ didFocused$,
35
+ TabsScreenScreenSelectorContainerRegistry,
36
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
37
+
38
+ // ----------------- TESTS -----------------
39
+ describe("useIsFocusEnabled", () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ it("returns true by default", () => {
45
+ const { result } = renderHook(() => useIsFocusEnabled(true));
46
+
47
+ expect(result.current).toBe(true);
48
+ });
49
+
50
+ it("disables focus when focus moves to menu", () => {
51
+ (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
52
+
53
+ const { result } = renderHook(() => useIsFocusEnabled(true));
54
+
55
+ act(() => {
56
+ willFocused$.next();
57
+ didFocused$.next();
58
+ });
59
+
60
+ expect(result.current).toBe(false);
61
+ });
62
+
63
+ it("re-enables focus when focus lands on tabs screen", () => {
64
+ (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
65
+ (focusManager.isFocusOnTabsScreen as jest.Mock).mockReturnValue(true);
66
+
67
+ const { result } = renderHook(() => useIsFocusEnabled(true));
68
+
69
+ // Disable focus first
70
+ act(() => {
71
+ willFocused$.next();
72
+ didFocused$.next();
73
+ });
74
+
75
+ expect(result.current).toBe(false);
76
+
77
+ // Enable focus again
78
+ act(() => {
79
+ didFocused$.next();
80
+ TabsScreenScreenSelectorContainerRegistry.observable$.next("tabs-id");
81
+ });
82
+
83
+ expect(result.current).toBe(true);
84
+ });
85
+
86
+ it("does nothing when not above tabs screen", () => {
87
+ const { result } = renderHook(() => useIsFocusEnabled(false));
88
+
89
+ act(() => {
90
+ willFocused$.next();
91
+ didFocused$.next();
92
+ TabsScreenScreenSelectorContainerRegistry.observable$.next("id");
93
+ });
94
+
95
+ expect(result.current).toBe(true);
96
+ });
97
+
98
+ it("cleans up subscriptions on unmount", () => {
99
+ (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
100
+
101
+ const { unmount, result } = renderHook(() => useIsFocusEnabled(true));
102
+
103
+ unmount();
104
+
105
+ act(() => {
106
+ willFocused$.next();
107
+ didFocused$.next();
108
+ });
109
+
110
+ // State should not change after unmount
111
+ expect(result.current).toBe(true);
112
+ });
113
+ });
@@ -0,0 +1 @@
1
+ export { useIsFocusEnabled } from "./useIsFocusEnabled";
@@ -0,0 +1,68 @@
1
+ import * as React from "react";
2
+ import { switchMap, first, filter, repeat } from "rxjs/operators";
3
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
4
+
5
+ import {
6
+ willFocused$,
7
+ didFocused$,
8
+ TabsScreenScreenSelectorContainerRegistry,
9
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
10
+
11
+ export const useIsFocusEnabled = (isAboveTabsScreen: boolean): boolean => {
12
+ const [isFocusEnabled, setIsFocusedEnabled] = React.useState(true);
13
+
14
+ const enableFocus = React.useCallback(() => {
15
+ setIsFocusedEnabled(true);
16
+ }, []);
17
+
18
+ const disableFocus = React.useCallback(() => {
19
+ setIsFocusedEnabled(false);
20
+ }, []);
21
+
22
+ React.useEffect(() => {
23
+ const subscription = didFocused$
24
+ .pipe(
25
+ filter(() => isAboveTabsScreen && !isFocusEnabled),
26
+
27
+ switchMap(() => TabsScreenScreenSelectorContainerRegistry.observable$),
28
+ filter((id) => id && focusManager.isFocusOnTabsScreen(id)),
29
+
30
+ // run only once, then re-subscribe on didFocused$ again
31
+ first(),
32
+ repeat()
33
+ )
34
+ .subscribe(() => {
35
+ enableFocus();
36
+ });
37
+
38
+ return () => {
39
+ subscription.unsubscribe();
40
+ };
41
+ }, [enableFocus, isFocusEnabled, isAboveTabsScreen]);
42
+
43
+ React.useEffect(() => {
44
+ const subscription = willFocused$
45
+ .pipe(
46
+ filter(() => isAboveTabsScreen && isFocusEnabled),
47
+
48
+ // start waiting onFocus event
49
+ switchMap(() => didFocused$),
50
+
51
+ // focus is landed on top-menu
52
+ filter(() => focusManager.isFocusOnMenu()),
53
+
54
+ // run only once, then re-subscribe on willFocused$ again
55
+ first(),
56
+ repeat()
57
+ )
58
+ .subscribe(() => {
59
+ disableFocus();
60
+ });
61
+
62
+ return () => {
63
+ subscription.unsubscribe();
64
+ };
65
+ }, [disableFocus, isFocusEnabled, isAboveTabsScreen]);
66
+
67
+ return isFocusEnabled;
68
+ };