@applicaster/zapp-react-dom-app 15.1.15 → 15.1.16-alpha.1335343245
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.
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { DISPLAY_STATES } from "@applicaster/zapp-react-native-ui-components/Contexts/DisplayStateContext";
|
|
2
|
+
import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
|
|
3
|
+
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
|
|
4
|
+
|
|
5
|
+
import { InteractionManagerClass } from "..";
|
|
6
|
+
|
|
7
|
+
jest.mock("@applicaster/zapp-react-native-utils/alertUtils", () => ({
|
|
8
|
+
showConfirmationDialog: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock(
|
|
12
|
+
"@applicaster/zapp-react-native-ui-components/Decorators/Navigator",
|
|
13
|
+
() => ({ withNavigator: (C) => C })
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
jest.mock("@applicaster/zapp-react-native-redux/utils/connectToStore", () => ({
|
|
17
|
+
connectToStore: () => (C) => C,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
|
|
21
|
+
const actual = jest.requireActual(
|
|
22
|
+
"@applicaster/zapp-react-native-utils/logger"
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
getXray: () => ({ ...actual.getXray(), withXray: (C) => C }),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const createBackEvent = () => ({
|
|
32
|
+
code: "Escape",
|
|
33
|
+
preventDefault: jest.fn(),
|
|
34
|
+
stopPropagation: jest.fn(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createManager = (props = {}) => {
|
|
38
|
+
const navigator = {
|
|
39
|
+
canGoBack: jest.fn(() => false),
|
|
40
|
+
goBack: jest.fn(),
|
|
41
|
+
replace: jest.fn(),
|
|
42
|
+
currentRoute: "/river/home",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const defaultProps = {
|
|
46
|
+
displayState: DISPLAY_STATES.DEFAULT,
|
|
47
|
+
setDisplayState: jest.fn(),
|
|
48
|
+
navigator,
|
|
49
|
+
rivers: {},
|
|
50
|
+
backToTopAction: jest.fn(() => "GO_BACK"),
|
|
51
|
+
emitFocusOnSelectedTopMenuItem: jest.fn(),
|
|
52
|
+
emitFocusOnSelectedTab: jest.fn(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mergedProps = { ...defaultProps, ...props };
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
manager: new InteractionManagerClass(mergedProps as any),
|
|
59
|
+
props: mergedProps,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("InteractionManager (web)", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
jest.clearAllMocks();
|
|
66
|
+
(focusManager as any).setInitialFocus = jest.fn();
|
|
67
|
+
confirmationDialogStore.getState().hideDialog();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("onBackPress with exit dialog visible", () => {
|
|
71
|
+
it.each([DISPLAY_STATES.DEFAULT, DISPLAY_STATES.PLAYER])(
|
|
72
|
+
"dismisses the dialog instead of navigating (displayState: %s)",
|
|
73
|
+
(displayState) => {
|
|
74
|
+
const { manager, props } = createManager({ displayState });
|
|
75
|
+
confirmationDialogStore.getState().showDialog();
|
|
76
|
+
|
|
77
|
+
const event = createBackEvent();
|
|
78
|
+
manager.onBackPress(event);
|
|
79
|
+
|
|
80
|
+
expect(confirmationDialogStore.getState().isDialogVisible).toBe(false);
|
|
81
|
+
expect(props.backToTopAction).not.toHaveBeenCalled();
|
|
82
|
+
expect(props.navigator.goBack).not.toHaveBeenCalled();
|
|
83
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
84
|
+
|
|
85
|
+
expect(focusManager.setInitialFocus).toHaveBeenCalledWith(undefined, {
|
|
86
|
+
source: "cancel",
|
|
87
|
+
preserveScroll: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it("resolves the back action normally when the dialog is hidden", () => {
|
|
93
|
+
const { manager, props } = createManager();
|
|
94
|
+
|
|
95
|
+
manager.onBackPress(createBackEvent());
|
|
96
|
+
|
|
97
|
+
expect(props.backToTopAction).toHaveBeenCalled();
|
|
98
|
+
expect(props.navigator.canGoBack).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -92,7 +92,7 @@ type KeyCode = { code: string; keyCode: number | string };
|
|
|
92
92
|
const VIDEO_PLAYER_CONTROLS_NAVIGATION_MESSAGE =
|
|
93
93
|
"Video player controls - use left/right to navigate";
|
|
94
94
|
|
|
95
|
-
class InteractionManagerClass extends React.Component<Props, State> {
|
|
95
|
+
export class InteractionManagerClass extends React.Component<Props, State> {
|
|
96
96
|
onKeyDownListener: (keyCode: KeyCode) => void;
|
|
97
97
|
onMouseDownListener: (event: MouseEvent) => void;
|
|
98
98
|
onMouseMoveListener: (event: MouseEvent) => void;
|
|
@@ -207,7 +207,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
componentWillUnmount() {
|
|
210
|
-
window.removeEventListener("keydown", this.onKeyDownListener);
|
|
210
|
+
window.removeEventListener("keydown", this.onKeyDownListener, true);
|
|
211
211
|
window.removeEventListener("keyup", this.onKeyUp);
|
|
212
212
|
document.removeEventListener("wheel", this.onScrollListener);
|
|
213
213
|
|
|
@@ -421,9 +421,29 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
// Focus is inside the dialog, so backToTopAction() can't resolve a
|
|
425
|
+
// meaningful action — Back must dismiss the dialog in every display state.
|
|
426
|
+
if (isDialogVisible) {
|
|
427
|
+
this.onConfirmDialogClose();
|
|
428
|
+
event.preventDefault();
|
|
429
|
+
event.stopPropagation();
|
|
430
|
+
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
424
434
|
const action = backToTopAction();
|
|
425
435
|
|
|
426
|
-
|
|
436
|
+
const effectiveDisplayState = this.getEffectiveDisplayState();
|
|
437
|
+
|
|
438
|
+
// TEMP(native-controls): remove after device verification
|
|
439
|
+
if (effectiveDisplayState !== displayState) {
|
|
440
|
+
log_debug("[NativeControls] Back handled as PLAYER", {
|
|
441
|
+
displayState,
|
|
442
|
+
action,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
switch (effectiveDisplayState) {
|
|
427
447
|
case DISPLAY_STATES.DEFAULT:
|
|
428
448
|
if (action === "PLATFORM_BACK") {
|
|
429
449
|
log_info(action);
|
|
@@ -472,9 +492,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
472
492
|
|
|
473
493
|
break;
|
|
474
494
|
case DISPLAY_STATES.PLAYER:
|
|
475
|
-
if (
|
|
476
|
-
this.onConfirmDialogClose();
|
|
477
|
-
} else if (navigator.canGoBack()) {
|
|
495
|
+
if (navigator.canGoBack()) {
|
|
478
496
|
navigator.goBack();
|
|
479
497
|
} else if (this.isHomeScreen()) {
|
|
480
498
|
this.platformBack();
|
|
@@ -523,8 +541,42 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
523
541
|
this.onBackPress(event);
|
|
524
542
|
}
|
|
525
543
|
|
|
544
|
+
// True only while the active (primary) player renders its own controls.
|
|
545
|
+
isNativePlayerControlsEnabled = () =>
|
|
546
|
+
!!playerManager.getInstanceController?.()?.supportsNativeControls?.();
|
|
547
|
+
|
|
548
|
+
// PLAYER/HUD are set by QuickBrick's TV transport controls, which are not
|
|
549
|
+
// mounted when the player renders native controls — the state would stay
|
|
550
|
+
// DEFAULT and keys would reach the screen behind the player (Enter re-opens it).
|
|
551
|
+
// The play-next overlay is the exception: it is QuickBrick UI, so the real
|
|
552
|
+
// state applies and Enter presses the focused overlay button.
|
|
553
|
+
getEffectiveDisplayState = () =>
|
|
554
|
+
this.isNativePlayerControlsEnabled() && !this.isPlayNextOverlayVisible()
|
|
555
|
+
? DISPLAY_STATES.PLAYER
|
|
556
|
+
: this.props.displayState;
|
|
557
|
+
|
|
558
|
+
// True when the player closed something of its own instead.
|
|
559
|
+
playerConsumesBack = () =>
|
|
560
|
+
!!playerManager.getInstanceController?.()?.handlesBackKey?.();
|
|
561
|
+
|
|
562
|
+
// While the play-next overlay is up it owns the remote: its buttons are
|
|
563
|
+
// QuickBrick focusables, so the keys must not go to the player's own controls.
|
|
564
|
+
isPlayNextOverlayVisible = () =>
|
|
565
|
+
!!this.props.navigator?.getPlayNextOverlay?.();
|
|
566
|
+
|
|
567
|
+
shouldDeferPlaybackNavigationToNativeControls = (event: KeyboardEvent) => {
|
|
568
|
+
return (
|
|
569
|
+
this.isNativePlayerControlsEnabled() &&
|
|
570
|
+
!this.isPlayNextOverlayVisible() &&
|
|
571
|
+
(keyCode(event).matchesAny(...ARROW_KEYS) ||
|
|
572
|
+
keyCode(event).matches(KEYS.Enter) ||
|
|
573
|
+
(__DEV__ && keyCode(event).matches(KEYS.X)))
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
526
577
|
onKeyDown(event) {
|
|
527
|
-
const {
|
|
578
|
+
const { setDisplayState, navigator, xray } = this.props;
|
|
579
|
+
const displayState = this.getEffectiveDisplayState();
|
|
528
580
|
const { isDialogVisible } = confirmationDialogStore.getState();
|
|
529
581
|
const { isKeyboardVisible } = this.state;
|
|
530
582
|
|
|
@@ -542,6 +594,12 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
542
594
|
}
|
|
543
595
|
|
|
544
596
|
if (keyCode(event).matchesAny(KEYS.Back, KEYS.Escape, KEYS.EscapeDevice)) {
|
|
597
|
+
// A player with native controls can have something of its own open that
|
|
598
|
+
// Back should close first (e.g. an ad click-through overlay).
|
|
599
|
+
if (this.isNativePlayerControlsEnabled() && this.playerConsumesBack()) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
545
603
|
this.onBackPress(event);
|
|
546
604
|
|
|
547
605
|
return;
|
|
@@ -554,6 +612,21 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
554
612
|
// Only handle repeat events for arrow keys
|
|
555
613
|
if (event.repeat && !keyCode(event).matchesAny(...ARROW_KEYS)) return;
|
|
556
614
|
|
|
615
|
+
if (
|
|
616
|
+
!isDialogVisible &&
|
|
617
|
+
this.shouldDeferPlaybackNavigationToNativeControls(event)
|
|
618
|
+
) {
|
|
619
|
+
// The player's own spatial navigation handles these keys.
|
|
620
|
+
// TEMP(native-controls): remove after device verification
|
|
621
|
+
log_debug("[NativeControls] key deferred to player", {
|
|
622
|
+
key: event.key,
|
|
623
|
+
displayState,
|
|
624
|
+
activeElement: document.activeElement?.className,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
557
630
|
/**
|
|
558
631
|
When the player is displayed, we listen to Native Play / Pause / Stop
|
|
559
632
|
buttons. Any other key press will display the transport controls
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-dom-app",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.16-alpha.1335343245",
|
|
4
4
|
"description": "Zapp App Component for Applicaster's Quick Brick React Native App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"homepage": "https://github.com/applicaster/zapp-react-dom-app#readme",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@applicaster/zapp-react-dom-ui-components": "15.1.
|
|
26
|
-
"@applicaster/zapp-react-native-bridge": "15.1.
|
|
27
|
-
"@applicaster/zapp-react-native-redux": "15.1.
|
|
28
|
-
"@applicaster/zapp-react-native-ui-components": "15.1.
|
|
29
|
-
"@applicaster/zapp-react-native-utils": "15.1.
|
|
25
|
+
"@applicaster/zapp-react-dom-ui-components": "15.1.16-alpha.1335343245",
|
|
26
|
+
"@applicaster/zapp-react-native-bridge": "15.1.16-alpha.1335343245",
|
|
27
|
+
"@applicaster/zapp-react-native-redux": "15.1.16-alpha.1335343245",
|
|
28
|
+
"@applicaster/zapp-react-native-ui-components": "15.1.16-alpha.1335343245",
|
|
29
|
+
"@applicaster/zapp-react-native-utils": "15.1.16-alpha.1335343245",
|
|
30
30
|
"abortcontroller-polyfill": "^1.7.5",
|
|
31
31
|
"typeface-montserrat": "^0.0.54",
|
|
32
32
|
"video.js": "7.14.3",
|