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