@applicaster/zapp-react-native-ui-components 16.0.0-rc.117 → 16.0.0-rc.119

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.
@@ -76,6 +76,66 @@ describe("masterCellBuilder", () => {
76
76
  });
77
77
  });
78
78
 
79
+ describe("cellOptions.interactive", () => {
80
+ // renders a single View tagged with the state the cell was built with
81
+ const dataAdapter =
82
+ () =>
83
+ ({ state }) => [
84
+ { type: "View", style: {}, props: { testID: `state-${state}` } },
85
+ ];
86
+
87
+ // Only the root View gets this testID: cell elements pass their own testID, which overrides it
88
+ const ROOT_TEST_ID = "master-cell-root";
89
+
90
+ // forwardRef: MasterCell attaches a ref to its root View
91
+ const TestView = React.forwardRef((props, ref) => (
92
+ <View testID={ROOT_TEST_ID} {...props} ref={ref} />
93
+ ));
94
+
95
+ const renderCell = (cellOptions, state) => {
96
+ const MasterCell = masterCellBuilder({
97
+ cellConfiguration: singleView,
98
+ dataAdapter,
99
+ components: { View: TestView },
100
+ cellOptions,
101
+ });
102
+
103
+ return renderWithProviders(<MasterCell item={entry} state={state} />);
104
+ };
105
+
106
+ const getRoot = (wrapper) => wrapper.getByTestId(ROOT_TEST_ID);
107
+
108
+ it("keeps focused state and default root props when omitted", () => {
109
+ const wrapper = renderCell(undefined, "focused");
110
+ const root = getRoot(wrapper);
111
+
112
+ expect(wrapper.getByTestId("state-focused")).toBeDefined();
113
+ expect(root.props.onStartShouldSetResponder).toBeUndefined();
114
+ expect(root.props.collapsable).toBeUndefined();
115
+ });
116
+
117
+ it("claims body touches on a non-collapsable root when false", () => {
118
+ const wrapper = renderCell({ interactive: false }, "default");
119
+ const root = getRoot(wrapper);
120
+
121
+ expect(root.props.onStartShouldSetResponder()).toBe(true);
122
+ expect(root.props.collapsable).toBe(false);
123
+ });
124
+
125
+ it("renders focused as default when false", () => {
126
+ const wrapper = renderCell({ interactive: false }, "focused");
127
+
128
+ expect(wrapper.getByTestId("state-default")).toBeDefined();
129
+ expect(wrapper.queryByTestId("state-focused")).toBeNull();
130
+ });
131
+
132
+ it("renders focused_selected as selected when false", () => {
133
+ const wrapper = renderCell({ interactive: false }, "focused_selected");
134
+
135
+ expect(wrapper.getByTestId("state-selected")).toBeDefined();
136
+ });
137
+ });
138
+
79
139
  describe.skip("when using custom default components", () => {
80
140
  // the collapsable prop is added here to easily check from the snapshot that the mock
81
141
  // component is being used instead of the default one
@@ -10,7 +10,11 @@ import { useScreenState } from "@applicaster/zapp-react-native-utils/screenState
10
10
  import { defaultComponents } from "./DefaultComponents";
11
11
  import { defaultDataAdapter } from "./dataAdapter";
12
12
  import { elementMapper, mapElementWithKey } from "./elementMapper";
13
- import { getEntryState, hasFocusableInsideBuilder } from "./utils";
13
+ import {
14
+ getEntryState,
15
+ hasFocusableInsideBuilder,
16
+ withoutFocusedState,
17
+ } from "./utils";
14
18
 
15
19
  type Props = {
16
20
  item: ZappEntry;
@@ -19,6 +23,16 @@ type Props = {
19
23
 
20
24
  const defaultContainerStyle = { flex: 1 };
21
25
 
26
+ /**
27
+ * Root props of a non-interactive cell: the root view claims touches on the cell body,
28
+ * so the host list's touchable never fires (buttons, being deeper, still receive theirs).
29
+ * `collapsable: false` keeps Android / Fabric from flattening the view away.
30
+ */
31
+ const nonInteractiveRootProps = {
32
+ onStartShouldSetResponder: () => true,
33
+ collapsable: false,
34
+ };
35
+
22
36
  /**
23
37
  * Constructs a MasterCell with the provided options
24
38
  * @param {Object} options
@@ -27,6 +41,8 @@ const defaultContainerStyle = { flex: 1 };
27
41
  * @param {Function} options.dataAdapter optional dataAdapter to use instead of the default one.
28
42
  * See dataAdapter.js for more info
29
43
  * @param {containerStyles} options.containerStyle style to apply at the top level of the master cell
44
+ * @param {Object} options.cellOptions optional cell options. `interactive: false` makes the cell body
45
+ * ignore taps and render without focused / pressed states (inner buttons keep working). Defaults to `true`.
30
46
  * @returns {Function} Functional React Component to render the masterCell with the provided options
31
47
  */
32
48
 
@@ -39,6 +55,7 @@ type MasterCellBuilderOptions = {
39
55
  isRTL?: boolean;
40
56
  skipButtons?: boolean;
41
57
  component?: ZappComponent;
58
+ interactive?: boolean;
42
59
  };
43
60
  };
44
61
 
@@ -49,7 +66,7 @@ export function masterCellBuilder({
49
66
  containerStyle = defaultContainerStyle,
50
67
  cellOptions = {},
51
68
  }: MasterCellBuilderOptions) {
52
- const { skipButtons = false, component } = cellOptions;
69
+ const { skipButtons = false, component, interactive = true } = cellOptions;
53
70
 
54
71
  const componentsMap = R.merge(defaultComponents, components);
55
72
  const { View } = componentsMap;
@@ -88,7 +105,10 @@ export function masterCellBuilder({
88
105
  () =>
89
106
  elementsBuilder({
90
107
  entry: item,
91
- state: getEntryState(state, entryIsSelected),
108
+ state: getEntryState(
109
+ interactive ? state : withoutFocusedState(state),
110
+ entryIsSelected
111
+ ),
92
112
  allowDynamicColorsOutsideExtensions:
93
113
  theme?.allow_dynamic_colors_outside_extensions,
94
114
  }),
@@ -134,7 +154,11 @@ export function masterCellBuilder({
134
154
  );
135
155
 
136
156
  return (
137
- <View style={containerStyle} ref={wrapperRef}>
157
+ <View
158
+ style={containerStyle}
159
+ ref={wrapperRef}
160
+ {...(interactive ? undefined : nonInteractiveRootProps)}
161
+ >
138
162
  {elements.map(remapElements)}
139
163
  </View>
140
164
  );
@@ -0,0 +1,20 @@
1
+ import { withoutFocusedState } from "..";
2
+
3
+ describe("withoutFocusedState", () => {
4
+ it("maps focused to default", () => {
5
+ expect(withoutFocusedState("focused")).toBe("default");
6
+ });
7
+
8
+ it("maps focused_selected to selected", () => {
9
+ expect(withoutFocusedState("focused_selected")).toBe("selected");
10
+ });
11
+
12
+ it("keeps default and selected states", () => {
13
+ expect(withoutFocusedState("default")).toBe("default");
14
+ expect(withoutFocusedState("selected")).toBe("selected");
15
+ });
16
+
17
+ it("keeps a missing state", () => {
18
+ expect(withoutFocusedState(undefined)).toBeUndefined();
19
+ });
20
+ });
@@ -386,3 +386,21 @@ export function getEntryState(state, selected) {
386
386
 
387
387
  return selected ? "selected" : "default";
388
388
  }
389
+
390
+ /**
391
+ * Drops focused (pressed) variants of a cell state, so a non-interactive cell
392
+ * always renders its idle look. `selected` is kept – it is not touch feedback.
393
+ */
394
+ export function withoutFocusedState(
395
+ state?: CellState | null
396
+ ): CellState | null | undefined {
397
+ if (state === "focused_selected") {
398
+ return "selected";
399
+ }
400
+
401
+ if (state === "focused") {
402
+ return "default";
403
+ }
404
+
405
+ return state;
406
+ }
@@ -14,6 +14,7 @@ type Props = {
14
14
  inline?: boolean;
15
15
  docked?: boolean;
16
16
  pip?: boolean;
17
+ vertical?: boolean;
17
18
  isModal?: boolean;
18
19
  fullscreen?: boolean;
19
20
  isTabletPortrait?: boolean;
@@ -32,6 +33,7 @@ const PlayerWrapperComponent = (props: Props) => {
32
33
  isTabletPortrait,
33
34
  configuration,
34
35
  pip,
36
+ vertical,
35
37
  playerContent,
36
38
  } = props;
37
39
 
@@ -42,6 +44,7 @@ const PlayerWrapperComponent = (props: Props) => {
42
44
  isTabletPortrait,
43
45
  configuration,
44
46
  pip,
47
+ vertical,
45
48
  });
46
49
 
47
50
  const isTablet = useIsTablet();
@@ -52,7 +55,7 @@ const PlayerWrapperComponent = (props: Props) => {
52
55
 
53
56
  return (
54
57
  <WrapperView
55
- edges={getEdges(isTablet, isInlineModal) as readonly Edge[]}
58
+ edges={getEdges(isTablet, isInlineModal, vertical) as readonly Edge[]}
56
59
  style={playerDimensionsHack}
57
60
  >
58
61
  <View
@@ -37,6 +37,7 @@ jest.mock(
37
37
 
38
38
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/device", () => ({
39
39
  useIsTablet: mockUseIsDeviceTablet,
40
+ useOrientation: () => "portrait",
40
41
  }));
41
42
 
42
43
  jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
@@ -0,0 +1,58 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ import { useStyle } from "../playerWrapperStyle";
4
+ import { getScreenAspectRatio } from "../playerWrapperUtils";
5
+
6
+ let mockOrientation = "portrait";
7
+
8
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
9
+ useIsTablet: () => false,
10
+ useOrientation: () => mockOrientation,
11
+ }));
12
+
13
+ const getAspectRatio = (props: {
14
+ vertical: boolean;
15
+ inline?: boolean;
16
+ isModal?: boolean;
17
+ }) => {
18
+ const { result } = renderHook(() =>
19
+ useStyle({
20
+ style: {},
21
+ inline: false,
22
+ isModal: true,
23
+ isTabletPortrait: false,
24
+ configuration: {},
25
+ pip: false,
26
+ ...props,
27
+ })
28
+ );
29
+
30
+ return result.current.containerDimensions.aspectRatio;
31
+ };
32
+
33
+ describe("useStyle aspectRatio", () => {
34
+ const screenRatio = getScreenAspectRatio();
35
+
36
+ it("uses the screen ratio when not vertical", () => {
37
+ mockOrientation = "portrait";
38
+ expect(getAspectRatio({ vertical: false })).toBeCloseTo(screenRatio);
39
+ });
40
+
41
+ it("inverts the screen ratio for vertical fullscreen in portrait", () => {
42
+ mockOrientation = "portrait";
43
+ expect(getAspectRatio({ vertical: true })).toBeCloseTo(1 / screenRatio);
44
+ });
45
+
46
+ it("keeps the screen ratio for vertical fullscreen in landscape", () => {
47
+ mockOrientation = "landscape";
48
+ expect(getAspectRatio({ vertical: true })).toBeCloseTo(screenRatio);
49
+ });
50
+
51
+ it("inverts 16:9 for the vertical inline modal in landscape", () => {
52
+ mockOrientation = "landscape";
53
+
54
+ expect(getAspectRatio({ vertical: true, inline: true })).toBeCloseTo(
55
+ 9 / 16
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,16 @@
1
+ import { getEdges } from "../playerWrapperUtils";
2
+
3
+ describe("getEdges", () => {
4
+ it("keeps the top edge in fullscreen when not vertical", () => {
5
+ expect(getEdges(false, false)).toEqual(["top"]);
6
+ });
7
+
8
+ it("has no edges in vertical fullscreen", () => {
9
+ expect(getEdges(false, false, true)).toEqual([]);
10
+ expect(getEdges(true, false, true)).toEqual([]);
11
+ });
12
+
13
+ it("keeps the top edge for the vertical inline modal", () => {
14
+ expect(getEdges(false, true, true)).toEqual(["top"]);
15
+ });
16
+ });
@@ -1,7 +1,10 @@
1
1
  import * as React from "react";
2
2
  import { StyleSheet, ViewStyle } from "react-native";
3
3
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
4
- import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
4
+ import {
5
+ useIsTablet,
6
+ useOrientation,
7
+ } from "@applicaster/zapp-react-native-utils/reactHooks";
5
8
  import { playerDimensionsHack } from "./utils";
6
9
  import { getTabletWidth } from "@applicaster/zapp-react-native-utils/playerUtils";
7
10
  import {
@@ -22,8 +25,10 @@ export const useStyle = ({
22
25
  isTabletPortrait,
23
26
  configuration,
24
27
  pip,
28
+ vertical,
25
29
  }) => {
26
30
  const isTablet = useIsTablet();
31
+ const isLandscape = useOrientation() === "landscape";
27
32
 
28
33
  const isInlineModal = inline && isModal;
29
34
 
@@ -40,10 +45,23 @@ export const useStyle = ({
40
45
  );
41
46
 
42
47
  const aspectRatio = React.useMemo(
43
- () => calculateAspectRatio(isInlineModal, pip),
48
+ () => {
49
+ const value = calculateAspectRatio(isInlineModal, pip);
50
+
51
+ if (!vertical) {
52
+ return value;
53
+ }
54
+
55
+ // vertical fullscreen in landscape: the width is the long side, keep the screen ratio so the container matches the screen
56
+ if (isLandscape && !isInlineModal && !pip) {
57
+ return value;
58
+ }
59
+
60
+ return 1 / value;
61
+ },
44
62
  // ignoring insets - only initial needed
45
63
  // eslint-disable-next-line react-hooks/exhaustive-deps
46
- [isInlineModal, pip]
64
+ [isInlineModal, pip, vertical, isLandscape]
47
65
  );
48
66
 
49
67
  return React.useMemo(() => {
@@ -44,8 +44,14 @@ export const getScreenAspectRatio = () => {
44
44
 
45
45
  export const getEdges = (
46
46
  isTablet: boolean,
47
- isInlineModal: boolean
47
+ isInlineModal: boolean,
48
+ vertical: boolean = false
48
49
  ): readonly Edge[] => {
50
+ // vertical fullscreen: the player takes the full screen height, a top inset would push it below the screen
51
+ if (vertical && !isInlineModal) {
52
+ return [];
53
+ }
54
+
49
55
  if (isTablet) {
50
56
  return ["top"];
51
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "16.0.0-rc.117",
3
+ "version": "16.0.0-rc.119",
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",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-rc.117",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-rc.117",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-rc.117",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-rc.117",
31
+ "@applicaster/applicaster-types": "16.0.0-rc.119",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-rc.119",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-rc.119",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-rc.119",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "react-native-sortables": "1.7.1",