@applicaster/zapp-react-dom-app 16.0.0-rc.111 → 16.0.0-rc.113

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