@applicaster/zapp-react-native-utils 15.0.0-alpha.1456157166 → 15.0.0-alpha.1615225514

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.
Files changed (101) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +3 -6
  2. package/actionsExecutor/feedDecorator.ts +6 -6
  3. package/adsUtils/index.ts +2 -2
  4. package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
  5. package/analyticsUtils/README.md +1 -1
  6. package/analyticsUtils/analyticsMapper.ts +10 -2
  7. package/appUtils/HooksManager/index.ts +10 -10
  8. package/appUtils/RiverFocusManager/{index.js → index.ts} +25 -18
  9. package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
  10. package/appUtils/accessibilityManager/const.ts +4 -0
  11. package/appUtils/accessibilityManager/hooks.ts +20 -13
  12. package/appUtils/accessibilityManager/index.ts +28 -1
  13. package/appUtils/accessibilityManager/utils.ts +59 -8
  14. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +4 -0
  15. package/appUtils/focusManager/index.ios.ts +59 -3
  16. package/appUtils/focusManagerAux/utils/index.ios.ts +122 -0
  17. package/appUtils/focusManagerAux/utils/index.ts +19 -1
  18. package/appUtils/focusManagerAux/utils/utils.ios.ts +231 -0
  19. package/appUtils/keyCodes/keys/keys.web.ts +1 -4
  20. package/appUtils/orientationHelper.ts +2 -4
  21. package/appUtils/platform/platformUtils.ts +117 -18
  22. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
  23. package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
  24. package/appUtils/playerManager/conts.ts +21 -0
  25. package/appUtils/playerManager/player.ts +4 -0
  26. package/appUtils/playerManager/playerNative.ts +29 -16
  27. package/appUtils/playerManager/usePlayerState.tsx +14 -2
  28. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  29. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  30. package/arrayUtils/index.ts +5 -0
  31. package/cellUtils/index.ts +32 -0
  32. package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
  33. package/focusManager/aux/index.ts +1 -1
  34. package/manifestUtils/defaultManifestConfigurations/player.js +96 -11
  35. package/manifestUtils/keys.js +21 -0
  36. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
  37. package/manifestUtils/tvAction/container/index.js +1 -1
  38. package/navigationUtils/index.ts +19 -16
  39. package/package.json +4 -4
  40. package/playerUtils/usePlayerTTS.ts +8 -3
  41. package/pluginUtils/index.ts +4 -0
  42. package/reactHooks/advertising/index.ts +2 -2
  43. package/reactHooks/app/__tests__/useAppState.test.ts +1 -1
  44. package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +1 -1
  45. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +1 -2
  46. package/reactHooks/cell-click/__tests__/index.test.js +1 -3
  47. package/reactHooks/configuration/__tests__/index.test.tsx +1 -1
  48. package/reactHooks/connection/__tests__/index.test.js +1 -1
  49. package/reactHooks/debugging/__tests__/index.test.js +4 -4
  50. package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
  51. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +32 -23
  52. package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +19 -19
  53. package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +4 -1
  54. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +42 -30
  55. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +1 -1
  56. package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
  57. package/reactHooks/feed/useBatchLoading.ts +7 -1
  58. package/reactHooks/feed/useEntryScreenId.ts +2 -2
  59. package/reactHooks/feed/useInflatedUrl.ts +43 -17
  60. package/reactHooks/feed/usePipesCacheReset.ts +3 -1
  61. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
  62. package/reactHooks/layout/__tests__/index.test.tsx +1 -1
  63. package/reactHooks/layout/__tests__/useLayoutVersion.test.tsx +1 -1
  64. package/reactHooks/layout/index.ts +1 -1
  65. package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
  66. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
  67. package/reactHooks/navigation/__tests__/index.test.tsx +1 -1
  68. package/reactHooks/navigation/index.ts +11 -6
  69. package/reactHooks/navigation/useRoute.ts +8 -6
  70. package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
  71. package/reactHooks/player/__tests__/useAutoSeek._test.tsx +1 -1
  72. package/reactHooks/player/__tests__/useTapSeek._test.ts +1 -1
  73. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +1 -1
  74. package/reactHooks/resolvers/__tests__/useComponentResolver.test.tsx +1 -1
  75. package/reactHooks/resolvers/useCellResolver.ts +6 -2
  76. package/reactHooks/resolvers/useComponentResolver.ts +8 -2
  77. package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +2 -2
  78. package/reactHooks/screen/__tests__/useScreenBackgroundColor.test.tsx +1 -1
  79. package/reactHooks/screen/__tests__/useScreenData.test.tsx +1 -1
  80. package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +12 -4
  81. package/reactHooks/screen/useTargetScreenData.ts +4 -2
  82. package/reactHooks/state/useRivers.ts +1 -1
  83. package/reactHooks/usePluginConfiguration.ts +2 -2
  84. package/reactHooks/utils/__tests__/index.test.js +1 -1
  85. package/screenState/__tests__/index.test.ts +1 -1
  86. package/searchUtils/const.ts +7 -0
  87. package/searchUtils/index.ts +3 -0
  88. package/stringUtils/index.ts +1 -1
  89. package/testUtils/index.tsx +30 -21
  90. package/utils/__tests__/mapAccum.test.ts +73 -0
  91. package/utils/__tests__/mergeRight.test.ts +48 -0
  92. package/utils/__tests__/selectors.test.ts +124 -0
  93. package/utils/index.ts +20 -0
  94. package/utils/mapAccum.ts +23 -0
  95. package/utils/mergeRight.ts +5 -0
  96. package/utils/path.ts +6 -3
  97. package/utils/pathOr.ts +5 -1
  98. package/utils/selectors.ts +46 -0
  99. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +128 -38
  100. package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +26 -10
  101. package/zappFrameworkUtils/HookCallback/useCallbackActions.ts +6 -9
@@ -23,19 +23,6 @@ export const useAccessibilityManager = (
23
23
  }
24
24
  }, [pluginConfiguration, accessibilityManager]);
25
25
 
26
- useEffect(() => {
27
- const subscription = accessibilityManager.getStateAsObservable().subscribe({
28
- next: () => {
29
- // TODO: handle accessibility states
30
- // screenReaderEnabled: false
31
- // reduceMotionEnabled: false
32
- // boldTextEnabled: false
33
- },
34
- });
35
-
36
- return () => subscription.unsubscribe();
37
- }, [accessibilityManager]);
38
-
39
26
  return accessibilityManager;
40
27
  };
41
28
 
@@ -72,3 +59,23 @@ export const useAnnouncementActive = (
72
59
 
73
60
  return isActive;
74
61
  };
62
+
63
+ export const useAccessibilityState = (
64
+ pluginConfiguration: Record<string, any> = {}
65
+ ) => {
66
+ const accessibilityManager = useAccessibilityManager(pluginConfiguration);
67
+
68
+ const [state, setState] = useState<AccessibilityState>(
69
+ accessibilityManager.getState()
70
+ );
71
+
72
+ useEffect(() => {
73
+ const subscription = accessibilityManager
74
+ .getStateAsObservable()
75
+ .subscribe(setState);
76
+
77
+ return () => subscription.unsubscribe();
78
+ }, [accessibilityManager]);
79
+
80
+ return state;
81
+ };
@@ -36,7 +36,20 @@ export class AccessibilityManager {
36
36
  false
37
37
  );
38
38
 
39
- private constructor() {}
39
+ private constructor() {
40
+ this.ttsManager
41
+ .getScreenReaderEnabledAsObservable()
42
+ .subscribe((enabled) => {
43
+ const state = this.state$.getValue();
44
+
45
+ if (state.screenReaderEnabled !== enabled) {
46
+ this.state$.next({
47
+ ...state,
48
+ screenReaderEnabled: enabled,
49
+ });
50
+ }
51
+ });
52
+ }
40
53
 
41
54
  public static getInstance(): AccessibilityManager {
42
55
  if (!AccessibilityManager._instance) {
@@ -92,8 +105,15 @@ export class AccessibilityManager {
92
105
  /**
93
106
  * Adds a heading to the queue, headings will be read before the next text
94
107
  * Each heading will be read once and removed from the queue
108
+ * Does nothing if screen reader is not enabled
95
109
  */
96
110
  public addHeading(heading: string) {
111
+ const state = this.state$.getValue();
112
+
113
+ if (!state.screenReaderEnabled) {
114
+ return;
115
+ }
116
+
97
117
  if (!this.pendingFocusId) {
98
118
  this.pendingFocusId = Date.now().toString();
99
119
  }
@@ -108,6 +128,7 @@ export class AccessibilityManager {
108
128
  *
109
129
  * Implements a delay mechanism to reduce noise during rapid navigation.
110
130
  * Only the most recent announcement will be read after the delay period.
131
+ * Does nothing if screen reader is not enabled
111
132
  */
112
133
  public readText({
113
134
  text,
@@ -116,6 +137,12 @@ export class AccessibilityManager {
116
137
  text: string;
117
138
  keyOfLocalizedText?: string;
118
139
  }) {
140
+ const state = this.state$.getValue();
141
+
142
+ if (!state.screenReaderEnabled) {
143
+ return;
144
+ }
145
+
119
146
  let textToRead = text;
120
147
 
121
148
  if (keyOfLocalizedText) {
@@ -1,24 +1,75 @@
1
+ import { createLogger } from "../../logger";
2
+
3
+ const { log_error } = createLogger({
4
+ category: "AccessibilityManager",
5
+ subsystem: "AppUtils",
6
+ });
7
+
1
8
  /**
2
9
  * Calculates the reading time for a given text based on word count
3
- * @param text - The text to calculate the reading time for
4
- * @param wordsPerMinute - Words per minute reading speed (default: 160)
10
+ * @param text - The text to calculate the reading time for (string or number)
11
+ * @param wordsPerMinute - Words per minute reading speed (default: 140)
5
12
  * @param minimumPause - Minimum pause time in milliseconds (default: 500)
6
13
  * @param announcementDelay - Additional delay for announcement in milliseconds (default: 700)
7
14
  * @returns The reading time in milliseconds
8
15
  */
9
16
  export function calculateReadingTime(
10
- text: string,
17
+ text: string | number,
11
18
  wordsPerMinute: number = 140,
12
19
  minimumPause: number = 500,
13
20
  announcementDelay: number = 700
14
21
  ): number {
15
- const words = text
16
- .trim()
22
+ if (typeof text !== "string" && typeof text !== "number") {
23
+ log_error(
24
+ `Invalid text input for reading time calculation got: ${
25
+ typeof text === "symbol" ? String(text) : text
26
+ }`
27
+ );
28
+
29
+ return 0;
30
+ }
31
+
32
+ const trimmed = typeof text === "number" ? String(text) : text.trim();
33
+
34
+ if (!trimmed) {
35
+ return 0;
36
+ }
37
+
38
+ const words = trimmed
17
39
  .split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
18
40
  .filter(Boolean).length;
19
41
 
20
- return (
21
- Math.max(minimumPause, (words / wordsPerMinute) * 60 * 1000) +
22
- announcementDelay
42
+ // Count spaces - multiple consecutive spaces add extra pause time
43
+ const spaceMatches: string[] = trimmed.match(/\s+/g) || [];
44
+
45
+ const totalSpaces = spaceMatches.reduce(
46
+ (sum: number, match: string) => sum + match.length,
47
+ 0
23
48
  );
49
+
50
+ const extraSpaces = Math.max(0, totalSpaces - (words - 1)); // words-1 is normal spacing
51
+
52
+ // Heuristic: punctuation increases TTS duration beyond word-based WPM.
53
+ // Commas typically introduce short pauses, sentence terminators longer ones.
54
+ const commaCount = (trimmed.match(/,/g) || []).length;
55
+ const semicolonCount = (trimmed.match(/;/g) || []).length;
56
+ const colonCount = (trimmed.match(/:/g) || []).length;
57
+ const dashCount = (trimmed.match(/\u2013|\u2014|-/g) || []).length; // – — -
58
+ const sentenceEndCount = (trimmed.match(/[.!?](?!\d)/g) || []).length;
59
+
60
+ const commaPauseMs = 220; // short prosody pause for ","
61
+ const midPauseMs = 260; // for ";", ":", dashes
62
+ const sentenceEndPauseMs = 420; // for ".", "!", "?"
63
+ const extraSpacePauseMs = 50; // per extra space beyond normal spacing
64
+
65
+ const punctuationPause =
66
+ commaCount * commaPauseMs +
67
+ (semicolonCount + colonCount + dashCount) * midPauseMs +
68
+ sentenceEndCount * sentenceEndPauseMs +
69
+ extraSpaces * extraSpacePauseMs;
70
+
71
+ const baseByWordsMs = (words / wordsPerMinute) * 60 * 1000;
72
+ const estimatedMs = Math.max(minimumPause, baseByWordsMs + punctuationPause);
73
+
74
+ return estimatedMs + announcementDelay;
24
75
  }
@@ -69,7 +69,11 @@ exports[`focusManagerIOS should be defined 1`] = `
69
69
  "getGroupRootById": [Function],
70
70
  "getPreferredFocusChild": [Function],
71
71
  "invokeHandler": [Function],
72
+ "isChildOf": [Function],
72
73
  "isFocusOn": [Function],
74
+ "isFocusOnContent": [Function],
75
+ "isFocusOnMenu": [Function],
76
+ "isFocusOnTabsScreenContent": [Function],
73
77
  "isGroupItemFocused": [Function],
74
78
  "moveFocus": [Function],
75
79
  "on": [Function],
@@ -1,12 +1,24 @@
1
1
  import { NativeModules } from "react-native";
2
2
  import * as R from "ramda";
3
3
 
4
- import { isCurrentFocusOn } from "../focusManagerAux/utils";
4
+ import {
5
+ isCurrentFocusOn,
6
+ isChildOf as isChildOfUtils,
7
+ isPartOfMenu,
8
+ isPartOfContent,
9
+ isPartOfTabsScreenContent,
10
+ } from "../focusManagerAux/utils/index.ios";
11
+
5
12
  import { Tree } from "./treeDataStructure/Tree";
6
13
  import { findFocusableNode } from "./treeDataStructure/Utils";
7
14
  import { subscriber } from "../../functionUtils";
8
15
  import { findChild } from "./utils";
9
16
 
17
+ import {
18
+ emitRegistered,
19
+ emitUnregistered,
20
+ } from "../focusManagerAux/utils/utils.ios";
21
+
10
22
  const { FocusableManagerModule } = NativeModules;
11
23
 
12
24
  /**
@@ -180,10 +192,20 @@ export const focusManager = (function () {
180
192
  function register({ id, component }) {
181
193
  const { isGroup = false } = component;
182
194
 
183
- return isGroup ? registerGroup(id, component) : registerItem(id, component);
195
+ if (isGroup) {
196
+ registerGroup(id, component);
197
+ } else {
198
+ registerItem(id, component);
199
+ }
200
+
201
+ const groupId = component?.props?.groupId;
202
+
203
+ emitRegistered({ id, groupId, isGroup });
184
204
  }
185
205
 
186
206
  function unregister(id, { group = false } = {}) {
207
+ emitUnregistered(id);
208
+
187
209
  group ? unregisterGroup(id) : unregisterItem(id);
188
210
  }
189
211
 
@@ -261,7 +283,9 @@ export const focusManager = (function () {
261
283
  function setFocus(
262
284
  id: string,
263
285
  direction?: FocusManager.IOS.Direction,
264
- options?: Partial<{ groupFocusedChanged: boolean }>,
286
+ options?: Partial<{
287
+ groupFocusedChanged: boolean;
288
+ }>,
265
289
  callback?: any
266
290
  ) {
267
291
  blur(direction);
@@ -400,6 +424,34 @@ export const focusManager = (function () {
400
424
  return id && isCurrentFocusOn(id, currentFocusNode);
401
425
  }
402
426
 
427
+ function isFocusOnMenu(): boolean {
428
+ const currentFocusable = getCurrentFocus();
429
+
430
+ return isPartOfMenu(focusableTree, currentFocusable?.props?.id);
431
+ }
432
+
433
+ function isFocusOnContent(): boolean {
434
+ const currentFocusable = getCurrentFocus();
435
+
436
+ return isPartOfContent(focusableTree, currentFocusable?.props?.id);
437
+ }
438
+
439
+ function isFocusOnTabsScreenContent(
440
+ screenPickerContentContainerId: string
441
+ ): boolean {
442
+ const currentFocusable = getCurrentFocus();
443
+
444
+ return isPartOfTabsScreenContent(
445
+ focusableTree,
446
+ screenPickerContentContainerId,
447
+ currentFocusable?.props?.id
448
+ );
449
+ }
450
+
451
+ function isChildOf(childId, parentId): boolean {
452
+ return isChildOfUtils(focusableTree, childId, parentId);
453
+ }
454
+
403
455
  return {
404
456
  on,
405
457
  invokeHandler,
@@ -422,5 +474,9 @@ export const focusManager = (function () {
422
474
  isGroupItemFocused,
423
475
  getPreferredFocusChild,
424
476
  isFocusOn,
477
+ isFocusOnMenu,
478
+ isFocusOnContent,
479
+ isFocusOnTabsScreenContent,
480
+ isChildOf,
425
481
  };
426
482
  })();
@@ -0,0 +1,122 @@
1
+ import { isNil, startsWith } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ import {
4
+ QUICK_BRICK_CONTENT,
5
+ QUICK_BRICK_NAVBAR,
6
+ } from "@applicaster/quick-brick-core/const";
7
+
8
+ const isNavBar = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
9
+ const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
10
+ const isRoot = (node) => node?.id === "root";
11
+
12
+ export const isPartOfTabsScreenContent = (
13
+ focusableTree,
14
+ screenPickerContentContainerId,
15
+ id
16
+ ) => {
17
+ const node = focusableTree.findInTree(id);
18
+
19
+ if (isNil(node)) {
20
+ return false;
21
+ }
22
+
23
+ if (isRoot(node)) {
24
+ return false;
25
+ }
26
+
27
+ if (isNavBar(node)) {
28
+ return false;
29
+ }
30
+
31
+ if (isContent(node)) {
32
+ return false;
33
+ }
34
+
35
+ if (node?.id === screenPickerContentContainerId) {
36
+ return true;
37
+ }
38
+
39
+ return isPartOfTabsScreenContent(
40
+ focusableTree,
41
+ screenPickerContentContainerId,
42
+ node.parent?.id
43
+ );
44
+ };
45
+
46
+ export const isPartOfMenu = (focusableTree, id): boolean => {
47
+ const node = focusableTree.findInTree(id);
48
+
49
+ if (isNil(node)) {
50
+ return false;
51
+ }
52
+
53
+ if (isRoot(node)) {
54
+ return false;
55
+ }
56
+
57
+ if (isNavBar(node)) {
58
+ return true;
59
+ }
60
+
61
+ if (isContent(node)) {
62
+ return false;
63
+ }
64
+
65
+ return isPartOfMenu(focusableTree, node.parent?.id);
66
+ };
67
+
68
+ export const isPartOfContent = (focusableTree, id) => {
69
+ const node = focusableTree.findInTree(id);
70
+
71
+ if (isNil(node)) {
72
+ return false;
73
+ }
74
+
75
+ if (isRoot(node)) {
76
+ return false;
77
+ }
78
+
79
+ if (isNavBar(node)) {
80
+ return false;
81
+ }
82
+
83
+ if (isContent(node)) {
84
+ return true;
85
+ }
86
+
87
+ return isPartOfContent(focusableTree, node.parent?.id);
88
+ };
89
+
90
+ export const isCurrentFocusOn = (id, node) => {
91
+ if (!node) {
92
+ return false;
93
+ }
94
+
95
+ if (isRoot(node)) {
96
+ return false;
97
+ }
98
+
99
+ if (node?.id === id) {
100
+ return true;
101
+ }
102
+
103
+ return isCurrentFocusOn(id, node.parent);
104
+ };
105
+
106
+ export const isChildOf = (focusableTree, childId, parentId) => {
107
+ if (isNil(childId) || isNil(parentId)) {
108
+ return false;
109
+ }
110
+
111
+ const childNode = focusableTree.findInTree(childId);
112
+
113
+ if (isNil(childNode)) {
114
+ return false;
115
+ }
116
+
117
+ if (childNode.parent?.id === parentId) {
118
+ return true;
119
+ }
120
+
121
+ return isChildOf(focusableTree, childNode.parent?.id, parentId);
122
+ };
@@ -102,7 +102,7 @@ export const getNavbarNode = (focusableTree) => {
102
102
 
103
103
  export const waitForContent = (focusableTree) => {
104
104
  const contentHasAnyChildren = (): boolean => {
105
- const countOfChildren = pathOr(
105
+ const countOfChildren = pathOr<number>(
106
106
  0,
107
107
  ["children", "length"],
108
108
  getContentNode(focusableTree)
@@ -190,3 +190,21 @@ export const isCurrentFocusOn = (id, node) => {
190
190
 
191
191
  return isCurrentFocusOn(id, node.parent);
192
192
  };
193
+
194
+ export const isChildOf = (focusableTree, childId, parentId) => {
195
+ if (isNil(childId) || isNil(parentId)) {
196
+ return false;
197
+ }
198
+
199
+ const childNode = focusableTree.findInTree(childId);
200
+
201
+ if (isNil(childNode)) {
202
+ return false;
203
+ }
204
+
205
+ if (childNode.parent?.id === parentId) {
206
+ return true;
207
+ }
208
+
209
+ return isChildOf(focusableTree, childNode.parent?.id, parentId);
210
+ };
@@ -0,0 +1,231 @@
1
+ import { ReplaySubject, Subject } from "rxjs";
2
+ import { filter, switchMap, take, withLatestFrom, map } from "rxjs/operators";
3
+
4
+ import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
5
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
6
+ import { isPartOfMenu, isPartOfContent } from "./index.ios";
7
+
8
+ type FocusableID = string;
9
+ type RegistrationEvent = {
10
+ id: FocusableID;
11
+ registered: boolean;
12
+ };
13
+
14
+ let focusableViewRegistrationSubject$ = new Subject<{
15
+ id: FocusableID;
16
+ groupId: FocusableID;
17
+ }>();
18
+
19
+ let focusableGroupRegistrationSubject$ = new ReplaySubject<{
20
+ id: FocusableID;
21
+ }>();
22
+
23
+ let focusableNativeViewRegistrationSubject$ = new Subject<{
24
+ id: FocusableID;
25
+ groupId: FocusableID;
26
+ }>();
27
+
28
+ let focusableNativeGroupRegistrationSubject$ = new ReplaySubject<{
29
+ id: FocusableID;
30
+ groupId: FocusableID;
31
+ }>();
32
+
33
+ const isFocusableButton = (id: Option<FocusableID>): boolean =>
34
+ id && id.includes?.(BUTTON_PREFIX);
35
+
36
+ const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
37
+
38
+ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
39
+ registeredSubject$.pipe(
40
+ filter(
41
+ (value) =>
42
+ value.registered && focusManager.isChildOf(value.id, focusableGroupId)
43
+ )
44
+ );
45
+
46
+ export const resetFocusableRegistration = () => {
47
+ // complete the old subject so subscribers are notified and resources are freed
48
+ if (!focusableViewRegistrationSubject$.closed) {
49
+ focusableViewRegistrationSubject$.complete();
50
+ }
51
+
52
+ if (!focusableGroupRegistrationSubject$.closed) {
53
+ focusableGroupRegistrationSubject$.complete();
54
+ }
55
+
56
+ if (!focusableNativeViewRegistrationSubject$.closed) {
57
+ focusableNativeViewRegistrationSubject$.complete();
58
+ }
59
+
60
+ if (!focusableNativeGroupRegistrationSubject$.closed) {
61
+ focusableNativeGroupRegistrationSubject$.complete();
62
+ }
63
+
64
+ focusableViewRegistrationSubject$ = new Subject<{
65
+ id: FocusableID;
66
+ groupId: FocusableID;
67
+ }>();
68
+
69
+ focusableGroupRegistrationSubject$ = new ReplaySubject<{
70
+ id: FocusableID;
71
+ }>();
72
+
73
+ focusableNativeViewRegistrationSubject$ = new Subject<{
74
+ id: FocusableID;
75
+ groupId: FocusableID;
76
+ }>();
77
+
78
+ focusableNativeGroupRegistrationSubject$ = new ReplaySubject<{
79
+ id: FocusableID;
80
+ groupId: FocusableID;
81
+ }>();
82
+ };
83
+
84
+ const focusableNativeViewRegistration = ({ focusableView, focusableGroup }) => {
85
+ return focusableNativeViewRegistrationSubject$.pipe(
86
+ filter(
87
+ (focusableNativeView) => focusableNativeView.id === focusableView.id
88
+ ),
89
+ take(1),
90
+ switchMap((focusableNativeView) =>
91
+ // start waiting registration of its parent FocusableNativeGroup
92
+ focusableNativeGroupRegistrationSubject$.pipe(
93
+ filter(
94
+ (focusableNativeGroup) =>
95
+ focusableNativeGroup.id === focusableNativeView.groupId
96
+ ),
97
+ take(1),
98
+ map((focusableNativeGroup) => ({
99
+ focusableNativeGroup,
100
+ focusableNativeView,
101
+ focusableView,
102
+ focusableGroup,
103
+ }))
104
+ )
105
+ )
106
+ );
107
+ };
108
+
109
+ export const firstFocusableViewRegistrationFactory = () =>
110
+ focusableViewRegistrationSubject$.pipe(
111
+ take(1), // we care about only first FocusableView registration
112
+ switchMap((focusableView) =>
113
+ // start waiting registration of its parent FocusableGroup
114
+ focusableGroupRegistrationSubject$.pipe(
115
+ filter((focusableGroup) => focusableGroup.id === focusableView.groupId),
116
+ take(1),
117
+ map((focusableGroup) => ({
118
+ focusableView,
119
+ focusableGroup,
120
+ }))
121
+ )
122
+ ),
123
+ // start waiting registration for FocusableNativeView and its parent FocusableNativeGroup
124
+ switchMap(({ focusableView, focusableGroup }) =>
125
+ focusableNativeViewRegistration({
126
+ focusableView,
127
+ focusableGroup,
128
+ })
129
+ )
130
+ );
131
+
132
+ // registration on RN level(into RN focusManager)
133
+ export const emitRegistered = ({
134
+ id,
135
+ groupId,
136
+ isGroup,
137
+ }: {
138
+ id: Option<FocusableID>;
139
+ groupId: Option<FocusableID>;
140
+ isGroup: boolean;
141
+ }): void => {
142
+ if (isFocusableButton(id)) {
143
+ registeredSubject$.next({ id, registered: true });
144
+ }
145
+
146
+ if (isGroup && id) {
147
+ focusableGroupRegistrationSubject$.next({ id });
148
+ }
149
+
150
+ if (!isGroup && id && groupId) {
151
+ focusableViewRegistrationSubject$.next({ id, groupId });
152
+ }
153
+ };
154
+
155
+ // unregistration on RN level(into RN focusManager)
156
+ export const emitUnregistered = (id: Option<FocusableID>): void => {
157
+ if (isFocusableButton(id)) {
158
+ registeredSubject$.next({ id, registered: false });
159
+ }
160
+ };
161
+
162
+ // registration focusableNativeView and focusableNativeGroup
163
+ export const emitNativeRegistered = ({
164
+ id,
165
+ groupId,
166
+ isGroup,
167
+ }: {
168
+ id: Option<FocusableID>;
169
+ groupId: Option<FocusableID>;
170
+ isGroup: boolean;
171
+ }): void => {
172
+ if (!isGroup && id && groupId) {
173
+ focusableNativeViewRegistrationSubject$.next({ id, groupId });
174
+ }
175
+
176
+ if (isGroup && id && groupId) {
177
+ focusableNativeGroupRegistrationSubject$.next({ id, groupId });
178
+ }
179
+ };
180
+
181
+ // /////
182
+
183
+ const focusedSubject$ = new Subject<FocusableID>();
184
+
185
+ const focused$ = focusedSubject$.asObservable();
186
+
187
+ export const emitFocused = (id: FocusableID): void => {
188
+ focusedSubject$.next(id);
189
+ };
190
+
191
+ export const topMenuItemFocused$ = focused$.pipe(
192
+ filter((id) => id && isPartOfMenu(focusManager.focusableTree, id))
193
+ );
194
+
195
+ export const contentFocused$ = focused$.pipe(
196
+ filter((id) => {
197
+ const isContent = isPartOfContent(focusManager.focusableTree, id);
198
+
199
+ return id && isContent;
200
+ })
201
+ );
202
+
203
+ const createFocusableRegistry = () => {
204
+ const subject$ = new ReplaySubject<FocusableID | undefined>(1);
205
+
206
+ return {
207
+ observable$: subject$.asObservable(),
208
+ register: (id: FocusableID) => {
209
+ // save focusable_id on registration
210
+ subject$.next(id);
211
+ },
212
+ unregister: () => {
213
+ // reset focusable_id on unregistration
214
+ subject$.next(undefined);
215
+ },
216
+ };
217
+ };
218
+
219
+ /// HOME_TOP_MENU_ITEM
220
+ export const HomeTopMenuItemRegistry = createFocusableRegistry();
221
+
222
+ export const homeTopMenuItemFocused$ = topMenuItemFocused$.pipe(
223
+ withLatestFrom(HomeTopMenuItemRegistry.observable$),
224
+ filter(([id, homeId]) => id === homeId)
225
+ );
226
+
227
+ /// SCREEN_PICKER
228
+ export const ScreenPickerContentContainerRegistry = createFocusableRegistry();
229
+
230
+ /// SEARCH_INPUT
231
+ export const SearchInputRegistry = createFocusableRegistry();
@@ -10,10 +10,7 @@ import { Platform } from "react-native";
10
10
  * platformKeys[Platform.OS] should only include keys
11
11
  * that are unique to that platform, i.e. Exit: { keyCode: 10182 }
12
12
  */
13
- export const KEYS = Object.assign(
14
- platformKeys["web"],
15
- platformKeys[Platform.OS]
16
- );
13
+ export const KEYS = Object.assign(platformKeys.web, platformKeys[Platform.OS]);
17
14
 
18
15
  export const ARROW_KEYS = [
19
16
  KEYS.ArrowUp,