@applicaster/zapp-react-dom-app 15.1.19-rc.0 → 15.1.19

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.
@@ -1,9 +1,25 @@
1
1
  import { DISPLAY_STATES } from "@applicaster/zapp-react-native-ui-components/Contexts/DisplayStateContext";
2
2
  import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
3
- import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
3
+ import {
4
+ focusManager,
5
+ KEYS,
6
+ playerManager,
7
+ } from "@applicaster/zapp-react-native-utils/appUtils";
8
+ import { KeyInputHandler } from "@applicaster/zapp-react-native-utils/appUtils/keyInputHandler/KeyInputHandler";
9
+ import { TVSeekController } from "@applicaster/zapp-react-native-utils/reactHooks/player/TVSeekControlller/TVSeekController";
10
+ import { SKIP_TIME_BASE } from "@applicaster/zapp-react-native-utils/reactHooks/player/const";
4
11
 
5
12
  import { InteractionManagerClass } from "..";
6
13
 
14
+ // The web app runs with the web key map
15
+ jest.mock(
16
+ "@applicaster/zapp-react-native-utils/appUtils/keyCodes/keys/keys",
17
+ () =>
18
+ jest.requireActual(
19
+ "@applicaster/zapp-react-native-utils/appUtils/keyCodes/keys/keys.web"
20
+ )
21
+ );
22
+
7
23
  jest.mock("@applicaster/zapp-react-native-utils/alertUtils", () => ({
8
24
  showConfirmationDialog: jest.fn(),
9
25
  }));
@@ -98,4 +114,146 @@ describe("InteractionManager (web)", () => {
98
114
  expect(props.navigator.canGoBack).toHaveBeenCalled();
99
115
  });
100
116
  });
117
+
118
+ describe("Forward / Rewind keys", () => {
119
+ const POSITION = 100;
120
+
121
+ const createPlayer = (supportsNativeControls: boolean) => ({
122
+ supportsNativeControls: () => supportsNativeControls,
123
+ isPaused: () => false,
124
+ isLive: () => false,
125
+ getPosition: () => POSITION,
126
+ getSeekableDuration: () => 1000,
127
+ pause: jest.fn(),
128
+ play: jest.fn(),
129
+ seekTo: jest.fn(),
130
+ forward: jest.fn(),
131
+ rewind: jest.fn(),
132
+ });
133
+
134
+ let removeListener = () => {};
135
+
136
+ // Same wiring as usePlayerControllerSetup
137
+ const setup = ({ supportsNativeControls, ...props }: any) => {
138
+ const player = createPlayer(supportsNativeControls);
139
+
140
+ jest
141
+ .spyOn(playerManager, "getInstanceController")
142
+ .mockReturnValue(player as any);
143
+
144
+ removeListener = KeyInputHandler.getInstance().addListener(
145
+ new TVSeekController(player as any)
146
+ );
147
+
148
+ return { player, ...createManager(props) };
149
+ };
150
+
151
+ const press = (manager, key) => {
152
+ manager.onKeyDown(KEYS[key]);
153
+ manager.onKeyUp(KEYS[key]);
154
+ jest.runAllTimers();
155
+ };
156
+
157
+ beforeEach(() => jest.useFakeTimers());
158
+
159
+ afterEach(() => {
160
+ removeListener();
161
+ jest.useRealTimers();
162
+ jest.restoreAllMocks();
163
+ });
164
+
165
+ describe("with a player rendering its own controls", () => {
166
+ const expectUntouched = (player, props) => {
167
+ expect(player.seekTo).not.toHaveBeenCalled();
168
+ expect(player.forward).not.toHaveBeenCalled();
169
+ expect(player.rewind).not.toHaveBeenCalled();
170
+ expect(player.pause).not.toHaveBeenCalled();
171
+ expect(props.setDisplayState).not.toHaveBeenCalled();
172
+ };
173
+
174
+ it.each(["Forward", "Rewind"])("%s is left to the player", (key) => {
175
+ const { player, manager, props } = setup({
176
+ supportsNativeControls: true,
177
+ });
178
+
179
+ const event = { ...KEYS[key], stopPropagation: jest.fn() };
180
+
181
+ manager.onKeyDown(event);
182
+ manager.onKeyUp(event);
183
+ jest.runAllTimers();
184
+
185
+ expectUntouched(player, props);
186
+ // the key has to reach the player's own handler
187
+ expect(event.stopPropagation).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it("does not seek the player while the play-next overlay is visible", () => {
191
+ const { player, manager } = setup({ supportsNativeControls: true });
192
+
193
+ (manager.props.navigator as any).getPlayNextOverlay = () => ({});
194
+
195
+ press(manager, "Forward");
196
+
197
+ expect(player.seekTo).not.toHaveBeenCalled();
198
+ expect(player.forward).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it("keeps the keys while a dialog is visible", () => {
202
+ const { player, manager } = setup({ supportsNativeControls: true });
203
+
204
+ confirmationDialogStore.getState().showDialog();
205
+
206
+ press(manager, "Forward");
207
+
208
+ expect(player.seekTo).toHaveBeenCalledWith(POSITION + SKIP_TIME_BASE);
209
+ });
210
+
211
+ it("sends the key-up to the owner of the key-down when a dialog closes in between", () => {
212
+ const { player, manager } = setup({ supportsNativeControls: true });
213
+
214
+ confirmationDialogStore.getState().showDialog();
215
+ manager.onKeyDown(KEYS.Forward);
216
+ confirmationDialogStore.getState().hideDialog();
217
+ manager.onKeyUp(KEYS.Forward);
218
+ jest.runAllTimers();
219
+
220
+ // a single tap seek, not a long press left running
221
+ expect(player.seekTo).toHaveBeenCalledTimes(1);
222
+ expect(player.seekTo).toHaveBeenCalledWith(POSITION + SKIP_TIME_BASE);
223
+ });
224
+
225
+ it("leaves the key-up to the player when a dialog opens after a deferred key-down", () => {
226
+ const { player, manager, props } = setup({
227
+ supportsNativeControls: true,
228
+ });
229
+
230
+ manager.onKeyDown(KEYS.Forward);
231
+ confirmationDialogStore.getState().showDialog();
232
+ manager.onKeyUp(KEYS.Forward);
233
+ jest.runAllTimers();
234
+
235
+ expectUntouched(player, props);
236
+ });
237
+ });
238
+
239
+ describe("with QuickBrick's transport controls", () => {
240
+ it.each([
241
+ ["Forward", POSITION + SKIP_TIME_BASE],
242
+ ["Rewind", POSITION - SKIP_TIME_BASE],
243
+ ])("%s keeps the delayed seek", (key, expectedPosition) => {
244
+ const { player, manager, props } = setup({
245
+ supportsNativeControls: false,
246
+ displayState: DISPLAY_STATES.PLAYER,
247
+ });
248
+
249
+ press(manager, key);
250
+
251
+ expect(props.setDisplayState).toHaveBeenCalledWith(DISPLAY_STATES.HUD);
252
+ expect(player.seekTo).toHaveBeenCalledWith(expectedPosition);
253
+ expect(player.play).toHaveBeenCalled();
254
+ expect(player.forward).not.toHaveBeenCalled();
255
+ expect(player.rewind).not.toHaveBeenCalled();
256
+ });
257
+ });
258
+ });
101
259
  });
@@ -104,6 +104,8 @@ export class InteractionManagerClass extends React.Component<Props, State> {
104
104
  private sequenceTimeout: NodeJS.Timeout | null;
105
105
  private userSequence: number[] = [];
106
106
  private isDismissing = false;
107
+ // Whether each key's key-down was left to the player's own controls
108
+ private deferredKeyDowns = new Map<number, boolean>();
107
109
  private confirmDialog: ConfirmationDialogState =
108
110
  confirmationDialogStore.getState();
109
111
 
@@ -369,6 +371,18 @@ export class InteractionManagerClass extends React.Component<Props, State> {
369
371
  focusManager.pressOut();
370
372
  }
371
373
 
374
+ // The key-up goes to whoever got the key-down, even if a dialog or the
375
+ // play-next overlay opened or closed in between.
376
+ const deferred =
377
+ this.deferredKeyDowns.get(event.keyCode) ??
378
+ this.isDeferredToNativeControls(event);
379
+
380
+ this.deferredKeyDowns.delete(event.keyCode);
381
+
382
+ if (deferred) {
383
+ return;
384
+ }
385
+
372
386
  if (
373
387
  keyCode(event).matches(KEYS.Forward) ||
374
388
  (__DEV__ && keyCode(event).matches(KEYS.C))
@@ -435,14 +449,6 @@ export class InteractionManagerClass extends React.Component<Props, State> {
435
449
 
436
450
  const effectiveDisplayState = this.getEffectiveDisplayState();
437
451
 
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
452
  switch (effectiveDisplayState) {
447
453
  case DISPLAY_STATES.DEFAULT:
448
454
  if (action === "PLATFORM_BACK") {
@@ -568,12 +574,22 @@ export class InteractionManagerClass extends React.Component<Props, State> {
568
574
  return (
569
575
  this.isNativePlayerControlsEnabled() &&
570
576
  !this.isPlayNextOverlayVisible() &&
571
- (keyCode(event).matchesAny(...ARROW_KEYS) ||
572
- keyCode(event).matches(KEYS.Enter) ||
573
- (__DEV__ && keyCode(event).matches(KEYS.X)))
577
+ (keyCode(event).matchesAny(
578
+ ...ARROW_KEYS,
579
+ KEYS.Enter,
580
+ // The player seeks on its own with these keys
581
+ KEYS.Forward,
582
+ KEYS.Rewind
583
+ ) ||
584
+ (__DEV__ && keyCode(event).matchesAny(KEYS.X, KEYS.C, KEYS.Z)))
574
585
  );
575
586
  };
576
587
 
588
+ // An open dialog owns the remote, whatever the player renders.
589
+ isDeferredToNativeControls = (event: KeyboardEvent) =>
590
+ !confirmationDialogStore.getState().isDialogVisible &&
591
+ this.shouldDeferPlaybackNavigationToNativeControls(event);
592
+
577
593
  onKeyDown(event) {
578
594
  const { setDisplayState, navigator, xray } = this.props;
579
595
  const displayState = this.getEffectiveDisplayState();
@@ -612,18 +628,11 @@ export class InteractionManagerClass extends React.Component<Props, State> {
612
628
  // Only handle repeat events for arrow keys
613
629
  if (event.repeat && !keyCode(event).matchesAny(...ARROW_KEYS)) return;
614
630
 
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
- });
631
+ const deferred = this.isDeferredToNativeControls(event);
632
+ this.deferredKeyDowns.set(event.keyCode, deferred);
626
633
 
634
+ if (deferred) {
635
+ // The player's own key handling takes these keys.
627
636
  return;
628
637
  }
629
638
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-dom-app",
3
- "version": "15.1.19-rc.0",
3
+ "version": "15.1.19",
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.19-rc.0",
26
- "@applicaster/zapp-react-native-bridge": "15.1.19-rc.0",
27
- "@applicaster/zapp-react-native-redux": "15.1.19-rc.0",
28
- "@applicaster/zapp-react-native-ui-components": "15.1.19-rc.0",
29
- "@applicaster/zapp-react-native-utils": "15.1.19-rc.0",
25
+ "@applicaster/zapp-react-dom-ui-components": "15.1.19",
26
+ "@applicaster/zapp-react-native-bridge": "15.1.19",
27
+ "@applicaster/zapp-react-native-redux": "15.1.19",
28
+ "@applicaster/zapp-react-native-ui-components": "15.1.19",
29
+ "@applicaster/zapp-react-native-utils": "15.1.19",
30
30
  "abortcontroller-polyfill": "^1.7.5",
31
31
  "typeface-montserrat": "^0.0.54",
32
32
  "video.js": "7.14.3",