@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.
- package/Components/AnimatedInOut/index.tsx +69 -26
- package/Components/BaseFocusable/index.ios.ts +12 -2
- package/Components/Cell/Cell.tsx +8 -3
- package/Components/Cell/FocusableWrapper.tsx +3 -0
- package/Components/Cell/TvOSCellComponent.tsx +26 -5
- package/Components/Focusable/Focusable.tsx +4 -2
- package/Components/Focusable/FocusableTvOS.tsx +18 -1
- package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
- package/Components/FocusableGroup/FocusableTvOS.tsx +55 -1
- package/Components/FocusableGroup/hooks/__tests__/useIsFocusEnabled.test.ts +113 -0
- package/Components/FocusableGroup/hooks/index.ts +1 -0
- package/Components/FocusableGroup/hooks/useIsFocusEnabled.ts +68 -0
- package/Components/GeneralContentScreen/utils/useCurationAPI.ts +19 -3
- package/Components/HandlePlayable/HandlePlayable.tsx +17 -65
- package/Components/HandlePlayable/const.ts +3 -0
- package/Components/HandlePlayable/utils.ts +74 -0
- package/Components/Layout/TV/LayoutBackground.tsx +5 -2
- package/Components/Layout/TV/ScreenContainer.tsx +2 -6
- package/Components/Layout/TV/index.tsx +3 -4
- package/Components/Layout/TV/index.web.tsx +3 -4
- package/Components/LinkHandler/LinkHandler.tsx +2 -2
- package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +16 -1
- package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +30 -2
- package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
- package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
- package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
- package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
- package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +10 -6
- package/Components/MasterCell/DefaultComponents/Text/index.tsx +8 -8
- package/Components/MasterCell/index.tsx +2 -0
- package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
- package/Components/MasterCell/utils/index.ts +61 -31
- package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
- package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
- package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
- package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
- package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
- package/Components/PlayerContainer/PlayerContainer.tsx +51 -55
- package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
- package/Components/PlayerImageBackground/index.tsx +3 -22
- package/Components/River/TV/River.tsx +31 -14
- package/Components/River/TV/index.tsx +8 -4
- package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
- package/Components/River/TV/utils/index.ts +4 -0
- package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
- package/Components/Screen/TV/index.web.tsx +4 -2
- package/Components/Screen/__tests__/Screen.test.tsx +65 -42
- package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
- package/Components/Screen/hooks.ts +2 -3
- package/Components/Screen/index.tsx +2 -3
- package/Components/Screen/navigationHandler.ts +49 -24
- package/Components/Screen/orientationHandler.ts +3 -3
- package/Components/ScreenResolver/index.tsx +13 -7
- package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
- package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
- package/Components/Tabs/TV/Tabs.tsx +20 -3
- package/Components/Transitioner/Scene.tsx +15 -2
- package/Components/Transitioner/index.js +3 -3
- package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
- package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
- package/Components/VideoModal/ModalAnimation/index.ts +2 -13
- package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
- package/Components/VideoModal/PlayerWrapper.tsx +14 -88
- package/Components/VideoModal/VideoModal.tsx +1 -5
- package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
- package/Components/VideoModal/hooks/useModalSize.ts +10 -5
- package/Components/VideoModal/playerWrapperStyle.ts +70 -0
- package/Components/VideoModal/playerWrapperUtils.ts +91 -0
- package/Components/VideoModal/utils.ts +19 -9
- package/Contexts/AboveTabsScreenContext/index.tsx +33 -0
- package/Decorators/Analytics/index.tsx +6 -5
- package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
- package/Decorators/ConfigurationWrapper/const.ts +1 -0
- package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
- package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
- package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
- package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
- package/Helpers/DataSourceHelper/index.ts +19 -0
- package/index.d.ts +7 -0
- package/package.json +6 -5
- package/Components/River/TV/withTVEventHandler.tsx +0 -27
- package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
- package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -417
- package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
- package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
- package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
- package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
- package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
- 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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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={
|
|
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
|
|
package/Components/Cell/Cell.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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>
|
|
@@ -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
|
-
|
|
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
|
+
};
|