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