@applicaster/zapp-react-native-ui-components 13.0.0-alpha.7120331332 → 13.0.0-alpha.7122212380

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 (48) hide show
  1. package/Components/BaseFocusable/index.ios.ts +1 -1
  2. package/Components/BaseFocusable/index.tsx +66 -29
  3. package/Components/Cell/Cell.tsx +7 -3
  4. package/Components/Cell/TvOSCellComponent.tsx +0 -4
  5. package/Components/Focusable/Focusable.tsx +0 -54
  6. package/Components/Focusable/FocusableTvOS.tsx +1 -1
  7. package/Components/Focusable/Touchable.tsx +19 -20
  8. package/Components/FocusableList/index.tsx +0 -1
  9. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +66 -0
  10. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +4 -1
  11. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +6 -12
  12. package/Components/MasterCell/DefaultComponents/ImageBorderContainer/__tests__/index.test.ts +93 -0
  13. package/Components/MasterCell/DefaultComponents/SecondaryImage/utils.ts +1 -1
  14. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +1 -1
  15. package/Components/MasterCell/dataAdapter.ts +10 -3
  16. package/Components/MasterCell/hooks/useAsyncRendering/index.ts +2 -2
  17. package/Components/MasterCell/utils/index.ts +21 -7
  18. package/Components/PlayerContainer/PlayerContainer.tsx +3 -7
  19. package/Components/PlayerContainer/PlayerContainerContext.tsx +9 -0
  20. package/Components/River/__tests__/componentsMap.test.js +14 -1
  21. package/Components/Tabs/Tab.tsx +6 -6
  22. package/Components/TextInputTv/index.tsx +2 -2
  23. package/Components/Transitioner/AnimationManager.js +8 -8
  24. package/Components/VideoLive/LiveImageManager.ts +27 -1
  25. package/Components/VideoLive/PlayerLiveImageComponent.tsx +18 -16
  26. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +51 -1
  27. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +0 -5
  28. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +5 -1
  29. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +5 -1
  30. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +8 -7
  31. package/Components/VideoModal/ModalAnimation/utils.ts +2 -2
  32. package/Components/VideoModal/OpaqueLayer.tsx +33 -0
  33. package/Components/VideoModal/PlayerWrapper.tsx +16 -35
  34. package/Components/VideoModal/VideoModal.tsx +14 -23
  35. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -1
  36. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +0 -90
  37. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +89 -0
  38. package/Components/VideoModal/hooks/index.ts +7 -0
  39. package/Components/VideoModal/hooks/useDelayedPlayerDetails.ts +49 -0
  40. package/Components/VideoModal/hooks/utils/__tests__/showDetails.test.ts +91 -0
  41. package/Components/VideoModal/hooks/utils/index.ts +33 -0
  42. package/Components/VideoModal/utils.ts +1 -1
  43. package/Components/default-cell-renderer/__tests__/defaultCellRenderer.test.tsx +7 -3
  44. package/Components/default-cell-renderer/viewTrees/tv/DefaultCell/__tests__/DefaultCell.test.tsx +5 -1
  45. package/Contexts/ScreenContext/index.tsx +3 -2
  46. package/Decorators/ZappPipesDataConnector/__tests__/Hero.js +1 -1
  47. package/Decorators/ZappPipesDataConnector/index.tsx +31 -1
  48. package/package.json +12 -6
@@ -25,7 +25,7 @@ type Props = {
25
25
  };
26
26
 
27
27
  export class BaseFocusable<
28
- T extends Props = Props
28
+ T extends Props = Props,
29
29
  > extends BaseFocusableInterface<T> {
30
30
  constructor(props) {
31
31
  super(props);
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import * as R from "ramda";
2
3
 
3
4
  import {
4
5
  BaseFocusable as BaseFocusableInterface,
@@ -7,7 +8,6 @@ import {
7
8
 
8
9
  import { createReactRef } from "@applicaster/zapp-react-native-utils/reactUtils";
9
10
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
10
- import * as FOCUS_EVENTS from "@applicaster/zapp-react-native-utils/appUtils/focusManager/events";
11
11
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
12
12
 
13
13
  type ScrollDirection = FocusManager.IOS.Direction | FocusManager.Web.Direction;
@@ -35,7 +35,7 @@ type Props = {
35
35
  };
36
36
 
37
37
  export class BaseFocusable<
38
- T extends Props = Props
38
+ T extends Props = Props,
39
39
  > extends BaseFocusableInterface<T> {
40
40
  constructor(props) {
41
41
  super(props);
@@ -131,12 +131,6 @@ export class BaseFocusable<
131
131
  willLoseFocus: FocusManager.FocusEventCB = (focusable, scrollDirection) => {
132
132
  const { willLoseFocus = noop } = this.props;
133
133
  willLoseFocus(focusable, scrollDirection);
134
-
135
- focusManager.invokeHandler?.(
136
- FOCUS_EVENTS.WILL_LOSE_FOCUS,
137
- this,
138
- scrollDirection
139
- );
140
134
  };
141
135
 
142
136
  willReceiveFocus: FocusManager.FocusEventCB = (
@@ -145,12 +139,6 @@ export class BaseFocusable<
145
139
  ) => {
146
140
  const { willReceiveFocus = noop } = this.props;
147
141
  willReceiveFocus(focusable, scrollDirection);
148
-
149
- focusManager.invokeHandler?.(
150
- FOCUS_EVENTS.WILL_RECEIVE_FOCUS,
151
- this,
152
- scrollDirection
153
- );
154
142
  };
155
143
 
156
144
  /**
@@ -163,8 +151,6 @@ export class BaseFocusable<
163
151
  const { onFocus = noop } = this.props;
164
152
  this.setFocusedState(true);
165
153
  onFocus(focusable, scrollDirection);
166
-
167
- focusManager.invokeHandler?.(FOCUS_EVENTS.FOCUS, this, scrollDirection);
168
154
  };
169
155
 
170
156
  /**
@@ -179,12 +165,6 @@ export class BaseFocusable<
179
165
  ) => {
180
166
  const { hasReceivedFocus = noop } = this.props;
181
167
  hasReceivedFocus(focusable, scrollDirection);
182
-
183
- focusManager.invokeHandler?.(
184
- FOCUS_EVENTS.HAS_RECEIVED_FOCUS,
185
- this,
186
- scrollDirection
187
- );
188
168
  };
189
169
 
190
170
  /**
@@ -196,12 +176,6 @@ export class BaseFocusable<
196
176
  hasLostFocus: FocusManager.FocusEventCB = (focusable, scrollDirection) => {
197
177
  const { hasLostFocus = noop } = this.props;
198
178
  hasLostFocus(focusable, scrollDirection);
199
-
200
- focusManager.invokeHandler?.(
201
- FOCUS_EVENTS.HAS_LOST_FOCUS,
202
- this,
203
- scrollDirection
204
- );
205
179
  };
206
180
 
207
181
  /**
@@ -228,7 +202,6 @@ export class BaseFocusable<
228
202
  const { onBlur = noop } = this.props;
229
203
  this.setFocusedState(false);
230
204
  onBlur(focusable, scrollDirection);
231
- focusManager.invokeHandler?.(FOCUS_EVENTS.BLUR, this, scrollDirection);
232
205
  };
233
206
 
234
207
  onPress = (keyEvent?: Nullable<React.MouseEvent>) => {
@@ -269,4 +242,68 @@ export class BaseFocusable<
269
242
  this.setState({ focused });
270
243
  }
271
244
  };
245
+
246
+ /**
247
+ * will invoke the underlying component's focus method
248
+ * @param {Object} scrollDirection
249
+ * @returns {Promise}
250
+ */
251
+ focus(_, scrollDirection) {
252
+ return this.onFocus(this, scrollDirection); // invokeComponentMethod(this, "onFocus", scrollDirection);
253
+ }
254
+
255
+ /**
256
+ * will invoke the underlying component's blur method
257
+ * @param {Object} scrollDirection
258
+ * @returns {Promise}
259
+ */
260
+ blur(
261
+ _,
262
+ scrollDirection?: FocusManager.Web.Direction | FocusManager.IOS.Direction
263
+ ) {
264
+ return this.onBlur(this, scrollDirection);
265
+ }
266
+
267
+ /**
268
+ * Sets the focus on this item. Will trigger sequentially a sequence of
269
+ * functions (willReceiveFocus, focus, hasReceivedFocus). If these functions (defined in the Focusable
270
+ * Item underlying component) return promises, execution will wait before it proceeds to the next. This
271
+ * is useful for triggering sequential operations that specifically require to fully run before or after
272
+ * focus is actually set on that item.
273
+ * @param {string} scrollDirection string representation of the direction of the navigation which landed
274
+ * to this item being focused
275
+ */
276
+ _executeFocusSequence(methodNames, scrollDirection) {
277
+ return R.reduce(
278
+ (sequence, methodName) => {
279
+ const method = this[methodName]; // Access the method by name
280
+
281
+ if (typeof method !== "function") {
282
+ throw new Error(
283
+ `Method '${methodName}' not found or not a function.`
284
+ );
285
+ }
286
+
287
+ return sequence
288
+ .then(() => method.call(this, scrollDirection))
289
+ .catch((e) => {
290
+ throw e; // Re-throw for consistent error handling
291
+ });
292
+ },
293
+ Promise.resolve(),
294
+ methodNames
295
+ );
296
+ }
297
+
298
+ setFocus(scrollDirection) {
299
+ const focusMethods = ["willReceiveFocus", "focus", "hasReceivedFocus"];
300
+
301
+ return this._executeFocusSequence(focusMethods, scrollDirection);
302
+ }
303
+
304
+ setBlur(scrollDirection) {
305
+ const blurMethods = ["willLoseFocus", "blur", "hasLostFocus"];
306
+
307
+ return this._executeFocusSequence(blurMethods, scrollDirection);
308
+ }
272
309
  }
@@ -273,10 +273,14 @@ export class CellComponent extends React.Component<Props, State> {
273
273
  if (isFocused) {
274
274
  const accessibilityManager = AccessibilityManager.getInstance();
275
275
 
276
+ const accessibilityTitle =
277
+ item?.extensions?.accessibility?.label || item?.title || "";
278
+
279
+ const accessibilityHint =
280
+ item?.extensions?.accessibility?.hint || "";
281
+
276
282
  accessibilityManager.readText({
277
- text: String(
278
- item.extensions?.accessibility?.label || item.title
279
- ),
283
+ text: `${accessibilityTitle} ${accessibilityHint}`,
280
284
  });
281
285
  }
282
286
 
@@ -10,10 +10,6 @@ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focu
10
10
  import { sendSelectCellEvent } from "@applicaster/zapp-react-native-utils/analyticsUtils";
11
11
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
12
12
  import { CellWithFocusable } from "./CellWithFocusable";
13
- import { getCellState } from "./utils";
14
-
15
- const _getCellState = (focused, selected) =>
16
- getCellState({ focused, selected });
17
13
 
18
14
  type Props = {
19
15
  item: ZappEntry;
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
3
2
 
4
3
  import { BaseFocusable } from "../BaseFocusable";
5
4
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
@@ -42,8 +41,6 @@ class Focusable extends BaseFocusable<Props> {
42
41
  this.resetLongPressTimeout = this.resetLongPressTimeout.bind(this);
43
42
  this.longPress = this.longPress.bind(this);
44
43
  this.press = this.press.bind(this);
45
- this.focus = this.focus.bind(this);
46
- this.blur = this.blur.bind(this);
47
44
  }
48
45
 
49
46
  /**
@@ -55,37 +52,6 @@ class Focusable extends BaseFocusable<Props> {
55
52
  return this.preferredFocus();
56
53
  }
57
54
 
58
- /**
59
- * Sets the focus on this item. Will trigger sequentially a sequence of
60
- * functions (willReceiveFocus, focus, hasReceivedFocus). If these functions (defined in the Focusable
61
- * Item underlying component) return promises, execution will wait before it proceeds to the next. This
62
- * is useful for triggering sequential operations that specifically require to fully run before or after
63
- * focus is actually set on that item.
64
- * @param {string} scrollDirection string representation of the direction of the navigation which landed
65
- * to this item being focused
66
- */
67
- setFocus(scrollDirection) {
68
- const focusMethods = [
69
- this.willReceiveFocus,
70
- this.focus,
71
- this.hasReceivedFocus,
72
- ];
73
-
74
- const self = this;
75
-
76
- return R.reduce(
77
- (sequence, method) => {
78
- return sequence
79
- .then(() => method.apply(self, [scrollDirection]))
80
- .catch((e) => {
81
- throw e;
82
- });
83
- },
84
- Promise.resolve(),
85
- focusMethods
86
- );
87
- }
88
-
89
55
  startLongPressTimeout() {
90
56
  this.longPressTimeout = setTimeout(() => {
91
57
  this.longPress(null);
@@ -102,26 +68,6 @@ class Focusable extends BaseFocusable<Props> {
102
68
  }
103
69
  }
104
70
 
105
- /**
106
- * will invoke the underlying component's focus method
107
- * @param {Object} scrollDirection
108
- * @returns {Promise}
109
- */
110
- focus(scrollDirection) {
111
- return this.onFocus(this, scrollDirection); // invokeComponentMethod(this, "onFocus", scrollDirection);
112
- }
113
-
114
- /**
115
- * will invoke the underlying component's blur method
116
- * @param {Object} scrollDirection
117
- * @returns {Promise}
118
- */
119
- blur(
120
- scrollDirection?: FocusManager.Web.Direction | FocusManager.IOS.Direction
121
- ) {
122
- return this.onBlur(this, scrollDirection);
123
- }
124
-
125
71
  /**
126
72
  * will invoke the underlying component's press method
127
73
  * @param {Object} keyEvent
@@ -90,7 +90,7 @@ export class Focusable extends BaseFocusable<Props> {
90
90
  onBlur(nativeEvent);
91
91
  }
92
92
 
93
- setFocus(direction, callback) {
93
+ setFocus(_direction, callback) {
94
94
  const focusMethods = [
95
95
  { method: this.willReceiveFocus },
96
96
  { method: this.focus, args: [callback] },
@@ -1,25 +1,24 @@
1
1
  import * as React from "react";
2
2
 
3
- type Props =
4
- | {
5
- id: string;
6
- children: React.ReactNode;
7
- onPress?: (ref: FocusManager.TouchableRef) => void;
8
- onPressIn?: (ref: FocusManager.TouchableRef) => void;
9
- onPressOut?: (ref: FocusManager.TouchableRef) => void;
10
- onLongPress?: (ref: FocusManager.TouchableRef) => void;
11
- onFocus?: (
12
- ref: FocusManager.TouchableRef,
13
- options: FocusManager.Android.CallbackOptions
14
- ) => void;
15
- onBlur?: (
16
- ref: FocusManager.TouchableRef,
17
- options: FocusManager.Android.CallbackOptions
18
- ) => void;
19
-
20
- disableFocus?: boolean;
21
- blockFocus?: boolean;
22
- } & Partial<ParentFocus>;
3
+ type Props = {
4
+ id: string;
5
+ children: React.ReactNode;
6
+ onPress?: (ref: FocusManager.TouchableRef) => void;
7
+ onPressIn?: (ref: FocusManager.TouchableRef) => void;
8
+ onPressOut?: (ref: FocusManager.TouchableRef) => void;
9
+ onLongPress?: (ref: FocusManager.TouchableRef) => void;
10
+ onFocus?: (
11
+ ref: FocusManager.TouchableRef,
12
+ options: FocusManager.Android.CallbackOptions
13
+ ) => void;
14
+ onBlur?: (
15
+ ref: FocusManager.TouchableRef,
16
+ options: FocusManager.Android.CallbackOptions
17
+ ) => void;
18
+
19
+ disableFocus?: boolean;
20
+ blockFocus?: boolean;
21
+ } & Partial<ParentFocus>;
23
22
 
24
23
  export class Touchable extends React.Component<Props> {
25
24
  onPress(focusableRef: FocusManager.TouchableRef): void {
@@ -87,7 +87,6 @@ function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
87
87
  initialFocusDirection = "down",
88
88
  data = [],
89
89
  // eslint-disable-next-line unused-imports/no-unused-vars
90
- onAllComponentsLoaded, // TODO: re-implement it
91
90
  omitPropsPropagation = [],
92
91
  } = props;
93
92
 
@@ -0,0 +1,66 @@
1
+ import {
2
+ BorderContainerView,
3
+ getBorderPadding, // Export for testing (using a double underscore prefix is a common convention)
4
+ } from "../index";
5
+ import * as React from "react";
6
+ import { render } from "@testing-library/react-native";
7
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
8
+ import { View } from "react-native";
9
+
10
+ jest.mock("@applicaster/zapp-react-native-utils/numberUtils", () => ({
11
+ toNumberWithDefaultZero: jest.fn((value) => Number(value) || 0),
12
+ }));
13
+
14
+ describe("BorderContainerView", () => {
15
+ describe("getBorderPadding", () => {
16
+ it("returns 0 for inside", () => {
17
+ expect(getBorderPadding("inside", 10)).toBe(0);
18
+ });
19
+
20
+ it("returns borderWidth / 2 for center", () => {
21
+ expect(getBorderPadding("center", 10)).toBe(5);
22
+ });
23
+
24
+ it("returns borderWidth for outside", () => {
25
+ expect(getBorderPadding("outside", 10)).toBe(10);
26
+ });
27
+
28
+ it("returns borderWidth for invalid position", () => {
29
+ // @ts-ignore
30
+ expect(getBorderPadding("other_value", 10)).toBe(10);
31
+ });
32
+ });
33
+
34
+ it("Border component renders null if no borderPosition", () => {
35
+ const style = { borderWidth: 5, borderRadius: 10, borderColor: "red" };
36
+
37
+ const padding = {
38
+ paddingTop: 2,
39
+ paddingRight: 3,
40
+ paddingBottom: 4,
41
+ paddingLeft: 5,
42
+ };
43
+
44
+ const borderPosition = null;
45
+
46
+ const { queryByTestId } = render(
47
+ <BorderContainerView
48
+ style={style}
49
+ testID="border-container"
50
+ borderPosition={borderPosition}
51
+ borderPaddingTop={toNumberWithDefaultZero(padding.paddingTop)}
52
+ borderPaddingRight={toNumberWithDefaultZero(padding.paddingRight)}
53
+ borderPaddingBottom={toNumberWithDefaultZero(padding.paddingBottom)}
54
+ borderPaddingLeft={toNumberWithDefaultZero(padding.paddingLeft)}
55
+ >
56
+ <View testID="child" />
57
+ </BorderContainerView>
58
+ );
59
+
60
+ const children = queryByTestId("border-container").children;
61
+
62
+ expect(children[1].props.testID).toBe("child");
63
+
64
+ expect(children[0].children.length).toBe(0);
65
+ });
66
+ });
@@ -28,7 +28,10 @@ const styles = StyleSheet.create({
28
28
  },
29
29
  });
30
30
 
31
- const getBorderPadding = (borderPosition: BorderPosition, borderWidth) => {
31
+ export const getBorderPadding = (
32
+ borderPosition: BorderPosition,
33
+ borderWidth: number
34
+ ) => {
32
35
  switch (borderPosition) {
33
36
  case "inside":
34
37
  return 0;
@@ -38,22 +38,16 @@ 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
44
- const defaultPlaceHolderImage = React.useMemo(
45
- () => ({ uri: placeholderImage || "" }),
46
- [source, error]
47
- );
41
+ const _source = shouldRenderImg
42
+ ? withDimensions(source)
43
+ : { uri: placeholderImage };
48
44
 
49
45
  return (
50
- // @ts-ignore
51
46
  <MemoizedImage
52
47
  style={style as ImageStyle}
53
- onError={() => setErrorState(true)}
54
- source={
55
- shouldRenderImg ? withDimensions(source) : defaultPlaceHolderImage
56
- }
48
+ onError={React.useCallback(() => setErrorState(true), [])}
49
+ // as we have defaults as "" for placeholder image, we need to pass undefined to source to not throw warnings
50
+ source={_source?.uri ? _source : undefined}
57
51
  {...R.omit(["source"], otherProps)}
58
52
  />
59
53
  );
@@ -0,0 +1,93 @@
1
+ import { ImageBorderContainer } from "../index";
2
+ import { getImageContainerMarginStyles } from "@applicaster/zapp-react-native-utils/cellUtils";
3
+
4
+ const mockValue = (key) => {
5
+ const values = {
6
+ cell_padding_top: 1,
7
+ cell_padding_left: 2,
8
+ cell_padding_right: 3,
9
+ cell_padding_bottom: 4,
10
+ image_border_size: 5,
11
+ image_focused_border_color: "red",
12
+ image_selected_border_color: "blue",
13
+ image_focused_selected_border_color: "green",
14
+ image_border_color: "black",
15
+ image_corner_radius: 10,
16
+ image_border_position: "outside",
17
+ image_border_padding_top: 6,
18
+ image_border_padding_right: 7,
19
+ image_border_padding_bottom: 8,
20
+ image_border_padding_left: 9,
21
+ image_margin_top: 11,
22
+ image_margin_left: 12,
23
+ image_margin_right: 13,
24
+ image_margin_bottom: 14,
25
+ };
26
+
27
+ return values[key] || 0;
28
+ };
29
+
30
+ describe("ImageBorderContainer", () => {
31
+ it("calculates style properties correctly", () => {
32
+ const marginStyles = getImageContainerMarginStyles({ value: mockValue });
33
+
34
+ expect(marginStyles.marginTop).toBe(11);
35
+ expect(marginStyles.marginLeft).toBe(12);
36
+ expect(marginStyles.marginRight).toBe(13);
37
+ expect(marginStyles.marginBottom).toBe(14);
38
+
39
+ const props = {
40
+ value: mockValue,
41
+ state: "default",
42
+ imageStyles: {},
43
+ };
44
+
45
+ const result = ImageBorderContainer(props);
46
+
47
+ expect(result.style.marginTop).toBe(12); // 11 + 1
48
+ expect(result.style.marginLeft).toBe(14); // 12 + 2
49
+ expect(result.style.marginRight).toBe(16); // 13 + 3
50
+ expect(result.style.marginBottom).toBe(18); // 14 + 4
51
+ expect(result.style.borderWidth).toBe(5);
52
+ expect(result.style.borderColor).toBe("black");
53
+ expect(result.style.borderRadius).toBe(10);
54
+ expect(result.additionalProps.borderPosition).toBe("outside");
55
+ expect(result.additionalProps.borderPaddingTop).toBe(6);
56
+ expect(result.additionalProps.borderPaddingRight).toBe(7);
57
+ expect(result.additionalProps.borderPaddingBottom).toBe(8);
58
+ expect(result.additionalProps.borderPaddingLeft).toBe(9);
59
+ });
60
+
61
+ it("handles focused state correctly", () => {
62
+ const props = {
63
+ value: mockValue,
64
+ state: "focused",
65
+ imageStyles: {},
66
+ };
67
+
68
+ const result = ImageBorderContainer(props);
69
+ expect(result.style.borderColor).toBe("red");
70
+ });
71
+
72
+ it("handles selected state correctly", () => {
73
+ const props = {
74
+ value: mockValue,
75
+ state: "selected",
76
+ imageStyles: {},
77
+ };
78
+
79
+ const result = ImageBorderContainer(props);
80
+ expect(result.style.borderColor).toBe("blue");
81
+ });
82
+
83
+ it("handles focused and selected state correctly", () => {
84
+ const props = {
85
+ value: mockValue,
86
+ state: "focused_selected",
87
+ imageStyles: {},
88
+ };
89
+
90
+ const result = ImageBorderContainer(props);
91
+ expect(result.style.borderColor).toBe("green");
92
+ });
93
+ });
@@ -17,7 +17,7 @@ const SECONDARY_IMAGE_PREFIX = "secondary_image";
17
17
 
18
18
  type ImageSizing = typeof IMAGE_SIZING_FIT | typeof IMAGE_SIZING_FILL;
19
19
 
20
- type ImagePosition = typeof IMAGE_POSITION[keyof typeof IMAGE_POSITION];
20
+ type ImagePosition = (typeof IMAGE_POSITION)[keyof typeof IMAGE_POSITION];
21
21
 
22
22
  export type DisplayMode =
23
23
  | typeof DISPLAY_MODE_FIXED
@@ -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).toEqual({ uri: "" });
42
+ expect(testInstance.findByType(Image).props.source).toBeUndefined();
43
43
  });
44
44
  });
@@ -35,7 +35,6 @@ function retrieveData(entry, func, args) {
35
35
  * @param {?[Object]} element.elements Optional array of nested elements to render within the current node
36
36
  * @returns {Object} inflated configuration, ready to be rendered by the master cell's element mapper
37
37
  */
38
-
39
38
  export function configInflater(
40
39
  entry: any,
41
40
  {
@@ -65,7 +64,7 @@ export function configInflater(
65
64
 
66
65
  if (Array.isArray(elements)) {
67
66
  adjustedElements = elements.map((element) =>
68
- configInflater(entry, element as any)
67
+ configInflater(entry, element)
69
68
  );
70
69
  }
71
70
 
@@ -77,6 +76,14 @@ export function configInflater(
77
76
  };
78
77
  }
79
78
 
79
+ /**
80
+ * Memoized version of configInflater to cache results based on entry and masterCellConfig
81
+ */
82
+ const memoizedConfigInflater = R.memoizeWith(
83
+ (entry, config) => `${entry.id}-${JSON.stringify(config)}`,
84
+ configInflater
85
+ );
86
+
80
87
  /**
81
88
  * Return the proper view tree according to entry's content type and UI state.
82
89
  * Falls back to "default" content type or state.
@@ -97,7 +104,7 @@ function resolveElementsNode(entry, state, elements) {
97
104
  export function defaultDataAdapter(elements) {
98
105
  return function elementsBuilder({ entry, state = "default" }) {
99
106
  return resolveElementsNode(entry, state, elements).map((element) =>
100
- configInflater(entry, element)
107
+ memoizedConfigInflater(entry, element)
101
108
  );
102
109
  };
103
110
  }
@@ -8,7 +8,7 @@ import { MasterCellAsyncRenderManager } from "./MasterCellAsyncRenderManager";
8
8
  const layoutMeasure = (viewRef, onMeasure) => {
9
9
  viewRef?.measureLayout?.(
10
10
  findNodeHandle(viewRef),
11
- (x, y, width, height) => {
11
+ (_x, _y, width, height) => {
12
12
  onMeasure({ width, height });
13
13
  },
14
14
  noop
@@ -16,7 +16,7 @@ const layoutMeasure = (viewRef, onMeasure) => {
16
16
  };
17
17
 
18
18
  const regularMeasure = (viewRef, onMeasure) => {
19
- viewRef?.measure?.((x, y, width, height) => {
19
+ viewRef?.measure?.((_x, _y, width, height) => {
20
20
  onMeasure({ width, height });
21
21
  });
22
22
  };
@@ -78,13 +78,17 @@ export function resolveColor(entry, style) {
78
78
  return style;
79
79
  }
80
80
 
81
- // TODO can be optimized to remove 3 O(n) loops
82
- const styleKeys = Object.keys(style);
83
- const colorKeys = styleKeys.filter((key) => /color/i.test(key));
81
+ // Iterate through style object keys and resolve any color properties
82
+ // non-color properties are passed through unchanged
83
+ const resolvedStyle = Object.keys(style).reduce((acc, key) => {
84
+ if (key.includes("color") || key.includes("Color")) {
85
+ return { ...acc, ...resolveColorForProp(entry, acc, key) };
86
+ }
84
87
 
85
- return colorKeys.reduce((acc, value) => {
86
- return { ...style, ...resolveColorForProp(entry, acc, value) };
88
+ return acc;
87
89
  }, style);
90
+
91
+ return resolvedStyle;
88
92
  }
89
93
 
90
94
  export function isVideoPreviewEnabled({
@@ -103,7 +107,7 @@ export const useFilterChildren = <
103
107
  item: any;
104
108
  pluginIdentifier: string;
105
109
  };
106
- }
110
+ },
107
111
  >(
108
112
  children: T[]
109
113
  ): T[] => {
@@ -339,13 +343,23 @@ export const useCellState = ({
339
343
  return getCellState({ focused, selected: _isSelected });
340
344
  };
341
345
 
346
+ const cache = new Map<string, any>();
347
+
342
348
  export const hasFocusableInsideBuilder = (elementsBuilder) => (item) => {
343
349
  const elements = elementsBuilder({ entry: item });
344
350
 
345
- return R.anyPass([
351
+ if (cache.has(item.id)) {
352
+ return cache.get(item.id);
353
+ }
354
+
355
+ const result = R.anyPass([
346
356
  hasElementsSpecificViewType("ButtonContainerView"),
347
357
  hasElementsSpecificViewType("FocusableView"),
348
358
  ])(elements);
359
+
360
+ cache.set(item.id, result);
361
+
362
+ return result;
349
363
  };
350
364
 
351
365
  export function getEntryState(state, selected) {