@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?.platformToBackground;
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
- switch (displayState) {
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 (isDialogVisible) {
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 { displayState, setDisplayState, navigator, xray } = this.props;
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.1334507601",
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.1334507601",
26
- "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.1334507601",
27
- "@applicaster/zapp-react-native-redux": "16.0.0-alpha.1334507601",
28
- "@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.1334507601",
29
- "@applicaster/zapp-react-native-utils": "16.0.0-alpha.1334507601",
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",