@applicaster/zapp-react-native-utils 13.0.0-alpha.4244205068 → 13.0.0-alpha.4244246222

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 (81) hide show
  1. package/README.md +0 -10
  2. package/actionsExecutor/ActionExecutor.ts +16 -9
  3. package/actionsExecutor/ActionExecutorContext.tsx +47 -7
  4. package/analyticsUtils/currentScreenProperities.ts +6 -4
  5. package/appUtils/HooksManager/Hook.ts +2 -2
  6. package/appUtils/HooksManager/index.ts +1 -1
  7. package/appUtils/HooksManager/types.ts +5 -2
  8. package/appUtils/accessibilityManager/index.ts +42 -1
  9. package/appUtils/contextKeysManager/contextResolver.ts +6 -2
  10. package/appUtils/contextKeysManager/index.ts +2 -2
  11. package/appUtils/contextKeysManager/utils/index.ts +2 -1
  12. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +1 -0
  13. package/appUtils/focusManager/index.ts +11 -12
  14. package/appUtils/focusManagerAux/utils/index.ts +4 -3
  15. package/appUtils/keyCodes/index.ts +11 -7
  16. package/appUtils/orientationHelper.ts +11 -13
  17. package/appUtils/platform/const.ts +14 -0
  18. package/appUtils/platform/platformUtils.ts +47 -4
  19. package/appUtils/playerManager/index.ts +1 -1
  20. package/appUtils/playerManager/player.ts +26 -14
  21. package/appUtils/playerManager/playerNative.ts +18 -3
  22. package/equalUtils/__tests__/isEqual/combination.test.ts +263 -0
  23. package/equalUtils/index.ts +49 -30
  24. package/focusManager/FocusManager.ts +11 -1
  25. package/focusManager/index.ts +49 -4
  26. package/functionUtils/index.ts +21 -8
  27. package/idleUtils/__tests__/withTimeout$.test.ts +37 -0
  28. package/idleUtils/index.ts +1 -1
  29. package/idleUtils/withTimeout.ts +15 -2
  30. package/logger/const.ts +9 -67
  31. package/manifestUtils/__tests__/createConfig.test.js +0 -2
  32. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  33. package/manifestUtils/fonts/index.js +5 -6
  34. package/manifestUtils/sharedConfiguration/screenPicker/__tests__/utils.test.js +304 -0
  35. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +875 -0
  36. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +130 -0
  37. package/navigationUtils/index.ts +3 -3
  38. package/numberUtils/index.ts +7 -5
  39. package/package.json +4 -10
  40. package/playerUtils/configurationGenerator.ts +8 -0
  41. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +5 -7
  42. package/reactHooks/autoscrolling/useTrackedView.ts +12 -1
  43. package/reactHooks/cell-click/__tests__/index.test.js +5 -0
  44. package/reactHooks/cell-click/index.ts +7 -1
  45. package/reactHooks/feed/useFeedLoader.tsx +1 -1
  46. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +1 -1
  47. package/reactHooks/layout/useDimensions/useDimensions.ts +8 -0
  48. package/reactHooks/navigation/__mocks__/useNavigation.ts +3 -0
  49. package/reactHooks/navigation/__tests__/index.test.tsx +6 -17
  50. package/reactHooks/navigation/index.ts +13 -23
  51. package/reactHooks/navigation/useGetTabBarHeight.ts +1 -3
  52. package/reactHooks/navigation/useProfilerLogging.ts +15 -0
  53. package/reactHooks/state/useRefWithInitialValue.ts +14 -0
  54. package/reactHooks/state/useStateFromSubscribe.ts +23 -0
  55. package/reactHooks/usePluginConfiguration.ts +1 -1
  56. package/reactHooks/useSubscriberFor.ts +4 -3
  57. package/reactUtils/__tests__/__snapshots__/reactUtilsHelpers.test.js.snap +16 -4
  58. package/reactUtils/__tests__/reactUtils.test.js +1 -19
  59. package/reactUtils/__tests__/reactUtilsHelpers.test.js +72 -71
  60. package/reactUtils/createContext/__tests__/__snapshots__/helpers.test.js.snap +6 -72
  61. package/reactUtils/createContext/__tests__/__snapshots__/provider.test.js.snap +3 -28
  62. package/reactUtils/createContext/__tests__/__snapshots__/withConsumer.test.js.snap +3 -4
  63. package/reactUtils/createContext/__tests__/__snapshots__/withProvider.test.js.snap +7 -3
  64. package/reactUtils/createContext/__tests__/helpers.test.js +107 -121
  65. package/reactUtils/createContext/__tests__/{index.test.js → index.test.ts} +2 -2
  66. package/reactUtils/createContext/__tests__/provider.test.js +4 -6
  67. package/reactUtils/createContext/__tests__/withConsumer.test.js +12 -12
  68. package/reactUtils/createContext/__tests__/withProvider.test.js +4 -4
  69. package/reactUtils/createContext/helpers.ts +6 -1
  70. package/reactUtils/createContext/index.ts +11 -25
  71. package/reactUtils/index.ts +0 -11
  72. package/screenState/index.ts +24 -10
  73. package/services/js2native.ts +673 -0
  74. package/testUtils/index.tsx +0 -10
  75. package/theme/index.ts +1 -1
  76. package/time/BackgroundTimer.ts +55 -0
  77. package/time/Timer.ts +9 -6
  78. package/types.ts +1 -0
  79. package/zappFrameworkUtils/presentPluginOneTime.ts +2 -3
  80. package/reactUtils/createContext/__tests__/index-no-react.test.js +0 -27
  81. /package/reactUtils/createContext/__tests__/__snapshots__/{index.test.js.snap → index.test.ts.snap} +0 -0
@@ -452,7 +452,7 @@ export class PlayerManager {
452
452
  if (this.castingReceiver) {
453
453
  this.castingReceiver.closeNativePlayer();
454
454
  } else {
455
- this.getActivePlayer().closeNativePlayer();
455
+ this.getActivePlayer()?.closeNativePlayer();
456
456
  this.invokeHandler("close");
457
457
  }
458
458
  }
@@ -97,7 +97,7 @@ export class Player {
97
97
  this.onVideoLoad = this.onVideoLoad.bind(this);
98
98
  this.isFullScreenSupported = this.isFullScreenSupported.bind(this);
99
99
  this.onPlayerSeekComplete = this.onPlayerSeekComplete.bind(this);
100
-
100
+ this.onAdBreakEnd = this.onAdBreakEnd.bind(this);
101
101
  this.entryObservable = null;
102
102
  }
103
103
 
@@ -154,8 +154,8 @@ export class Player {
154
154
  this.isSeeking()
155
155
  ? this.playerState.seekPosition
156
156
  : this.playerState.adState
157
- ? this.playerState.adState.adPosition
158
- : this.playerState.contentPosition;
157
+ ? this.playerState.adState.adPosition
158
+ : this.playerState.contentPosition;
159
159
 
160
160
  getEntry = (): ZappEntry | null => this.entry;
161
161
 
@@ -201,17 +201,25 @@ export class Player {
201
201
  adPosition: currentTime,
202
202
  };
203
203
  } else {
204
- if (!isEmptyOrNil(seekableDuration) && seekableDuration >= 0) {
205
- this.playerState.seekableDuration = seekableDuration;
206
- }
204
+ this.updateContentDuration({ seekableDuration, duration, currentTime });
205
+ }
206
+ };
207
207
 
208
- if (!isEmptyOrNil(duration) && duration >= 0) {
209
- this.playerState.contentDuration = duration;
210
- }
208
+ protected updateContentDuration = ({
209
+ seekableDuration = null,
210
+ duration = null,
211
+ currentTime = null,
212
+ }) => {
213
+ if (!isEmptyOrNil(seekableDuration) && seekableDuration >= 0) {
214
+ this.playerState.seekableDuration = seekableDuration;
215
+ }
211
216
 
212
- if (!isEmptyOrNil(currentTime) && currentTime >= 0) {
213
- this.playerState.contentPosition = currentTime;
214
- }
217
+ if (!isEmptyOrNil(duration) && duration >= 0) {
218
+ this.playerState.contentDuration = duration;
219
+ }
220
+
221
+ if (!isEmptyOrNil(currentTime) && currentTime >= 0) {
222
+ this.playerState.contentPosition = currentTime;
215
223
  }
216
224
  };
217
225
 
@@ -559,7 +567,7 @@ export class Player {
559
567
  });
560
568
  };
561
569
 
562
- onAdBreakEnd = (event) => {
570
+ onAdBreakEnd(event) {
563
571
  this.playerState.adState = null;
564
572
  this.playerState.isInAdBreak = false;
565
573
 
@@ -567,7 +575,7 @@ export class Player {
567
575
  callbackName: SharedPlayerCallBacksKeys.OnAdBreakEnd,
568
576
  event,
569
577
  });
570
- };
578
+ }
571
579
 
572
580
  onAdEnd = (event) => {
573
581
  this.playerState.adState = null;
@@ -736,6 +744,10 @@ export class Player {
736
744
  return null;
737
745
  };
738
746
 
747
+ public hasResumePosition() {
748
+ return false;
749
+ }
750
+
739
751
  public getConfig = () => this.config;
740
752
 
741
753
  // TODO: replace with enum of supported orientations
@@ -54,6 +54,10 @@ export class PlayerNative extends Player {
54
54
  });
55
55
  }
56
56
 
57
+ public hasResumePosition() {
58
+ return true;
59
+ }
60
+
57
61
  getContentPosition = () => {
58
62
  return this.playerState.contentPosition;
59
63
  };
@@ -221,11 +225,13 @@ export class PlayerNative extends Player {
221
225
  };
222
226
 
223
227
  onVideoLoad = (event) => {
224
- if (this.startPosition) {
225
- this.seekTo(this.startPosition);
226
- this.startPosition = null;
228
+ this.updateContentDuration(event);
227
229
 
230
+ if (this.startPosition && !this.playerState.isInAdBreak) {
228
231
  super.onVideoLoad({ ...event, ignoreContinueWatching: true });
232
+
233
+ this.seekTo(this.startPosition);
234
+ this.startPosition = null;
229
235
  } else {
230
236
  super.onVideoLoad(event);
231
237
  }
@@ -249,6 +255,15 @@ export class PlayerNative extends Player {
249
255
  }
250
256
  };
251
257
 
258
+ onAdBreakEnd = (event) => {
259
+ super.onAdBreakEnd(event);
260
+
261
+ if (this.startPosition) {
262
+ this.seekTo(this.startPosition);
263
+ this.startPosition = null;
264
+ }
265
+ };
266
+
252
267
  // This function is called when native player used directly without wrapped component,
253
268
  // Since event from native comes with wrapped in NativeEvent property
254
269
  getNativeViewListener = (): QuickBrickPlayer.SharedPlayerCallBacks => {
@@ -0,0 +1,263 @@
1
+ import { isEqual } from "../..";
2
+
3
+ const array = [1, 2, 3];
4
+ const sameProp = "sameProp";
5
+
6
+ describe("isEqual - comprehensive tests", () => {
7
+ describe("all conditions together", () => {
8
+ it("all conditions checked and not equal", () => {
9
+ const prev = {
10
+ byValueProp: { a: 1, b: 2 },
11
+ byRefProp: array,
12
+ byComparatorProp: "test",
13
+ extraProp1: 123,
14
+ extraProp2: sameProp,
15
+ };
16
+
17
+ const next = {
18
+ byValueProp: { a: 1, b: 2 },
19
+ byRefProp: array,
20
+ byComparatorProp: "different",
21
+ extraProp1: 456,
22
+ extraProp2: sameProp,
23
+ };
24
+
25
+ const customComparator = jest.fn().mockReturnValue(true);
26
+
27
+ const result = isEqual({
28
+ byValue: ["byValueProp"],
29
+ byReference: ["byRefProp"],
30
+ byComparator: {
31
+ comparator: customComparator,
32
+ keys: ["byComparatorProp"],
33
+ },
34
+ compareRemainingKeysBySameValue: true,
35
+ })(prev, next);
36
+
37
+ expect(result).toBe(false);
38
+
39
+ expect(customComparator).toHaveBeenCalledWith(
40
+ prev.byComparatorProp,
41
+ next.byComparatorProp
42
+ );
43
+ });
44
+
45
+ it("all conditions pass with exact remaining keys check", () => {
46
+ const prev = {
47
+ byValueProp: { a: 1, b: 2 },
48
+ byRefProp: array,
49
+ byComparatorProp: "test",
50
+ extraProp: sameProp,
51
+ };
52
+
53
+ const next = {
54
+ byValueProp: { a: 1, b: 2 },
55
+ byRefProp: array,
56
+ byComparatorProp: "test",
57
+ extraProp: sameProp,
58
+ };
59
+
60
+ const customComparator = jest.fn().mockReturnValue(true);
61
+
62
+ const result = isEqual({
63
+ byValue: ["byValueProp"],
64
+ byReference: ["byRefProp"],
65
+ byComparator: {
66
+ comparator: customComparator,
67
+ keys: ["byComparatorProp"],
68
+ },
69
+ compareRemainingKeysBySameValue: true,
70
+ })(prev, next);
71
+
72
+ expect(result).toBe(true);
73
+
74
+ expect(customComparator).toHaveBeenCalledWith(
75
+ prev.byComparatorProp,
76
+ next.byComparatorProp
77
+ );
78
+ });
79
+
80
+ it("byReference check fails first (early return)", () => {
81
+ const prev = {
82
+ byValueProp: { a: 1, b: 2 },
83
+ byRefProp: array,
84
+ byComparatorProp: sameProp,
85
+ };
86
+
87
+ const next = {
88
+ byValueProp: { a: 1, b: 2 },
89
+ byRefProp: [1, 2, 3],
90
+ byComparatorProp: sameProp,
91
+ };
92
+
93
+ const customComparator = jest.fn().mockReturnValue(true);
94
+
95
+ const result = isEqual({
96
+ byValue: ["byValueProp"],
97
+ byReference: ["byRefProp"],
98
+ byComparator: {
99
+ comparator: customComparator,
100
+ keys: ["byComparatorProp"],
101
+ },
102
+ })(prev, next);
103
+
104
+ expect(result).toBe(false);
105
+ expect(customComparator).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it("byValue check fails after byReference passes", () => {
109
+ const prev = {
110
+ byValueProp: { a: 1, b: 2 },
111
+ byRefProp: array,
112
+ byComparatorProp: sameProp,
113
+ };
114
+
115
+ const next = {
116
+ byValueProp: { a: 1, b: 3 },
117
+ byRefProp: array,
118
+ byComparatorProp: sameProp,
119
+ };
120
+
121
+ const customComparator = jest.fn().mockReturnValue(true);
122
+
123
+ const result = isEqual({
124
+ byValue: ["byValueProp"],
125
+ byReference: ["byRefProp"],
126
+ byComparator: {
127
+ comparator: customComparator,
128
+ keys: ["byComparatorProp"],
129
+ },
130
+ })(prev, next);
131
+
132
+ expect(result).toBe(false);
133
+ expect(customComparator).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it("byComparator check fails after other checks pass", () => {
137
+ const prev = {
138
+ byValueProp: { a: 1, b: 2 },
139
+ byRefProp: array,
140
+ byComparatorProp: "test",
141
+ };
142
+
143
+ const next = {
144
+ byValueProp: { a: 1, b: 2 },
145
+ byRefProp: array,
146
+ byComparatorProp: "different",
147
+ };
148
+
149
+ const customComparator = jest.fn().mockReturnValue(false);
150
+
151
+ const result = isEqual({
152
+ byValue: ["byValueProp"],
153
+ byReference: ["byRefProp"],
154
+ byComparator: {
155
+ comparator: customComparator,
156
+ keys: ["byComparatorProp"],
157
+ },
158
+ })(prev, next);
159
+
160
+ expect(result).toBe(false);
161
+
162
+ expect(customComparator).toHaveBeenCalledWith(
163
+ prev.byComparatorProp,
164
+ next.byComparatorProp
165
+ );
166
+ });
167
+
168
+ it("remaining keys comparison fails", () => {
169
+ const sharedArray = [1, 2, 3];
170
+
171
+ const prev = {
172
+ byValueProp: { a: 1, b: 2 },
173
+ byRefProp: sharedArray,
174
+ byComparatorProp: "test",
175
+ extraProp: "original",
176
+ };
177
+
178
+ const next = {
179
+ byValueProp: { a: 1, b: 2 },
180
+ byRefProp: sharedArray,
181
+ byComparatorProp: "different",
182
+ extraProp: "changed",
183
+ };
184
+
185
+ const customComparator = jest.fn().mockReturnValue(true);
186
+
187
+ const result = isEqual({
188
+ byValue: ["byValueProp"],
189
+ byReference: ["byRefProp"],
190
+ byComparator: {
191
+ comparator: customComparator,
192
+ keys: ["byComparatorProp"],
193
+ },
194
+ compareRemainingKeysBySameValue: true,
195
+ })(prev, next);
196
+
197
+ expect(result).toBe(false);
198
+ expect(customComparator).toHaveBeenCalled();
199
+ });
200
+
201
+ it("with debug flag", () => {
202
+ const consoleSpy = jest.spyOn(console, "log").mockImplementation();
203
+
204
+ const prev = {
205
+ byValueProp: { a: 1, b: 2 },
206
+ byRefProp: array,
207
+ byComparatorProp: "test",
208
+ extraProp: sameProp,
209
+ };
210
+
211
+ const next = {
212
+ byValueProp: { a: 1, b: 2 },
213
+ byRefProp: array,
214
+ byComparatorProp: "different",
215
+ extraProp: sameProp,
216
+ };
217
+
218
+ const customComparator = jest.fn().mockReturnValue(true);
219
+
220
+ const result = isEqual({
221
+ byValue: ["byValueProp"],
222
+ byReference: ["byRefProp"],
223
+ byComparator: {
224
+ comparator: customComparator,
225
+ keys: ["byComparatorProp"],
226
+ },
227
+ compareRemainingKeysBySameValue: true,
228
+ debug: true,
229
+ })(prev, next);
230
+
231
+ expect(result).toBe(true);
232
+ expect(consoleSpy).toHaveBeenCalled();
233
+
234
+ consoleSpy.mockRestore();
235
+ });
236
+
237
+ it("handles missing properties", () => {
238
+ const prev = {
239
+ byValueProp: { a: 1, b: 2 },
240
+ byRefProp: [1, 2, 3],
241
+ };
242
+
243
+ const next = {
244
+ byValueProp: { a: 1, b: 2 },
245
+ byComparatorProp: "test",
246
+ };
247
+
248
+ const customComparator = jest.fn().mockReturnValue(true);
249
+
250
+ const result = isEqual({
251
+ byValue: ["byValueProp"],
252
+ byReference: ["byRefProp"],
253
+ byComparator: {
254
+ comparator: customComparator,
255
+ keys: ["byComparatorProp"],
256
+ },
257
+ })(prev, next);
258
+
259
+ expect(result).toBe(false);
260
+ expect(customComparator).not.toHaveBeenCalled();
261
+ });
262
+ });
263
+ });
@@ -1,6 +1,8 @@
1
1
  /* eslint no-console: off */
2
2
 
3
- import * as R from "ramda";
3
+ import { equals } from "ramda";
4
+
5
+ const alwaysTrue = () => true;
4
6
 
5
7
  export const isParentFocusEqual = (
6
8
  prevParentFocus: ParentFocus,
@@ -18,7 +20,7 @@ export const isEqual =
18
20
  ({
19
21
  byValue = [],
20
22
  byReference = [],
21
- byComparator = { comparator: R.always(true), keys: [] },
23
+ byComparator = { comparator: alwaysTrue, keys: [] },
22
24
  debug = false,
23
25
  compareRemainingKeysBySameValue = false,
24
26
  }: {
@@ -29,42 +31,59 @@ export const isEqual =
29
31
  compareRemainingKeysBySameValue?: boolean;
30
32
  }) =>
31
33
  (prev, next) => {
32
- const isEqualByValue = R.isEmpty(byValue)
33
- ? true
34
- : R.all((key) => R.equals(prev[key], next[key]), byValue);
34
+ for (const key of byReference) {
35
+ if (prev[key] !== next[key]) {
36
+ if (debug) console.log({ isEqualByReference: false });
35
37
 
36
- const isEqualByReference = R.isEmpty(byReference)
37
- ? true
38
- : R.all((key) => prev[key] === next[key], byReference);
38
+ return false;
39
+ }
40
+ }
39
41
 
40
- const isEqualByComparator = R.isEmpty(byComparator.keys)
41
- ? true
42
- : R.all(
43
- (key) => byComparator.comparator(prev[key], next[key]),
44
- byComparator.keys
45
- );
42
+ for (const key of byValue) {
43
+ if (!equals(prev[key], next[key])) {
44
+ if (debug) console.log({ isEqualByValue: false });
46
45
 
47
- if (debug) {
48
- console.log({ isEqualByValue, isEqualByReference, isEqualByComparator });
46
+ return false;
47
+ }
48
+ }
49
+
50
+ for (const key of byComparator.keys) {
51
+ if (!byComparator.comparator(prev[key], next[key])) {
52
+ if (debug) console.log({ isEqualByComparator: false });
53
+
54
+ return false;
55
+ }
49
56
  }
50
57
 
51
58
  if (compareRemainingKeysBySameValue) {
52
- const undeclaredKeys = R.difference(
53
- [...byValue, ...byReference, byComparator.keys],
54
- Object.keys(prev)
55
- );
59
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
60
+
61
+ const declaredKeys = new Set([
62
+ ...byValue,
63
+ ...byReference,
64
+ ...byComparator.keys,
65
+ ]);
56
66
 
57
- const isRestKeysEqualAllBySameValue = undeclaredKeys.every((key) =>
58
- Object.is(prev[key], next[key])
59
- );
67
+ for (const key of Array.from(allKeys)) {
68
+ if (!declaredKeys.has(key) && !Object.is(prev[key], next[key])) {
69
+ if (debug) console.log({ isRestKeysEqualAllBySameValue: false });
60
70
 
61
- return (
62
- isEqualByValue &&
63
- isEqualByReference &&
64
- isEqualByComparator &&
65
- isRestKeysEqualAllBySameValue
66
- );
71
+ return false;
72
+ }
73
+ }
74
+ }
75
+
76
+ // if we made it here, everything matched
77
+ if (debug) {
78
+ console.log({
79
+ isEqualByValue: true,
80
+ isEqualByReference: true,
81
+ isEqualByComparator: true,
82
+ isRestKeysEqualAllBySameValue: compareRemainingKeysBySameValue
83
+ ? true
84
+ : undefined,
85
+ });
67
86
  }
68
87
 
69
- return isEqualByValue && isEqualByReference && isEqualByComparator;
88
+ return true;
70
89
  };
@@ -250,7 +250,17 @@ class FocusManager {
250
250
  }
251
251
 
252
252
  onDisableFocusChange = (id) => {
253
- if (FocusManager.instance.isFocused(id)) {
253
+ const isFocused = FocusManager.instance.isFocused(id);
254
+
255
+ const isDescendantFocused = (() => {
256
+ try {
257
+ return FocusManager.instance.isAnyDescendantFocused(id);
258
+ } catch (_e) {
259
+ return false;
260
+ }
261
+ })();
262
+
263
+ if (isFocused || isDescendantFocused) {
254
264
  // Move focus to next one
255
265
  const nextFocus = FocusManager.instance.focused?.props
256
266
  ?.nextFocusDown as string;
@@ -8,6 +8,8 @@ import { isNilOrEmpty } from "../reactUtils/helpers";
8
8
  import { isAndroidTVPlatform } from "../reactUtils";
9
9
  import { noop } from "../functionUtils";
10
10
 
11
+ const EMPTY_GROUP_COMPONENT = "empty_group_component";
12
+
11
13
  export function useFocusManager() {
12
14
  return focusManager;
13
15
  }
@@ -74,6 +76,14 @@ export function useFocusEffect(onFocus, focusWatcher) {
74
76
  }, focusWatcher);
75
77
  }
76
78
 
79
+ export function shouldShowEmptyGroup(component) {
80
+ return (
81
+ component?.ui_components?.filter(
82
+ ({ component_type }) => component_type === EMPTY_GROUP_COMPONENT
83
+ )?.length > 0
84
+ );
85
+ }
86
+
77
87
  export const isFocusable = (component: ZappEntry | ZappUIComponent | any) =>
78
88
  R.tryCatch(
79
89
  R.compose(
@@ -101,15 +111,50 @@ export const useIsFocusable = (props: Props): void => {
101
111
  component.rules.hide_component_if_data_is_empty
102
112
  );
103
113
 
114
+ const isLoadingData = !!zappPipesData?.loading;
115
+
104
116
  const hasNoData =
105
117
  isNilOrEmpty(zappPipesData?.data) ||
106
118
  isNilOrEmpty(zappPipesData?.data?.entry);
107
119
 
120
+ const hasNoUIComponents = isNilOrEmpty(component?.ui_components);
121
+
122
+ const hasDataError = !!zappPipesData?.error;
123
+
124
+ const hasEmptyGroup = shouldShowEmptyGroup(component);
125
+
126
+ const isGallery = component?.component_type === "gallery-qb";
127
+
128
+ const isGroup = component?.component_type === "group-qb";
129
+
130
+ const isHiddenDueToEmptyData =
131
+ !isLoadingData && hasNoData && hideIfDataEmpty && !hasEmptyGroup;
132
+
133
+ const isEmptyGroup = isGroup && hasNoUIComponents;
134
+
135
+ const defaultFocusableLogic =
136
+ componentCellsFocusable &&
137
+ (!hasNoData || !hideIfDataEmpty) &&
138
+ !isEmptyGroup;
139
+
108
140
  React.useEffect(() => {
109
141
  if (isAndroidTVPlatform()) {
110
- setIsFocusable(
111
- componentCellsFocusable && (!hasNoData || !hideIfDataEmpty)
112
- );
142
+ let isFocusableNew = false;
143
+
144
+ if (isGallery) {
145
+ isFocusableNew = true;
146
+ } else if (isFocusable(component)) {
147
+ isFocusableNew = defaultFocusableLogic;
148
+ }
149
+
150
+ setIsFocusable(isFocusableNew);
113
151
  }
114
- }, [setIsFocusable, componentCellsFocusable, hasNoData]);
152
+ }, [
153
+ defaultFocusableLogic,
154
+ isHiddenDueToEmptyData,
155
+ isGallery,
156
+ setIsFocusable,
157
+ hasDataError,
158
+ hasEmptyGroup,
159
+ ]);
115
160
  };
@@ -1,5 +1,5 @@
1
1
  import * as R from "ramda";
2
- import { useRef } from "react";
2
+ import { useCallback, useEffect, useRef } from "react";
3
3
 
4
4
  /**
5
5
  *
@@ -9,31 +9,44 @@ import { useRef } from "react";
9
9
  */
10
10
  export const useThrottle = (callback: () => void, delay: number) => {
11
11
  const lastCallRef = useRef<number>(0);
12
- const timeoutRef = useRef<any | null>(null);
12
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
13
+ const callbackRef = useRef<() => void>(callback);
13
14
 
14
- const throttledFunction = () => {
15
+ useEffect(() => {
16
+ callbackRef.current = callback;
17
+ }, [callback]);
18
+
19
+ const throttledFunction = useCallback(() => {
15
20
  const now = Date.now();
16
21
 
17
22
  if (now - lastCallRef.current >= delay) {
18
- callback();
23
+ callbackRef.current();
19
24
  lastCallRef.current = now;
20
25
  } else if (!timeoutRef.current) {
21
26
  const remainingTime = delay - (now - lastCallRef.current);
22
27
 
23
28
  timeoutRef.current = setTimeout(() => {
24
- callback();
29
+ callbackRef.current();
25
30
  lastCallRef.current = Date.now();
26
31
  timeoutRef.current = null;
27
32
  }, remainingTime);
28
33
  }
29
- };
34
+ }, [delay]);
30
35
 
31
- const cancel = () => {
36
+ const cancel = useCallback(() => {
32
37
  if (timeoutRef.current) {
33
38
  clearTimeout(timeoutRef.current);
34
39
  timeoutRef.current = null;
35
40
  }
36
- };
41
+ }, []);
42
+
43
+ useEffect(() => {
44
+ return () => {
45
+ if (timeoutRef.current) {
46
+ clearTimeout(timeoutRef.current);
47
+ }
48
+ };
49
+ }, []);
37
50
 
38
51
  return { throttledFunction, cancel };
39
52
  };
@@ -0,0 +1,37 @@
1
+ import { TestScheduler } from "rxjs/testing";
2
+
3
+ import { withTimeout$ } from "..";
4
+
5
+ describe("withTimeout$", () => {
6
+ let testScheduler: TestScheduler;
7
+
8
+ beforeEach(() => {
9
+ testScheduler = new TestScheduler((actual, expected) => {
10
+ expect(actual).toEqual(expected);
11
+ });
12
+ });
13
+
14
+ it("should emit undefined after the specified timeout", () => {
15
+ testScheduler.run(({ expectObservable }) => {
16
+ const timeout = 1000;
17
+ const expectedMarble = "1s (a|)";
18
+ const expectedValues = { a: undefined };
19
+
20
+ const result$ = withTimeout$(timeout);
21
+
22
+ expectObservable(result$).toBe(expectedMarble, expectedValues);
23
+ });
24
+ });
25
+
26
+ it("should complete after emitting undefined", () => {
27
+ testScheduler.run(({ expectObservable }) => {
28
+ const timeout = 500;
29
+ const expectedMarble = "500ms (a|)";
30
+ const expectedValues = { a: undefined };
31
+
32
+ const result$ = withTimeout$(timeout);
33
+
34
+ expectObservable(result$).toBe(expectedMarble, expectedValues);
35
+ });
36
+ });
37
+ });