@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.
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -0
- package/appUtils/focusManager/__tests__/focusManager.test.js +1 -1
- package/appUtils/focusManager/index.ts +72 -12
- package/appUtils/focusManagerAux/utils/index.ts +88 -3
- package/package.json +2 -2
- package/reactHooks/state/index.ts +1 -1
- package/reactHooks/state/useHomeRiver.ts +4 -2
- package/screenPickerUtils/index.ts +7 -0
- package/utils/__tests__/find.test.ts +36 -0
- package/utils/__tests__/pathOr.test.ts +37 -0
- package/utils/__tests__/startsWith.test.ts +30 -0
- package/utils/find.ts +3 -0
- package/utils/index.ts +6 -0
- package/utils/pathOr.ts +5 -0
- package/utils/startsWith.ts +9 -0
|
@@ -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],
|
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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 {
|
|
2
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,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
|
|
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
package/utils/index.ts
CHANGED
package/utils/pathOr.ts
ADDED