@applicaster/zapp-react-native-utils 14.0.0-rc.50 → 14.0.0-rc.51

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.
@@ -6,6 +6,7 @@ exports[`focusManager should be defined 1`] = `
6
6
  "disableFocus": [Function],
7
7
  "enableFocus": [Function],
8
8
  "findPreferredFocusChild": [Function],
9
+ "focusTopNavigation": [Function],
9
10
  "focusableTree": Tree {
10
11
  "loadingCounter": 0,
11
12
  "root": {
@@ -25,6 +26,8 @@ exports[`focusManager should be defined 1`] = `
25
26
  "isCurrentFocusOnTheTopScreen": [Function],
26
27
  "isFocusDisabled": [Function],
27
28
  "isFocusOn": [Function],
29
+ "isFocusOnContent": [Function],
30
+ "isFocusOnMenu": [Function],
28
31
  "isGroupItemFocused": [Function],
29
32
  "longPress": [Function],
30
33
  "moveFocus": [Function],
@@ -33,7 +33,7 @@ describe("focusManager", () => {
33
33
 
34
34
  expect(success).toBe(true);
35
35
  expect(mockSetFocus).toBeCalledTimes(1);
36
- expect(mockSetFocus).toBeCalledWith(null);
36
+ expect(mockSetFocus).toBeCalledWith(null, undefined);
37
37
  });
38
38
 
39
39
  describe("register", () => {});
@@ -14,7 +14,14 @@ import { subscriber } from "../../functionUtils";
14
14
  import { coreLogger } from "../../logger";
15
15
  import { ACTION } from "./utils/enums";
16
16
 
17
- import { isCurrentFocusOn } from "../focusManagerAux/utils";
17
+ import {
18
+ findSelectedTabId,
19
+ findSelectedMenuId,
20
+ isTabsScreenContentFocused,
21
+ isCurrentFocusOnContent,
22
+ isCurrentFocusOnMenu,
23
+ isCurrentFocusOn,
24
+ } from "../focusManagerAux/utils";
18
25
 
19
26
  const logger = coreLogger.addSubsystem("focusManager");
20
27
 
@@ -102,7 +109,7 @@ export const focusManager = (function () {
102
109
  * @private
103
110
  * @param {Object} direction of the navigation which led to this action
104
111
  */
105
- function focus(direction) {
112
+ function focus(direction, context?: FocusManager.FocusContext) {
106
113
  const currentFocusable = getCurrentFocus();
107
114
 
108
115
  if (
@@ -110,7 +117,7 @@ export const focusManager = (function () {
110
117
  !currentFocusable.isGroup &&
111
118
  currentFocusable.isMounted()
112
119
  ) {
113
- currentFocusable.setFocus(direction);
120
+ currentFocusable.setFocus(direction, context);
114
121
  }
115
122
  }
116
123
 
@@ -207,7 +214,7 @@ export const focusManager = (function () {
207
214
  * @param {Array<string>} ids - An array of node IDs to update.
208
215
  * @param {boolean} setFocus - A flag indicating whether to set focus (true) or blur (false) on the nodes.
209
216
  */
210
- const updateNodeFocus = (ids, action) => {
217
+ const updateNodeFocus = (ids, action, context: FocusManager.FocusContext) => {
211
218
  if (!ids || ids.length === 0) {
212
219
  return; // Nothing to do
213
220
  }
@@ -224,11 +231,13 @@ export const focusManager = (function () {
224
231
 
225
232
  // Function to apply the action (focus or blur)
226
233
  const applyAction = (node) => {
234
+ const direction = undefined;
235
+
227
236
  if (node && node.component) {
228
237
  if (action === "focus") {
229
- node.component.setFocus();
238
+ node.component.setFocus(direction, context);
230
239
  } else if (action === "blur") {
231
- node.component.setBlur();
240
+ node.component.setBlur(direction, context);
232
241
  }
233
242
  }
234
243
  };
@@ -255,7 +264,11 @@ export const focusManager = (function () {
255
264
  * @param {Object} direction of the navigation, which led to this focus change
256
265
  * to another group or not. defaults to false
257
266
  */
258
- function setFocus(id: string, direction?: FocusManager.Web.Direction) {
267
+ function setFocus(
268
+ id: string,
269
+ direction?: FocusManager.Web.Direction,
270
+ context?: FocusManager.FocusContext
271
+ ) {
259
272
  if (focusDisabled) return false;
260
273
 
261
274
  // due to optimisiation it's recommanded to set currentFocusNode before setFocus
@@ -268,21 +281,65 @@ export const focusManager = (function () {
268
281
  );
269
282
 
270
283
  // Set focus on current node parents and blur on previous node parents
271
- updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS);
272
- updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR);
284
+ updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS, context);
285
+ updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR, context);
273
286
 
274
287
  currentFocusNode = focusableTree.findInTree(id);
275
288
  }
276
289
 
277
290
  setLastFocusOnParentNode(currentFocusNode);
278
291
 
279
- focus(direction);
292
+ focus(direction, context);
293
+ }
294
+
295
+ function isFocusOnContent() {
296
+ return isCurrentFocusOnContent(currentFocusNode);
297
+ }
298
+
299
+ function isFocusOnMenu() {
300
+ return isCurrentFocusOnMenu(currentFocusNode);
301
+ }
302
+
303
+ // Move focus to appropriate top navigation tab with context
304
+ function focusTopNavigation(isTabsScreen: boolean, item: ZappEntry) {
305
+ const landFocusTo = (id) => {
306
+ if (id) {
307
+ // set focus on selected menu item
308
+ const direction = undefined;
309
+
310
+ const context: FocusManager.FocusContext = {
311
+ source: "back",
312
+ preserveScroll: true,
313
+ };
314
+
315
+ logger.log({ message: "landFocusTo", data: { id } });
316
+
317
+ blur(direction);
318
+ setFocus(id, direction, context);
319
+ }
320
+ };
321
+
322
+ if (isTabsScreen && isTabsScreenContentFocused(currentFocusNode)) {
323
+ const selectedTabId = findSelectedTabId(item);
324
+
325
+ // Set focus with back button context to tabs-menu
326
+ landFocusTo(selectedTabId);
327
+
328
+ return;
329
+ }
330
+
331
+ const selectedMenuItemId = findSelectedMenuId(focusableTree);
332
+ // Set focus with back button context to top-menu
333
+ landFocusTo(selectedMenuItemId);
280
334
  }
281
335
 
282
336
  /**
283
337
  * sets the initial focus when the screen loads, or when focus is lost
284
338
  */
285
- function setInitialFocus(lastAddedParentNode?: any) {
339
+ function setInitialFocus(
340
+ lastAddedParentNode?: any,
341
+ context?: FocusManager.FocusContext
342
+ ) {
286
343
  const preferredFocus = findPriorityItem(
287
344
  lastAddedParentNode?.children || focusableTree.root.children
288
345
  );
@@ -328,7 +385,7 @@ export const focusManager = (function () {
328
385
  },
329
386
  });
330
387
 
331
- focusableItem && setFocus(focusCandidate.id, null);
388
+ focusableItem && setFocus(focusCandidate.id, null, context);
332
389
 
333
390
  return { success: true };
334
391
  }
@@ -586,6 +643,9 @@ export const focusManager = (function () {
586
643
  recoverFocus,
587
644
  isCurrentFocusOnTheTopScreen,
588
645
  findPreferredFocusChild,
646
+ focusTopNavigation,
647
+ isFocusOnContent,
648
+ isFocusOnMenu,
589
649
  isFocusOn,
590
650
  };
591
651
  })();
@@ -1,16 +1,31 @@
1
- import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
2
- import { find, last, pathOr, startsWith } from "ramda";
1
+ import {
2
+ isNil,
3
+ last,
4
+ startsWith,
5
+ find,
6
+ pathOr,
7
+ } from "@applicaster/zapp-react-native-utils/utils";
8
+
3
9
  import {
4
10
  QUICK_BRICK_CONTENT,
5
11
  QUICK_BRICK_NAVBAR,
6
12
  } from "@applicaster/quick-brick-core/const";
7
13
 
14
+ import {
15
+ getFocusableId,
16
+ SCREEN_PICKER_CONTAINER,
17
+ } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
18
+
8
19
  // run check each 300 ms
9
20
  // run this check too often could lead to performance penalty on low-end devices
10
21
  const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
11
22
 
23
+ const isTopMenu = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
24
+ const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
12
25
  const isRoot = (node) => node?.id === "root";
13
26
 
27
+ const isScrenPicker = (node) => startsWith(SCREEN_PICKER_CONTAINER, node?.id);
28
+
14
29
  type Props = {
15
30
  maxTimeout: number;
16
31
  conditionFn: () => boolean;
@@ -51,7 +66,7 @@ export const waitForActiveScreen = (currentRoute: string, focusableTree) => {
51
66
 
52
67
  const route = find((route) => route.id === currentRoute, routes);
53
68
 
54
- return isNotNil(route);
69
+ return !isNil(route);
55
70
  };
56
71
 
57
72
  return waitUntil({
@@ -102,6 +117,76 @@ export const waitForContent = (focusableTree) => {
102
117
  });
103
118
  };
104
119
 
120
+ export const findSelectedTabId = (item: ZappEntry): string => {
121
+ const selectedTabId = getFocusableId(item.id);
122
+
123
+ return selectedTabId;
124
+ };
125
+
126
+ export const findSelectedMenuId = (focusableTree) => {
127
+ // Set focus with back button context
128
+ const navbar = getNavbarNode(focusableTree);
129
+
130
+ const selectedMenuItemId = find(
131
+ (child) => child.component.props.selected,
132
+ navbar.children
133
+ )?.id;
134
+
135
+ return selectedMenuItemId;
136
+ };
137
+
138
+ export const isTabsScreenContentFocused = (node) => {
139
+ if (isRoot(node)) {
140
+ return false;
141
+ }
142
+
143
+ if (isTopMenu(node)) {
144
+ return false;
145
+ }
146
+
147
+ if (isContent(node)) {
148
+ return false;
149
+ }
150
+
151
+ if (isScrenPicker(node)) {
152
+ return true;
153
+ }
154
+
155
+ return isTabsScreenContentFocused(node.parent);
156
+ };
157
+
158
+ export const isCurrentFocusOnMenu = (node) => {
159
+ if (isRoot(node)) {
160
+ return false;
161
+ }
162
+
163
+ if (isTopMenu(node)) {
164
+ return true;
165
+ }
166
+
167
+ if (isContent(node)) {
168
+ return false;
169
+ }
170
+
171
+ return isCurrentFocusOnMenu(node.parent);
172
+ };
173
+
174
+ export const isCurrentFocusOnContent = (node) => {
175
+ if (isRoot(node)) {
176
+ return false;
177
+ }
178
+
179
+ if (isTopMenu(node)) {
180
+ return false;
181
+ }
182
+
183
+ if (isContent(node)) {
184
+ return true;
185
+ }
186
+
187
+ return isCurrentFocusOnContent(node.parent);
188
+ };
189
+
105
190
  export const isCurrentFocusOn = (id, node) => {
106
191
  if (!node) {
107
192
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "14.0.0-rc.50",
3
+ "version": "14.0.0-rc.51",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "14.0.0-rc.50",
30
+ "@applicaster/applicaster-types": "14.0.0-rc.51",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -1,5 +1,5 @@
1
1
  export { useRivers } from "./useRivers";
2
2
 
3
- export { useHomeRiver } from "./useHomeRiver";
3
+ export { useHomeRiver, getHomeRiver } from "./useHomeRiver";
4
4
 
5
5
  export { ZStoreProvider, useZStore } from "./ZStoreProvider";
@@ -1,8 +1,10 @@
1
- import * as R from "ramda";
2
1
  import { useRivers } from "./useRivers";
3
2
 
3
+ export const getHomeRiver = (rivers: Record<string, ZappRiver>) =>
4
+ Object.values(rivers).find((river: ZappRiver) => river.home);
5
+
4
6
  export const useHomeRiver = () => {
5
7
  const rivers = useRivers();
6
8
 
7
- return R.compose(R.find(R.propEq("home", true)), R.values)(rivers);
9
+ return getHomeRiver(rivers);
8
10
  };
@@ -0,0 +1,7 @@
1
+ export const getFocusableId = (id) => `PickerItem.${id}`;
2
+
3
+ export const getPickerSelectorId = (id) => `PickerSelector.${id}`;
4
+
5
+ export const SCREEN_PICKER_CONTAINER = "ScreenPickerContainer";
6
+
7
+ export const getScreenPickerId = (id) => `${SCREEN_PICKER_CONTAINER}.${id}`;
@@ -0,0 +1,36 @@
1
+ import { find } from "../find";
2
+
3
+ test("example 1", () => {
4
+ const predicate = <T>(_: T, index: number): boolean => index === 0;
5
+ const xs = ["1", "2", "2", "3", "4"];
6
+
7
+ expect(find(predicate, xs)).toBe("1");
8
+ });
9
+
10
+ test("example 2", () => {
11
+ const predicate = <T>(_: T, index: number): boolean => index === 0;
12
+ const xs: string[] = [];
13
+
14
+ expect(find(predicate, xs)).toBe(undefined);
15
+ });
16
+
17
+ test("example 3", () => {
18
+ const predicate = () => false;
19
+ const xs = ["1", "2", "2", "3"];
20
+
21
+ expect(find(predicate, xs)).toBe(undefined);
22
+ });
23
+
24
+ test("example 4", () => {
25
+ const predicate = <T>(_: T, index: number): boolean => index === 1;
26
+ const xs = ["1", "2", "2", "3"];
27
+
28
+ expect(find(predicate, xs)).toBe("2");
29
+ });
30
+
31
+ test("example 5", () => {
32
+ const predicate = <T>(_: T, index: number): boolean => index === 2;
33
+ const xs = ["1", "2.1", "2", "3", "2", "4"];
34
+
35
+ expect(find(predicate, xs)).toBe("2");
36
+ });
@@ -0,0 +1,37 @@
1
+ import { pathOr } from "../pathOr";
2
+
3
+ test("example 1", () => {
4
+ const defaultValue = "defaultValue";
5
+ const path = ["a", "b", "c"];
6
+ const xs = { a: { b: { c: 1 } } };
7
+
8
+ const output = 1;
9
+
10
+ expect(pathOr(defaultValue, path, xs)).toEqual(output);
11
+ });
12
+
13
+ test("example 2", () => {
14
+ const defaultValue = "defaultValue";
15
+ const path = ["a", "b"];
16
+ const xs = { a: { b: { c: 1 } } };
17
+
18
+ const output = { c: 1 };
19
+
20
+ expect(pathOr(defaultValue, path, xs)).toEqual(output);
21
+ });
22
+
23
+ test("example 3", () => {
24
+ const defaultValue = "defaultValue";
25
+ const path = ["a", "b", "x"];
26
+ const xs = { a: { b: { c: 1 } } };
27
+
28
+ expect(pathOr(defaultValue, path, xs)).toBe(defaultValue);
29
+ });
30
+
31
+ test("example 4", () => {
32
+ const defaultValue = "defaultValue";
33
+ const path = ["a", "b", "c"];
34
+ const xs = undefined;
35
+
36
+ expect(pathOr(defaultValue, path, xs)).toBe(defaultValue);
37
+ });
@@ -0,0 +1,30 @@
1
+ import { startsWith } from "../startsWith";
2
+
3
+ describe("startsWith", () => {
4
+ it("returns false when str is null", () => {
5
+ expect(startsWith("a", null)).toBe(false);
6
+ });
7
+
8
+ it("returns false when str is undefined", () => {
9
+ expect(startsWith("a", undefined)).toBe(false);
10
+ });
11
+
12
+ it("returns true when string starts with target", () => {
13
+ expect(startsWith("he", "hello")).toBe(true);
14
+ expect(startsWith("", "hello")).toBe(true); // empty target always matches
15
+ });
16
+
17
+ it("returns false when string does not start with target", () => {
18
+ expect(startsWith("yo", "hello")).toBe(false);
19
+ });
20
+
21
+ it("works with single character target", () => {
22
+ expect(startsWith("h", "hello")).toBe(true);
23
+ expect(startsWith("x", "hello")).toBe(false);
24
+ });
25
+
26
+ it("is case-sensitive", () => {
27
+ expect(startsWith("He", "hello")).toBe(false);
28
+ expect(startsWith("he", "hello")).toBe(true);
29
+ });
30
+ });
package/utils/find.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const find = (predicate, xs) => {
2
+ return (xs || []).find((x, index) => predicate(x, index));
3
+ };
package/utils/index.ts CHANGED
@@ -2,6 +2,12 @@ export { chunk } from "./chunk";
2
2
 
3
3
  export { times } from "./times";
4
4
 
5
+ export { startsWith } from "./startsWith";
6
+
7
+ export { find } from "./find";
8
+
9
+ export { pathOr } from "./pathOr";
10
+
5
11
  export {
6
12
  cloneDeep as clone,
7
13
  flatten,
@@ -0,0 +1,5 @@
1
+ import { get } from "lodash";
2
+
3
+ export const pathOr = (defaultValue, path, record) => {
4
+ return get(record, path, defaultValue);
5
+ };
@@ -0,0 +1,9 @@
1
+ import { isNil } from "lodash";
2
+
3
+ export const startsWith = (target, str) => {
4
+ if (isNil(str)) {
5
+ return false;
6
+ }
7
+
8
+ return str.startsWith(target);
9
+ };