@noriginmedia/norigin-spatial-navigation-core 3.0.0
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/README.md +5 -0
- package/dist/SpatialNavigation.d.ts +281 -0
- package/dist/VisualDebugger.d.ts +21 -0
- package/dist/WritingDirection.d.ts +5 -0
- package/dist/__tests__/SpatialNavigation.RTL.test.d.ts +1 -0
- package/dist/__tests__/SpatialNavigation.test.d.ts +1 -0
- package/dist/__tests__/domNodes.d.ts +3 -0
- package/dist/__tests__/measureLayout.test.d.ts +1 -0
- package/dist/index.cjs +1235 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +1220 -0
- package/dist/measureLayout.d.ts +21 -0
- package/package.json +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var lodashEs = require('lodash-es');
|
|
4
|
+
|
|
5
|
+
/******************************************************************************
|
|
6
|
+
Copyright (c) Microsoft Corporation.
|
|
7
|
+
|
|
8
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
9
|
+
purpose with or without fee is hereby granted.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
12
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
13
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
14
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
15
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
16
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
17
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
18
|
+
***************************************************************************** */
|
|
19
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
var __assign = function() {
|
|
23
|
+
__assign = Object.assign || function __assign(t) {
|
|
24
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
25
|
+
s = arguments[i];
|
|
26
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
|
27
|
+
}
|
|
28
|
+
return t;
|
|
29
|
+
};
|
|
30
|
+
return __assign.apply(this, arguments);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function __spreadArray(to, from, pack) {
|
|
34
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
35
|
+
if (ar || !(i in from)) {
|
|
36
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
37
|
+
ar[i] = from[i];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
44
|
+
var e = new Error(message);
|
|
45
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
var WritingDirection$1;
|
|
49
|
+
(function (WritingDirection) {
|
|
50
|
+
WritingDirection[WritingDirection["LTR"] = 0] = "LTR";
|
|
51
|
+
WritingDirection[WritingDirection["RTL"] = 1] = "RTL";
|
|
52
|
+
})(WritingDirection$1 || (WritingDirection$1 = {}));
|
|
53
|
+
var WritingDirection = WritingDirection$1;
|
|
54
|
+
|
|
55
|
+
// We'll make VisualDebugger no-op for any environments lacking a DOM (e.g. SSR and React Native non-web platforms).
|
|
56
|
+
var hasDOM = typeof window !== 'undefined' && window.document;
|
|
57
|
+
var WIDTH = hasDOM ? window.innerWidth : 0;
|
|
58
|
+
var HEIGHT = hasDOM ? window.innerHeight : 0;
|
|
59
|
+
var VisualDebugger = /** @class */ (function () {
|
|
60
|
+
function VisualDebugger(writingDirection) {
|
|
61
|
+
if (hasDOM) {
|
|
62
|
+
this.debugCtx = VisualDebugger.createCanvas('sn-debug', '1010', writingDirection);
|
|
63
|
+
this.layoutsCtx = VisualDebugger.createCanvas('sn-layouts', '1000', writingDirection);
|
|
64
|
+
this.writingDirection = writingDirection;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
VisualDebugger.createCanvas = function (id, zIndex, writingDirection) {
|
|
68
|
+
var canvas = document.querySelector("#".concat(id)) || document.createElement('canvas');
|
|
69
|
+
canvas.setAttribute('id', id);
|
|
70
|
+
canvas.setAttribute('dir', writingDirection === WritingDirection.LTR ? 'ltr' : 'rtl');
|
|
71
|
+
var ctx = canvas.getContext('2d');
|
|
72
|
+
canvas.style.zIndex = zIndex;
|
|
73
|
+
canvas.style.position = 'fixed';
|
|
74
|
+
canvas.style.top = '0';
|
|
75
|
+
canvas.style.left = '0';
|
|
76
|
+
document.body.appendChild(canvas);
|
|
77
|
+
canvas.width = WIDTH;
|
|
78
|
+
canvas.height = HEIGHT;
|
|
79
|
+
return ctx;
|
|
80
|
+
};
|
|
81
|
+
VisualDebugger.prototype.clear = function () {
|
|
82
|
+
if (!hasDOM) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.debugCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
|
86
|
+
};
|
|
87
|
+
VisualDebugger.prototype.clearLayouts = function () {
|
|
88
|
+
if (!hasDOM) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.layoutsCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
|
92
|
+
};
|
|
93
|
+
VisualDebugger.prototype.drawLayout = function (layout, focusKey, parentFocusKey) {
|
|
94
|
+
if (!hasDOM) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.layoutsCtx.strokeStyle = 'green';
|
|
98
|
+
this.layoutsCtx.strokeRect(layout.left, layout.top, layout.width, layout.height);
|
|
99
|
+
this.layoutsCtx.font = '8px monospace';
|
|
100
|
+
this.layoutsCtx.fillStyle = 'red';
|
|
101
|
+
var horizontalStartDirection = this.writingDirection === WritingDirection.LTR ? 'left' : 'right';
|
|
102
|
+
var horizontalStartCoordinate = layout[horizontalStartDirection];
|
|
103
|
+
this.layoutsCtx.fillText(focusKey, horizontalStartCoordinate, layout.top + 10);
|
|
104
|
+
this.layoutsCtx.fillText(parentFocusKey, horizontalStartCoordinate, layout.top + 25);
|
|
105
|
+
this.layoutsCtx.fillText("".concat(horizontalStartDirection, ": ").concat(horizontalStartCoordinate), horizontalStartCoordinate, layout.top + 40);
|
|
106
|
+
this.layoutsCtx.fillText("top: ".concat(layout.top), horizontalStartCoordinate, layout.top + 55);
|
|
107
|
+
};
|
|
108
|
+
VisualDebugger.prototype.drawPoint = function (x, y, color, size) {
|
|
109
|
+
if (color === void 0) { color = 'blue'; }
|
|
110
|
+
if (size === void 0) { size = 10; }
|
|
111
|
+
if (!hasDOM) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.debugCtx.strokeStyle = color;
|
|
115
|
+
this.debugCtx.lineWidth = 3;
|
|
116
|
+
this.debugCtx.strokeRect(x - size / 2, y - size / 2, size, size);
|
|
117
|
+
};
|
|
118
|
+
return VisualDebugger;
|
|
119
|
+
}());
|
|
120
|
+
|
|
121
|
+
var ELEMENT_NODE = 1;
|
|
122
|
+
var getRect = function (node) {
|
|
123
|
+
var offsetParent = node.offsetParent;
|
|
124
|
+
var height = node.offsetHeight;
|
|
125
|
+
var width = node.offsetWidth;
|
|
126
|
+
var left = node.offsetLeft;
|
|
127
|
+
var top = node.offsetTop;
|
|
128
|
+
while (offsetParent && offsetParent.nodeType === ELEMENT_NODE) {
|
|
129
|
+
left += offsetParent.offsetLeft - offsetParent.scrollLeft;
|
|
130
|
+
top += offsetParent.offsetTop - offsetParent.scrollTop;
|
|
131
|
+
offsetParent = offsetParent.offsetParent;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
height: height,
|
|
135
|
+
left: left,
|
|
136
|
+
top: top,
|
|
137
|
+
width: width
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
var measureLayout = function (node) {
|
|
141
|
+
var relativeNode = node && node.parentElement;
|
|
142
|
+
if (node && relativeNode) {
|
|
143
|
+
var relativeRect = getRect(relativeNode);
|
|
144
|
+
var _a = getRect(node), height = _a.height, left = _a.left, top_1 = _a.top, width = _a.width;
|
|
145
|
+
var x = left - relativeRect.left;
|
|
146
|
+
var y = top_1 - relativeRect.top;
|
|
147
|
+
return {
|
|
148
|
+
x: x,
|
|
149
|
+
y: y,
|
|
150
|
+
width: width,
|
|
151
|
+
height: height,
|
|
152
|
+
left: left,
|
|
153
|
+
top: top_1,
|
|
154
|
+
get right() {
|
|
155
|
+
return this.left + this.width;
|
|
156
|
+
},
|
|
157
|
+
get bottom() {
|
|
158
|
+
return this.top + this.height;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
x: 0,
|
|
164
|
+
y: 0,
|
|
165
|
+
width: 0,
|
|
166
|
+
height: 0,
|
|
167
|
+
left: 0,
|
|
168
|
+
top: 0,
|
|
169
|
+
right: 0,
|
|
170
|
+
bottom: 0
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
var getBoundingClientRect = function (node) {
|
|
174
|
+
if (node && node.getBoundingClientRect) {
|
|
175
|
+
var rect = node.getBoundingClientRect();
|
|
176
|
+
return {
|
|
177
|
+
x: rect.x,
|
|
178
|
+
y: rect.y,
|
|
179
|
+
width: rect.width,
|
|
180
|
+
height: rect.height,
|
|
181
|
+
left: rect.left,
|
|
182
|
+
top: rect.top,
|
|
183
|
+
get right() {
|
|
184
|
+
return this.left + this.width;
|
|
185
|
+
},
|
|
186
|
+
get bottom() {
|
|
187
|
+
return this.top + this.height;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
x: 0,
|
|
193
|
+
y: 0,
|
|
194
|
+
width: 0,
|
|
195
|
+
height: 0,
|
|
196
|
+
left: 0,
|
|
197
|
+
top: 0,
|
|
198
|
+
right: 0,
|
|
199
|
+
bottom: 0
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
var _a;
|
|
204
|
+
var DIRECTION_LEFT = 'left';
|
|
205
|
+
var DIRECTION_RIGHT = 'right';
|
|
206
|
+
var DIRECTION_UP = 'up';
|
|
207
|
+
var DIRECTION_DOWN = 'down';
|
|
208
|
+
var KEY_ENTER = 'enter';
|
|
209
|
+
var DEFAULT_KEY_MAP = (_a = {},
|
|
210
|
+
_a[DIRECTION_LEFT] = [37, 'ArrowLeft'],
|
|
211
|
+
_a[DIRECTION_UP] = [38, 'ArrowUp'],
|
|
212
|
+
_a[DIRECTION_RIGHT] = [39, 'ArrowRight'],
|
|
213
|
+
_a[DIRECTION_DOWN] = [40, 'ArrowDown'],
|
|
214
|
+
_a[KEY_ENTER] = [13, 'Enter'],
|
|
215
|
+
_a);
|
|
216
|
+
var ROOT_FOCUS_KEY = 'SN:ROOT';
|
|
217
|
+
var ADJACENT_SLICE_THRESHOLD = 0.2;
|
|
218
|
+
/**
|
|
219
|
+
* Adjacent slice is 5 times more important than diagonal
|
|
220
|
+
*/
|
|
221
|
+
var ADJACENT_SLICE_WEIGHT = 5;
|
|
222
|
+
var DIAGONAL_SLICE_WEIGHT = 1;
|
|
223
|
+
/**
|
|
224
|
+
* Main coordinate distance is 5 times more important
|
|
225
|
+
*/
|
|
226
|
+
var MAIN_COORDINATE_WEIGHT = 5;
|
|
227
|
+
var AUTO_RESTORE_FOCUS_DELAY = 300;
|
|
228
|
+
var DEBUG_FN_COLORS = ['#0FF', '#FF0', '#F0F'];
|
|
229
|
+
var THROTTLE_OPTIONS = {
|
|
230
|
+
leading: true,
|
|
231
|
+
trailing: false
|
|
232
|
+
};
|
|
233
|
+
var getChildClosestToOrigin = function (children, writingDirection) {
|
|
234
|
+
var comparator = writingDirection === WritingDirection.LTR
|
|
235
|
+
? function (_a) {
|
|
236
|
+
var layout = _a.layout;
|
|
237
|
+
return Math.abs(layout.left) + Math.abs(layout.top);
|
|
238
|
+
}
|
|
239
|
+
: function (_a) {
|
|
240
|
+
var layout = _a.layout;
|
|
241
|
+
return Math.abs(window.innerWidth - layout.right) + Math.abs(layout.top);
|
|
242
|
+
};
|
|
243
|
+
var childrenClosestToOrigin = lodashEs.sortBy(children, comparator);
|
|
244
|
+
return lodashEs.first(childrenClosestToOrigin);
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Takes either a BackwardsCompatibleKeyMap and transforms it into a the new KeyMap format
|
|
248
|
+
* to ensure backwards compatibility.
|
|
249
|
+
*/
|
|
250
|
+
var normalizeKeyMap = function (keyMap) {
|
|
251
|
+
var newKeyMap = {};
|
|
252
|
+
Object.entries(keyMap).forEach(function (_a) {
|
|
253
|
+
var key = _a[0], value = _a[1];
|
|
254
|
+
newKeyMap[key] = Array.isArray(value) ? value : [value];
|
|
255
|
+
});
|
|
256
|
+
return newKeyMap;
|
|
257
|
+
};
|
|
258
|
+
var SpatialNavigationService = /** @class */ (function () {
|
|
259
|
+
function SpatialNavigationService() {
|
|
260
|
+
/**
|
|
261
|
+
* Storage for all focusable components
|
|
262
|
+
*/
|
|
263
|
+
this.focusableComponents = {};
|
|
264
|
+
/**
|
|
265
|
+
* Storing current focused key
|
|
266
|
+
*/
|
|
267
|
+
this.focusKey = null;
|
|
268
|
+
/**
|
|
269
|
+
* This collection contains focus keys of the elements that are having a child focused
|
|
270
|
+
* Might be handy for styling of certain parent components if their child is focused.
|
|
271
|
+
*/
|
|
272
|
+
this.parentsHavingFocusedChild = [];
|
|
273
|
+
this.domNodeFocusOptions = {};
|
|
274
|
+
this.enabled = false;
|
|
275
|
+
this.nativeMode = false;
|
|
276
|
+
this.throttle = 0;
|
|
277
|
+
this.throttleKeypresses = false;
|
|
278
|
+
this.useGetBoundingClientRect = false;
|
|
279
|
+
this.shouldFocusDOMNode = false;
|
|
280
|
+
this.shouldUseNativeEvents = false;
|
|
281
|
+
this.writingDirection = WritingDirection.LTR;
|
|
282
|
+
this.pressedKeys = {};
|
|
283
|
+
/**
|
|
284
|
+
* Flag used to block key events from this service
|
|
285
|
+
* @type {boolean}
|
|
286
|
+
*/
|
|
287
|
+
this.paused = false;
|
|
288
|
+
this.keyDownEventListener = null;
|
|
289
|
+
this.keyUpEventListener = null;
|
|
290
|
+
this.keyMap = DEFAULT_KEY_MAP;
|
|
291
|
+
this.pause = this.pause.bind(this);
|
|
292
|
+
this.resume = this.resume.bind(this);
|
|
293
|
+
this.setFocus = this.setFocus.bind(this);
|
|
294
|
+
this.updateAllLayouts = this.updateAllLayouts.bind(this);
|
|
295
|
+
this.navigateByDirection = this.navigateByDirection.bind(this);
|
|
296
|
+
this.init = this.init.bind(this);
|
|
297
|
+
this.setThrottle = this.setThrottle.bind(this);
|
|
298
|
+
this.destroy = this.destroy.bind(this);
|
|
299
|
+
this.setKeyMap = this.setKeyMap.bind(this);
|
|
300
|
+
this.getCurrentFocusKey = this.getCurrentFocusKey.bind(this);
|
|
301
|
+
this.doesFocusableExist = this.doesFocusableExist.bind(this);
|
|
302
|
+
this.updateRtl = this.updateRtl.bind(this);
|
|
303
|
+
this.setFocusDebounced = lodashEs.debounce(this.setFocus, AUTO_RESTORE_FOCUS_DELAY, {
|
|
304
|
+
leading: false,
|
|
305
|
+
trailing: true
|
|
306
|
+
});
|
|
307
|
+
this.debug = false;
|
|
308
|
+
this.visualDebugger = null;
|
|
309
|
+
this.logIndex = 0;
|
|
310
|
+
this.distanceCalculationMethod = 'corners';
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Used to determine the coordinate that will be used to filter items that are over the "edge"
|
|
314
|
+
*/
|
|
315
|
+
SpatialNavigationService.getCutoffCoordinate = function (isVertical, isIncremental, isSibling, layout, writingDirection) {
|
|
316
|
+
var itemStart = isVertical
|
|
317
|
+
? layout.top
|
|
318
|
+
: writingDirection === WritingDirection.LTR
|
|
319
|
+
? layout.left
|
|
320
|
+
: layout.right;
|
|
321
|
+
var itemEnd = isVertical
|
|
322
|
+
? layout.bottom
|
|
323
|
+
: writingDirection === WritingDirection.LTR
|
|
324
|
+
? layout.right
|
|
325
|
+
: layout.left;
|
|
326
|
+
return isIncremental
|
|
327
|
+
? isSibling
|
|
328
|
+
? itemStart
|
|
329
|
+
: itemEnd
|
|
330
|
+
: isSibling
|
|
331
|
+
? itemEnd
|
|
332
|
+
: itemStart;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Returns two corners (a and b) coordinates that are used as a reference points
|
|
336
|
+
* Where "a" is always leftmost and topmost corner, and "b" is rightmost bottommost corner
|
|
337
|
+
*/
|
|
338
|
+
SpatialNavigationService.getRefCorners = function (direction, isSibling, layout) {
|
|
339
|
+
var result = {
|
|
340
|
+
a: {
|
|
341
|
+
x: 0,
|
|
342
|
+
y: 0
|
|
343
|
+
},
|
|
344
|
+
b: {
|
|
345
|
+
x: 0,
|
|
346
|
+
y: 0
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
switch (direction) {
|
|
350
|
+
case DIRECTION_UP: {
|
|
351
|
+
var y = isSibling ? layout.bottom : layout.top;
|
|
352
|
+
result.a = {
|
|
353
|
+
x: layout.left,
|
|
354
|
+
y: y
|
|
355
|
+
};
|
|
356
|
+
result.b = {
|
|
357
|
+
x: layout.right,
|
|
358
|
+
y: y
|
|
359
|
+
};
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case DIRECTION_DOWN: {
|
|
363
|
+
var y = isSibling ? layout.top : layout.bottom;
|
|
364
|
+
result.a = {
|
|
365
|
+
x: layout.left,
|
|
366
|
+
y: y
|
|
367
|
+
};
|
|
368
|
+
result.b = {
|
|
369
|
+
x: layout.right,
|
|
370
|
+
y: y
|
|
371
|
+
};
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case DIRECTION_LEFT: {
|
|
375
|
+
var x = isSibling ? layout.right : layout.left;
|
|
376
|
+
result.a = {
|
|
377
|
+
x: x,
|
|
378
|
+
y: layout.top
|
|
379
|
+
};
|
|
380
|
+
result.b = {
|
|
381
|
+
x: x,
|
|
382
|
+
y: layout.bottom
|
|
383
|
+
};
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case DIRECTION_RIGHT: {
|
|
387
|
+
var x = isSibling ? layout.left : layout.right;
|
|
388
|
+
result.a = {
|
|
389
|
+
x: x,
|
|
390
|
+
y: layout.top
|
|
391
|
+
};
|
|
392
|
+
result.b = {
|
|
393
|
+
x: x,
|
|
394
|
+
y: layout.bottom
|
|
395
|
+
};
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Calculates if the sibling node is intersecting enough with the ref node by the secondary coordinate
|
|
403
|
+
*/
|
|
404
|
+
SpatialNavigationService.isAdjacentSlice = function (refCorners, siblingCorners, isVerticalDirection) {
|
|
405
|
+
var refA = refCorners.a, refB = refCorners.b;
|
|
406
|
+
var siblingA = siblingCorners.a, siblingB = siblingCorners.b;
|
|
407
|
+
var coordinate = isVerticalDirection ? 'x' : 'y';
|
|
408
|
+
var refCoordinateA = refA[coordinate];
|
|
409
|
+
var refCoordinateB = refB[coordinate];
|
|
410
|
+
var siblingCoordinateA = siblingA[coordinate];
|
|
411
|
+
var siblingCoordinateB = siblingB[coordinate];
|
|
412
|
+
var thresholdDistance = (refCoordinateB - refCoordinateA) * ADJACENT_SLICE_THRESHOLD;
|
|
413
|
+
var intersectionLength = Math.max(0, Math.min(refCoordinateB, siblingCoordinateB) -
|
|
414
|
+
Math.max(refCoordinateA, siblingCoordinateA));
|
|
415
|
+
return intersectionLength >= thresholdDistance;
|
|
416
|
+
};
|
|
417
|
+
SpatialNavigationService.getPrimaryAxisDistance = function (refCorners, siblingCorners, isVerticalDirection) {
|
|
418
|
+
var refA = refCorners.a;
|
|
419
|
+
var siblingA = siblingCorners.a;
|
|
420
|
+
var coordinate = isVerticalDirection ? 'y' : 'x';
|
|
421
|
+
return Math.abs(siblingA[coordinate] - refA[coordinate]);
|
|
422
|
+
};
|
|
423
|
+
SpatialNavigationService.getSecondaryAxisDistance = function (refCorners, siblingCorners, isVerticalDirection, distanceCalculationMethod, customDistanceCalculationFunction) {
|
|
424
|
+
if (customDistanceCalculationFunction) {
|
|
425
|
+
return customDistanceCalculationFunction(refCorners, siblingCorners, isVerticalDirection, distanceCalculationMethod);
|
|
426
|
+
}
|
|
427
|
+
var refA = refCorners.a, refB = refCorners.b;
|
|
428
|
+
var siblingA = siblingCorners.a, siblingB = siblingCorners.b;
|
|
429
|
+
var coordinate = isVerticalDirection ? 'x' : 'y';
|
|
430
|
+
var refCoordinateA = refA[coordinate];
|
|
431
|
+
var refCoordinateB = refB[coordinate];
|
|
432
|
+
var siblingCoordinateA = siblingA[coordinate];
|
|
433
|
+
var siblingCoordinateB = siblingB[coordinate];
|
|
434
|
+
if (distanceCalculationMethod === 'center') {
|
|
435
|
+
var refCoordinateCenter = (refCoordinateA + refCoordinateB) / 2;
|
|
436
|
+
var siblingCoordinateCenter = (siblingCoordinateA + siblingCoordinateB) / 2;
|
|
437
|
+
return Math.abs(refCoordinateCenter - siblingCoordinateCenter);
|
|
438
|
+
}
|
|
439
|
+
if (distanceCalculationMethod === 'edges') {
|
|
440
|
+
// 1. Find the minimum and maximum coordinates for both ref and sibling
|
|
441
|
+
var refCoordinateEdgeMin = Math.min(refCoordinateA, refCoordinateB);
|
|
442
|
+
var siblingCoordinateEdgeMin = Math.min(siblingCoordinateA, siblingCoordinateB);
|
|
443
|
+
var refCoordinateEdgeMax = Math.max(refCoordinateA, refCoordinateB);
|
|
444
|
+
var siblingCoordinateEdgeMax = Math.max(siblingCoordinateA, siblingCoordinateB);
|
|
445
|
+
// 2. Calculate the distances between the closest edges
|
|
446
|
+
var minEdgeDistance = Math.abs(refCoordinateEdgeMin - siblingCoordinateEdgeMin);
|
|
447
|
+
var maxEdgeDistance = Math.abs(refCoordinateEdgeMax - siblingCoordinateEdgeMax);
|
|
448
|
+
// 3. Return the smallest distance between the edges
|
|
449
|
+
return Math.min(minEdgeDistance, maxEdgeDistance);
|
|
450
|
+
}
|
|
451
|
+
// Default to corners
|
|
452
|
+
var distancesToCompare = [
|
|
453
|
+
Math.abs(siblingCoordinateA - refCoordinateA),
|
|
454
|
+
Math.abs(siblingCoordinateA - refCoordinateB),
|
|
455
|
+
Math.abs(siblingCoordinateB - refCoordinateA),
|
|
456
|
+
Math.abs(siblingCoordinateB - refCoordinateB)
|
|
457
|
+
];
|
|
458
|
+
return Math.min.apply(Math, distancesToCompare);
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Inspired by: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS_for_TV/TV_remote_control_navigation#Algorithm_design
|
|
462
|
+
* Ref Corners are the 2 corners of the current component in the direction of navigation
|
|
463
|
+
* They are used as a base to measure adjacent slices
|
|
464
|
+
*/
|
|
465
|
+
SpatialNavigationService.prototype.sortSiblingsByPriority = function (siblings, currentLayout, direction, focusKey) {
|
|
466
|
+
var _this = this;
|
|
467
|
+
var isVerticalDirection = direction === DIRECTION_DOWN || direction === DIRECTION_UP;
|
|
468
|
+
var refCorners = SpatialNavigationService.getRefCorners(direction, false, currentLayout);
|
|
469
|
+
return lodashEs.sortBy(siblings, function (sibling) {
|
|
470
|
+
var siblingCorners = SpatialNavigationService.getRefCorners(direction, true, sibling.layout);
|
|
471
|
+
var isAdjacentSlice = SpatialNavigationService.isAdjacentSlice(refCorners, siblingCorners, isVerticalDirection);
|
|
472
|
+
var primaryAxisFunction = isAdjacentSlice
|
|
473
|
+
? SpatialNavigationService.getPrimaryAxisDistance
|
|
474
|
+
: SpatialNavigationService.getSecondaryAxisDistance;
|
|
475
|
+
var secondaryAxisFunction = isAdjacentSlice
|
|
476
|
+
? SpatialNavigationService.getSecondaryAxisDistance
|
|
477
|
+
: SpatialNavigationService.getPrimaryAxisDistance;
|
|
478
|
+
var primaryAxisDistance = primaryAxisFunction(refCorners, siblingCorners, isVerticalDirection, _this.distanceCalculationMethod, _this.customDistanceCalculationFunction);
|
|
479
|
+
var secondaryAxisDistance = secondaryAxisFunction(refCorners, siblingCorners, isVerticalDirection, _this.distanceCalculationMethod, _this.customDistanceCalculationFunction);
|
|
480
|
+
/**
|
|
481
|
+
* The higher this value is, the less prioritised the candidate is
|
|
482
|
+
*/
|
|
483
|
+
var totalDistancePoints = primaryAxisDistance * MAIN_COORDINATE_WEIGHT + secondaryAxisDistance;
|
|
484
|
+
/**
|
|
485
|
+
* + 1 here is in case of distance is zero, but we still want to apply Adjacent priority weight
|
|
486
|
+
*/
|
|
487
|
+
var priority = (totalDistancePoints + 1) /
|
|
488
|
+
(isAdjacentSlice ? ADJACENT_SLICE_WEIGHT : DIAGONAL_SLICE_WEIGHT);
|
|
489
|
+
_this.log('smartNavigate', "distance (primary, secondary, total weighted) for ".concat(sibling.focusKey, " relative to ").concat(focusKey, " is"), primaryAxisDistance, secondaryAxisDistance, totalDistancePoints);
|
|
490
|
+
_this.log('smartNavigate', "priority for ".concat(sibling.focusKey, " relative to ").concat(focusKey, " is"), priority);
|
|
491
|
+
if (_this.visualDebugger) {
|
|
492
|
+
_this.visualDebugger.drawPoint(siblingCorners.a.x, siblingCorners.a.y, 'yellow', 6);
|
|
493
|
+
_this.visualDebugger.drawPoint(siblingCorners.b.x, siblingCorners.b.y, 'yellow', 6);
|
|
494
|
+
}
|
|
495
|
+
return priority;
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
SpatialNavigationService.prototype.init = function (_a) {
|
|
499
|
+
var _this = this;
|
|
500
|
+
var _b = _a === void 0 ? {} : _a, _c = _b.debug, debug = _c === void 0 ? false : _c, _d = _b.visualDebug, visualDebug = _d === void 0 ? false : _d, _e = _b.nativeMode, nativeMode = _e === void 0 ? false : _e, _f = _b.throttle, throttleParam = _f === void 0 ? 0 : _f, _g = _b.throttleKeypresses, throttleKeypresses = _g === void 0 ? false : _g, _h = _b.useGetBoundingClientRect, useGetBoundingClientRect = _h === void 0 ? false : _h, _j = _b.shouldFocusDOMNode, shouldFocusDOMNode = _j === void 0 ? false : _j, _k = _b.domNodeFocusOptions, domNodeFocusOptions = _k === void 0 ? {} : _k, _l = _b.shouldUseNativeEvents, shouldUseNativeEvents = _l === void 0 ? false : _l, _m = _b.rtl, rtl = _m === void 0 ? false : _m, _o = _b.distanceCalculationMethod, distanceCalculationMethod = _o === void 0 ? 'corners' : _o, _p = _b.customDistanceCalculationFunction, customDistanceCalculationFunction = _p === void 0 ? undefined : _p;
|
|
501
|
+
if (!this.enabled) {
|
|
502
|
+
this.domNodeFocusOptions = domNodeFocusOptions;
|
|
503
|
+
this.enabled = true;
|
|
504
|
+
this.nativeMode = nativeMode;
|
|
505
|
+
this.throttleKeypresses = throttleKeypresses;
|
|
506
|
+
this.useGetBoundingClientRect = useGetBoundingClientRect;
|
|
507
|
+
this.shouldFocusDOMNode = shouldFocusDOMNode && !nativeMode;
|
|
508
|
+
this.shouldUseNativeEvents = shouldUseNativeEvents;
|
|
509
|
+
this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR;
|
|
510
|
+
this.distanceCalculationMethod = distanceCalculationMethod;
|
|
511
|
+
this.customDistanceCalculationFunction =
|
|
512
|
+
customDistanceCalculationFunction;
|
|
513
|
+
this.debug = debug;
|
|
514
|
+
if (!this.nativeMode) {
|
|
515
|
+
if (Number.isInteger(throttleParam) && throttleParam > 0) {
|
|
516
|
+
this.throttle = throttleParam;
|
|
517
|
+
}
|
|
518
|
+
this.bindEventHandlers();
|
|
519
|
+
if (visualDebug) {
|
|
520
|
+
this.visualDebugger = new VisualDebugger(this.writingDirection);
|
|
521
|
+
var draw_1 = function () {
|
|
522
|
+
requestAnimationFrame(function () {
|
|
523
|
+
_this.visualDebugger.clearLayouts();
|
|
524
|
+
lodashEs.forOwn(_this.focusableComponents, function (component, focusKey) {
|
|
525
|
+
_this.visualDebugger.drawLayout(component.layout, focusKey, component.parentFocusKey);
|
|
526
|
+
});
|
|
527
|
+
draw_1();
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
draw_1();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
SpatialNavigationService.prototype.setThrottle = function (_a) {
|
|
536
|
+
var _b = _a === void 0 ? {} : _a, _c = _b.throttle, throttleParam = _c === void 0 ? 0 : _c, _d = _b.throttleKeypresses, throttleKeypresses = _d === void 0 ? false : _d;
|
|
537
|
+
this.throttleKeypresses = throttleKeypresses;
|
|
538
|
+
if (!this.nativeMode) {
|
|
539
|
+
this.unbindEventHandlers();
|
|
540
|
+
if (Number.isInteger(throttleParam)) {
|
|
541
|
+
this.throttle = throttleParam;
|
|
542
|
+
}
|
|
543
|
+
this.bindEventHandlers();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
SpatialNavigationService.prototype.destroy = function () {
|
|
547
|
+
if (this.enabled) {
|
|
548
|
+
this.enabled = false;
|
|
549
|
+
this.nativeMode = false;
|
|
550
|
+
this.throttle = 0;
|
|
551
|
+
this.throttleKeypresses = false;
|
|
552
|
+
this.focusKey = null;
|
|
553
|
+
this.parentsHavingFocusedChild = [];
|
|
554
|
+
this.focusableComponents = {};
|
|
555
|
+
this.paused = false;
|
|
556
|
+
this.keyMap = DEFAULT_KEY_MAP;
|
|
557
|
+
this.unbindEventHandlers();
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
SpatialNavigationService.prototype.getEventType = function (keyCode) {
|
|
561
|
+
return lodashEs.findKey(this.getKeyMap(), function (codeList) { return codeList.includes(keyCode); });
|
|
562
|
+
};
|
|
563
|
+
SpatialNavigationService.getKeyCode = function (event) {
|
|
564
|
+
return event.keyCode || event.code || event.key;
|
|
565
|
+
};
|
|
566
|
+
SpatialNavigationService.prototype.bindEventHandlers = function () {
|
|
567
|
+
var _this = this;
|
|
568
|
+
// We check both because the React Native remote debugger implements window, but not window.addEventListener.
|
|
569
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
570
|
+
this.keyDownEventListener = function (event) {
|
|
571
|
+
if (_this.paused === true) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (_this.debug) {
|
|
575
|
+
_this.logIndex += 1;
|
|
576
|
+
}
|
|
577
|
+
var keyCode = SpatialNavigationService.getKeyCode(event);
|
|
578
|
+
var eventType = _this.getEventType(keyCode);
|
|
579
|
+
if (!eventType) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
_this.pressedKeys[eventType] = _this.pressedKeys[eventType]
|
|
583
|
+
? _this.pressedKeys[eventType] + 1
|
|
584
|
+
: 1;
|
|
585
|
+
if (!_this.shouldUseNativeEvents) {
|
|
586
|
+
event.preventDefault();
|
|
587
|
+
event.stopPropagation();
|
|
588
|
+
}
|
|
589
|
+
var keysDetails = {
|
|
590
|
+
pressedKeys: _this.pressedKeys
|
|
591
|
+
};
|
|
592
|
+
if (eventType === KEY_ENTER && _this.focusKey) {
|
|
593
|
+
_this.onEnterPress(keysDetails);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
var preventDefaultNavigation = _this.onArrowPress(eventType, keysDetails) === false;
|
|
597
|
+
if (_this.visualDebugger) {
|
|
598
|
+
_this.visualDebugger.clear();
|
|
599
|
+
}
|
|
600
|
+
if (preventDefaultNavigation) {
|
|
601
|
+
_this.log('keyDownEventListener', 'default navigation prevented');
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
var direction = lodashEs.findKey(_this.getKeyMap(), function (codeList) {
|
|
605
|
+
return codeList.includes(keyCode);
|
|
606
|
+
});
|
|
607
|
+
_this.smartNavigate(direction, null, { event: event });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
// Apply throttle only if the option we got is > 0 to avoid limiting the listener to every animation frame
|
|
611
|
+
if (this.throttle) {
|
|
612
|
+
this.keyDownEventListenerThrottled = lodashEs.throttle(this.keyDownEventListener.bind(this), this.throttle, THROTTLE_OPTIONS);
|
|
613
|
+
}
|
|
614
|
+
// When throttling then make sure to only throttle key down and cancel any queued functions in case of key up
|
|
615
|
+
this.keyUpEventListener = function (event) {
|
|
616
|
+
var keyCode = SpatialNavigationService.getKeyCode(event);
|
|
617
|
+
var eventType = _this.getEventType(keyCode);
|
|
618
|
+
delete _this.pressedKeys[eventType];
|
|
619
|
+
if (_this.throttle && !_this.throttleKeypresses) {
|
|
620
|
+
_this.keyDownEventListenerThrottled.cancel();
|
|
621
|
+
}
|
|
622
|
+
if (eventType === KEY_ENTER && _this.focusKey) {
|
|
623
|
+
_this.onEnterRelease();
|
|
624
|
+
}
|
|
625
|
+
if (_this.focusKey && (eventType === DIRECTION_LEFT ||
|
|
626
|
+
eventType === DIRECTION_RIGHT ||
|
|
627
|
+
eventType === DIRECTION_UP ||
|
|
628
|
+
eventType === DIRECTION_DOWN)) {
|
|
629
|
+
_this.onArrowRelease(eventType);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
window.addEventListener('keyup', this.keyUpEventListener);
|
|
633
|
+
window.addEventListener('keydown', this.throttle
|
|
634
|
+
? this.keyDownEventListenerThrottled
|
|
635
|
+
: this.keyDownEventListener);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
SpatialNavigationService.prototype.unbindEventHandlers = function () {
|
|
639
|
+
// We check both because the React Native remote debugger implements window, but not window.removeEventListener.
|
|
640
|
+
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
641
|
+
window.removeEventListener('keyup', this.keyUpEventListener);
|
|
642
|
+
this.keyUpEventListener = null;
|
|
643
|
+
var listener = this.throttle
|
|
644
|
+
? this.keyDownEventListenerThrottled
|
|
645
|
+
: this.keyDownEventListener;
|
|
646
|
+
window.removeEventListener('keydown', listener);
|
|
647
|
+
this.keyDownEventListener = null;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
SpatialNavigationService.prototype.onEnterPress = function (keysDetails) {
|
|
651
|
+
var component = this.focusableComponents[this.focusKey];
|
|
652
|
+
/* Guard against last-focused component being unmounted at time of onEnterPress (e.g due to UI fading out) */
|
|
653
|
+
if (!component) {
|
|
654
|
+
this.log('onEnterPress', 'noComponent');
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
/* Suppress onEnterPress if the last-focused item happens to lose its 'focused' status. */
|
|
658
|
+
if (!component.focusable) {
|
|
659
|
+
this.log('onEnterPress', 'componentNotFocusable');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (component.onEnterPress) {
|
|
663
|
+
component.onEnterPress(keysDetails);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
SpatialNavigationService.prototype.onEnterRelease = function () {
|
|
667
|
+
var component = this.focusableComponents[this.focusKey];
|
|
668
|
+
/* Guard against last-focused component being unmounted at time of onEnterRelease (e.g due to UI fading out) */
|
|
669
|
+
if (!component) {
|
|
670
|
+
this.log('onEnterRelease', 'noComponent');
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
/* Suppress onEnterRelease if the last-focused item happens to lose its 'focused' status. */
|
|
674
|
+
if (!component.focusable) {
|
|
675
|
+
this.log('onEnterRelease', 'componentNotFocusable');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (component.onEnterRelease) {
|
|
679
|
+
component.onEnterRelease();
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
SpatialNavigationService.prototype.onArrowPress = function (direction, keysDetails) {
|
|
683
|
+
var component = this.focusableComponents[this.focusKey];
|
|
684
|
+
/* Guard against last-focused component being unmounted at time of onArrowPress (e.g due to UI fading out) */
|
|
685
|
+
if (!component) {
|
|
686
|
+
this.log('onArrowPress', 'noComponent');
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
/* It's okay to navigate AWAY from an item that has lost its 'focused' status, so we don't inspect
|
|
690
|
+
* component.focusable. */
|
|
691
|
+
return (component &&
|
|
692
|
+
component.onArrowPress &&
|
|
693
|
+
component.onArrowPress(direction, keysDetails));
|
|
694
|
+
};
|
|
695
|
+
SpatialNavigationService.prototype.onArrowRelease = function (direction) {
|
|
696
|
+
var component = this.focusableComponents[this.focusKey];
|
|
697
|
+
/* Guard against last-focused component being unmounted at time of onArrowRelease (e.g due to UI fading out) */
|
|
698
|
+
if (!component) {
|
|
699
|
+
this.log('onArrowRelease', 'noComponent');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
/* Suppress onArrowRelease if the last-focused item happens to lose its 'focused' status. */
|
|
703
|
+
if (!component.focusable) {
|
|
704
|
+
this.log('onArrowRelease', 'componentNotFocusable');
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (component.onArrowRelease) {
|
|
708
|
+
component.onArrowRelease(direction);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* Move focus by direction, if you can't use buttons or focusing by key.
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* navigateByDirection('right') // The focus is moved to right
|
|
716
|
+
*/
|
|
717
|
+
SpatialNavigationService.prototype.navigateByDirection = function (direction, focusDetails) {
|
|
718
|
+
if (this.paused === true || !this.enabled || this.nativeMode) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
var validDirections = [
|
|
722
|
+
DIRECTION_DOWN,
|
|
723
|
+
DIRECTION_UP,
|
|
724
|
+
DIRECTION_LEFT,
|
|
725
|
+
DIRECTION_RIGHT
|
|
726
|
+
];
|
|
727
|
+
if (validDirections.includes(direction)) {
|
|
728
|
+
this.log('navigateByDirection', 'direction', direction);
|
|
729
|
+
this.smartNavigate(direction, null, focusDetails);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
this.log('navigateByDirection', "Invalid direction. You passed: `".concat(direction, "`, but you can use only these: "), validDirections);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
/**
|
|
736
|
+
* This function navigates between siblings OR goes up by the Tree
|
|
737
|
+
* Based on the Direction
|
|
738
|
+
*/
|
|
739
|
+
SpatialNavigationService.prototype.smartNavigate = function (direction, fromParentFocusKey, focusDetails) {
|
|
740
|
+
var _this = this;
|
|
741
|
+
if (this.nativeMode) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
var isVerticalDirection = direction === DIRECTION_DOWN || direction === DIRECTION_UP;
|
|
745
|
+
var isIncrementalDirection = direction === DIRECTION_DOWN ||
|
|
746
|
+
(this.writingDirection === WritingDirection.LTR
|
|
747
|
+
? direction === DIRECTION_RIGHT
|
|
748
|
+
: direction === DIRECTION_LEFT);
|
|
749
|
+
this.log('smartNavigate', 'direction', direction);
|
|
750
|
+
this.log('smartNavigate', 'fromParentFocusKey', fromParentFocusKey);
|
|
751
|
+
this.log('smartNavigate', 'this.focusKey', this.focusKey);
|
|
752
|
+
if (!fromParentFocusKey) {
|
|
753
|
+
lodashEs.forOwn(this.focusableComponents, function (component) {
|
|
754
|
+
// eslint-disable-next-line no-param-reassign
|
|
755
|
+
component.layoutUpdated = false;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
var currentComponent = this.focusableComponents[fromParentFocusKey || this.focusKey];
|
|
759
|
+
/**
|
|
760
|
+
* When there's no currently focused component, an attempt is made, to force focus one of
|
|
761
|
+
* the Focusable Containers, that have "forceFocus" flag enabled.
|
|
762
|
+
*/
|
|
763
|
+
if (!fromParentFocusKey && !currentComponent) {
|
|
764
|
+
this.setFocus(this.getForcedFocusKey());
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
this.log('smartNavigate', 'currentComponent', currentComponent ? currentComponent.focusKey : undefined, currentComponent ? currentComponent.node : undefined, currentComponent);
|
|
768
|
+
if (currentComponent) {
|
|
769
|
+
this.updateLayout(currentComponent.focusKey);
|
|
770
|
+
var parentFocusKey_1 = currentComponent.parentFocusKey, focusKey = currentComponent.focusKey, layout = currentComponent.layout;
|
|
771
|
+
var currentCutoffCoordinate_1 = SpatialNavigationService.getCutoffCoordinate(isVerticalDirection, isIncrementalDirection, false, layout, this.writingDirection);
|
|
772
|
+
/**
|
|
773
|
+
* Get only the siblings with the coords on the way of our moving direction
|
|
774
|
+
*/
|
|
775
|
+
var siblings = lodashEs.filter(this.focusableComponents, function (component) {
|
|
776
|
+
if (component.parentFocusKey === parentFocusKey_1 &&
|
|
777
|
+
component.focusable) {
|
|
778
|
+
_this.updateLayout(component.focusKey);
|
|
779
|
+
var siblingCutoffCoordinate = SpatialNavigationService.getCutoffCoordinate(isVerticalDirection, isIncrementalDirection, true, component.layout, _this.writingDirection);
|
|
780
|
+
return isVerticalDirection
|
|
781
|
+
? isIncrementalDirection
|
|
782
|
+
? siblingCutoffCoordinate >= currentCutoffCoordinate_1 // vertical next
|
|
783
|
+
: siblingCutoffCoordinate <= currentCutoffCoordinate_1 // vertical previous
|
|
784
|
+
: _this.writingDirection === WritingDirection.LTR
|
|
785
|
+
? isIncrementalDirection
|
|
786
|
+
? siblingCutoffCoordinate >= currentCutoffCoordinate_1 // horizontal LTR next
|
|
787
|
+
: siblingCutoffCoordinate <= currentCutoffCoordinate_1 // horizontal LTR previous
|
|
788
|
+
: isIncrementalDirection
|
|
789
|
+
? siblingCutoffCoordinate <= currentCutoffCoordinate_1 // horizontal RTL next
|
|
790
|
+
: siblingCutoffCoordinate >= currentCutoffCoordinate_1; // horizontal RTL previous
|
|
791
|
+
}
|
|
792
|
+
return false;
|
|
793
|
+
});
|
|
794
|
+
if (this.debug) {
|
|
795
|
+
this.log('smartNavigate', 'currentCutoffCoordinate', currentCutoffCoordinate_1);
|
|
796
|
+
this.log('smartNavigate', 'siblings', "".concat(siblings.length, " elements:"), siblings.map(function (sibling) { return sibling.focusKey; }).join(', '), siblings.map(function (sibling) { return sibling.node; }), siblings.map(function (sibling) { return sibling; }));
|
|
797
|
+
}
|
|
798
|
+
if (this.visualDebugger) {
|
|
799
|
+
var refCorners = SpatialNavigationService.getRefCorners(direction, false, layout);
|
|
800
|
+
this.visualDebugger.drawPoint(refCorners.a.x, refCorners.a.y);
|
|
801
|
+
this.visualDebugger.drawPoint(refCorners.b.x, refCorners.b.y);
|
|
802
|
+
}
|
|
803
|
+
var sortedSiblings = this.sortSiblingsByPriority(siblings, layout, direction, focusKey);
|
|
804
|
+
var nextComponent = lodashEs.first(sortedSiblings);
|
|
805
|
+
this.log('smartNavigate', 'nextComponent', nextComponent ? nextComponent.focusKey : undefined, nextComponent ? nextComponent.node : undefined, nextComponent);
|
|
806
|
+
if (nextComponent) {
|
|
807
|
+
this.setFocus(nextComponent.focusKey, focusDetails);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
var parentComponent = this.focusableComponents[parentFocusKey_1];
|
|
811
|
+
var focusBoundaryDirections = (parentComponent === null || parentComponent === void 0 ? void 0 : parentComponent.isFocusBoundary)
|
|
812
|
+
? parentComponent.focusBoundaryDirections || [direction]
|
|
813
|
+
: [];
|
|
814
|
+
if (!parentComponent || !focusBoundaryDirections.includes(direction)) {
|
|
815
|
+
this.smartNavigate(direction, parentFocusKey_1, focusDetails);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
SpatialNavigationService.prototype.saveLastFocusedChildKey = function (component, focusKey) {
|
|
821
|
+
if (component) {
|
|
822
|
+
this.log('saveLastFocusedChildKey', "".concat(component.focusKey, " lastFocusedChildKey set"), focusKey);
|
|
823
|
+
// eslint-disable-next-line no-param-reassign
|
|
824
|
+
component.lastFocusedChildKey = focusKey;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
SpatialNavigationService.prototype.log = function (functionName, debugString) {
|
|
828
|
+
var rest = [];
|
|
829
|
+
for (var _i = 2; _i < arguments.length; _i++) {
|
|
830
|
+
rest[_i - 2] = arguments[_i];
|
|
831
|
+
}
|
|
832
|
+
if (this.debug) {
|
|
833
|
+
// eslint-disable-next-line no-console
|
|
834
|
+
console.log.apply(console, __spreadArray(["%c".concat(functionName, "%c").concat(debugString), "background: ".concat(DEBUG_FN_COLORS[this.logIndex % DEBUG_FN_COLORS.length], "; color: black; padding: 1px 5px;"), 'background: #333; color: #BADA55; padding: 1px 5px;'], rest, false));
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
/**
|
|
838
|
+
* Returns the current focus key
|
|
839
|
+
*/
|
|
840
|
+
SpatialNavigationService.prototype.getCurrentFocusKey = function () {
|
|
841
|
+
return this.focusKey;
|
|
842
|
+
};
|
|
843
|
+
/**
|
|
844
|
+
* Returns the focus key to which focus can be forced if there are force-focusable components.
|
|
845
|
+
* A component closest to the top left viewport corner (0,0) is returned.
|
|
846
|
+
*/
|
|
847
|
+
SpatialNavigationService.prototype.getForcedFocusKey = function () {
|
|
848
|
+
var _a;
|
|
849
|
+
var forceFocusableComponents = lodashEs.filter(this.focusableComponents, function (component) { return component.focusable && component.forceFocus; });
|
|
850
|
+
/**
|
|
851
|
+
* Searching of the top level component that is closest to the top left viewport corner (0,0).
|
|
852
|
+
* To achieve meaningful and coherent results, 'down' direction is forced.
|
|
853
|
+
*/
|
|
854
|
+
var sortedForceFocusableComponents = this.sortSiblingsByPriority(forceFocusableComponents, {
|
|
855
|
+
x: 0,
|
|
856
|
+
y: 0,
|
|
857
|
+
width: 0,
|
|
858
|
+
height: 0,
|
|
859
|
+
left: 0,
|
|
860
|
+
top: 0,
|
|
861
|
+
right: 0,
|
|
862
|
+
bottom: 0,
|
|
863
|
+
node: null
|
|
864
|
+
}, 'down', ROOT_FOCUS_KEY);
|
|
865
|
+
return (_a = lodashEs.first(sortedForceFocusableComponents)) === null || _a === void 0 ? void 0 : _a.focusKey;
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* This function tries to determine the next component to Focus
|
|
869
|
+
* It's either the target node OR the one down by the Tree if node has children components
|
|
870
|
+
* Based on "targetFocusKey" which means the "intended component to focus"
|
|
871
|
+
*/
|
|
872
|
+
SpatialNavigationService.prototype.getNextFocusKey = function (targetFocusKey) {
|
|
873
|
+
var _this = this;
|
|
874
|
+
var targetComponent = this.focusableComponents[targetFocusKey];
|
|
875
|
+
/**
|
|
876
|
+
* Security check, if component doesn't exist, stay on the same focusKey
|
|
877
|
+
*/
|
|
878
|
+
if (!targetComponent || this.nativeMode) {
|
|
879
|
+
return targetFocusKey;
|
|
880
|
+
}
|
|
881
|
+
var children = lodashEs.filter(this.focusableComponents, function (component) {
|
|
882
|
+
return component.parentFocusKey === targetFocusKey && component.focusable;
|
|
883
|
+
});
|
|
884
|
+
if (children.length > 0) {
|
|
885
|
+
var lastFocusedChildKey = targetComponent.lastFocusedChildKey, preferredChildFocusKey = targetComponent.preferredChildFocusKey;
|
|
886
|
+
this.log('getNextFocusKey', 'lastFocusedChildKey is', lastFocusedChildKey);
|
|
887
|
+
this.log('getNextFocusKey', 'preferredChildFocusKey is', preferredChildFocusKey);
|
|
888
|
+
/**
|
|
889
|
+
* First of all trying to focus last focused child
|
|
890
|
+
*/
|
|
891
|
+
if (lastFocusedChildKey &&
|
|
892
|
+
targetComponent.saveLastFocusedChild &&
|
|
893
|
+
this.isParticipatingFocusableComponent(lastFocusedChildKey)) {
|
|
894
|
+
this.log('getNextFocusKey', 'lastFocusedChildKey will be focused', lastFocusedChildKey);
|
|
895
|
+
return this.getNextFocusKey(lastFocusedChildKey);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* If there is no lastFocusedChild, trying to focus the preferred focused key
|
|
899
|
+
*/
|
|
900
|
+
if (preferredChildFocusKey &&
|
|
901
|
+
this.isParticipatingFocusableComponent(preferredChildFocusKey)) {
|
|
902
|
+
this.log('getNextFocusKey', 'preferredChildFocusKey will be focused', preferredChildFocusKey);
|
|
903
|
+
return this.getNextFocusKey(preferredChildFocusKey);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Otherwise, trying to focus something by coordinates
|
|
907
|
+
*/
|
|
908
|
+
children.forEach(function (component) { return _this.updateLayout(component.focusKey); });
|
|
909
|
+
var childKey = getChildClosestToOrigin(children, this.writingDirection).focusKey;
|
|
910
|
+
this.log('getNextFocusKey', 'childKey will be focused', childKey);
|
|
911
|
+
return this.getNextFocusKey(childKey);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* If no children, just return targetFocusKey back
|
|
915
|
+
*/
|
|
916
|
+
this.log('getNextFocusKey', 'targetFocusKey', targetFocusKey);
|
|
917
|
+
return targetFocusKey;
|
|
918
|
+
};
|
|
919
|
+
SpatialNavigationService.prototype.addFocusable = function (_a) {
|
|
920
|
+
var focusKey = _a.focusKey, node = _a.node, parentFocusKey = _a.parentFocusKey, onEnterPress = _a.onEnterPress, onEnterRelease = _a.onEnterRelease, onArrowPress = _a.onArrowPress, onArrowRelease = _a.onArrowRelease, onFocus = _a.onFocus, onBlur = _a.onBlur, saveLastFocusedChild = _a.saveLastFocusedChild, trackChildren = _a.trackChildren, onUpdateFocus = _a.onUpdateFocus, onUpdateHasFocusedChild = _a.onUpdateHasFocusedChild, preferredChildFocusKey = _a.preferredChildFocusKey, autoRestoreFocus = _a.autoRestoreFocus, forceFocus = _a.forceFocus, focusable = _a.focusable, isFocusBoundary = _a.isFocusBoundary, focusBoundaryDirections = _a.focusBoundaryDirections;
|
|
921
|
+
this.focusableComponents[focusKey] = {
|
|
922
|
+
focusKey: focusKey,
|
|
923
|
+
node: node,
|
|
924
|
+
parentFocusKey: parentFocusKey,
|
|
925
|
+
onEnterPress: onEnterPress,
|
|
926
|
+
onEnterRelease: onEnterRelease,
|
|
927
|
+
onArrowPress: onArrowPress,
|
|
928
|
+
onArrowRelease: onArrowRelease,
|
|
929
|
+
onFocus: onFocus,
|
|
930
|
+
onBlur: onBlur,
|
|
931
|
+
onUpdateFocus: onUpdateFocus,
|
|
932
|
+
onUpdateHasFocusedChild: onUpdateHasFocusedChild,
|
|
933
|
+
saveLastFocusedChild: saveLastFocusedChild,
|
|
934
|
+
trackChildren: trackChildren,
|
|
935
|
+
preferredChildFocusKey: preferredChildFocusKey,
|
|
936
|
+
focusable: focusable,
|
|
937
|
+
isFocusBoundary: isFocusBoundary,
|
|
938
|
+
focusBoundaryDirections: focusBoundaryDirections,
|
|
939
|
+
autoRestoreFocus: autoRestoreFocus,
|
|
940
|
+
forceFocus: forceFocus,
|
|
941
|
+
lastFocusedChildKey: null,
|
|
942
|
+
layout: {
|
|
943
|
+
x: 0,
|
|
944
|
+
y: 0,
|
|
945
|
+
width: 0,
|
|
946
|
+
height: 0,
|
|
947
|
+
left: 0,
|
|
948
|
+
top: 0,
|
|
949
|
+
right: 0,
|
|
950
|
+
bottom: 0,
|
|
951
|
+
/**
|
|
952
|
+
* Node ref is also duplicated in layout to be reported in onFocus callback
|
|
953
|
+
*/
|
|
954
|
+
node: node
|
|
955
|
+
},
|
|
956
|
+
layoutUpdated: false
|
|
957
|
+
};
|
|
958
|
+
if (!node) {
|
|
959
|
+
// eslint-disable-next-line no-console
|
|
960
|
+
console.warn('Component added without a node reference. This will result in its coordinates being empty and may cause lost focus. Check the "ref" passed to "useFocusable": ', this.focusableComponents[focusKey]);
|
|
961
|
+
}
|
|
962
|
+
if (this.nativeMode) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
this.updateLayout(focusKey);
|
|
966
|
+
this.log('addFocusable', 'Component added: ', this.focusableComponents[focusKey]);
|
|
967
|
+
/**
|
|
968
|
+
* If for some reason this component was already focused before it was added, call the update
|
|
969
|
+
*/
|
|
970
|
+
if (focusKey === this.focusKey) {
|
|
971
|
+
this.setFocus(preferredChildFocusKey || focusKey);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Parent nodes are created after children, and child may focus itself.
|
|
975
|
+
* If so, it's required to check if parent lies on a path to focused child.
|
|
976
|
+
*/
|
|
977
|
+
var currentComponent = this.focusableComponents[this.focusKey];
|
|
978
|
+
while (currentComponent) {
|
|
979
|
+
if (currentComponent.parentFocusKey === focusKey) {
|
|
980
|
+
this.updateParentsHasFocusedChild(this.focusKey, {});
|
|
981
|
+
this.updateParentsLastFocusedChild(this.focusKey);
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
currentComponent =
|
|
985
|
+
this.focusableComponents[currentComponent.parentFocusKey];
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
SpatialNavigationService.prototype.removeFocusable = function (_a) {
|
|
989
|
+
var focusKey = _a.focusKey;
|
|
990
|
+
var componentToRemove = this.focusableComponents[focusKey];
|
|
991
|
+
if (componentToRemove) {
|
|
992
|
+
var parentFocusKey = componentToRemove.parentFocusKey, onUpdateFocus = componentToRemove.onUpdateFocus;
|
|
993
|
+
onUpdateFocus(false);
|
|
994
|
+
this.log('removeFocusable', 'Component removed: ', componentToRemove);
|
|
995
|
+
delete this.focusableComponents[focusKey];
|
|
996
|
+
var hadFocusedChild = this.parentsHavingFocusedChild.includes(focusKey);
|
|
997
|
+
this.parentsHavingFocusedChild = this.parentsHavingFocusedChild.filter(function (parentWithFocusedChild) { return parentWithFocusedChild !== focusKey; });
|
|
998
|
+
var parentComponent = this.focusableComponents[parentFocusKey];
|
|
999
|
+
var isFocused = focusKey === this.focusKey;
|
|
1000
|
+
/**
|
|
1001
|
+
* If the component was stored as lastFocusedChild, clear lastFocusedChildKey from parent
|
|
1002
|
+
*/
|
|
1003
|
+
if (parentComponent && parentComponent.lastFocusedChildKey === focusKey) {
|
|
1004
|
+
parentComponent.lastFocusedChildKey = null;
|
|
1005
|
+
}
|
|
1006
|
+
if (this.nativeMode) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* If the component was also focused at this time, OR had focused child, focus its parent -> it will focus another child
|
|
1011
|
+
* Normally the order of components unmount is children -> parents, but sometimes parent can be removed before the child
|
|
1012
|
+
* So we need to check not only for the current Leaf component focus state, but also if it was a Parent that had focused child
|
|
1013
|
+
*/
|
|
1014
|
+
if ((isFocused || hadFocusedChild) &&
|
|
1015
|
+
parentComponent &&
|
|
1016
|
+
parentComponent.autoRestoreFocus) {
|
|
1017
|
+
this.log('removeFocusable', 'Component removed: ', isFocused ? 'Leaf component' : 'Container component', 'Auto restoring focus to: ', parentFocusKey);
|
|
1018
|
+
/**
|
|
1019
|
+
* Focusing parent with a slight delay
|
|
1020
|
+
* This is to avoid multiple focus restorations if multiple children getting unmounted in one render cycle
|
|
1021
|
+
*/
|
|
1022
|
+
this.setFocusDebounced(parentFocusKey);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
SpatialNavigationService.prototype.getNodeLayoutByFocusKey = function (focusKey) {
|
|
1027
|
+
var component = this.focusableComponents[focusKey];
|
|
1028
|
+
if (component) {
|
|
1029
|
+
this.updateLayout(component.focusKey);
|
|
1030
|
+
return component.layout;
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
};
|
|
1034
|
+
SpatialNavigationService.prototype.setCurrentFocusedKey = function (newFocusKey, focusDetails) {
|
|
1035
|
+
var _a, _b, _c, _d;
|
|
1036
|
+
if (this.isFocusableComponent(this.focusKey) &&
|
|
1037
|
+
newFocusKey !== this.focusKey) {
|
|
1038
|
+
var oldComponent = this.focusableComponents[this.focusKey];
|
|
1039
|
+
oldComponent.onUpdateFocus(false);
|
|
1040
|
+
oldComponent.onBlur(this.getNodeLayoutByFocusKey(this.focusKey), focusDetails);
|
|
1041
|
+
(_b = (_a = oldComponent.node) === null || _a === void 0 ? void 0 : _a.removeAttribute) === null || _b === void 0 ? void 0 : _b.call(_a, 'data-focused');
|
|
1042
|
+
this.log('setCurrentFocusedKey', 'onBlur', oldComponent);
|
|
1043
|
+
}
|
|
1044
|
+
this.focusKey = newFocusKey;
|
|
1045
|
+
if (this.isFocusableComponent(this.focusKey)) {
|
|
1046
|
+
var newComponent = this.focusableComponents[this.focusKey];
|
|
1047
|
+
if (this.shouldFocusDOMNode && newComponent.node) {
|
|
1048
|
+
newComponent.node.focus(this.domNodeFocusOptions);
|
|
1049
|
+
}
|
|
1050
|
+
(_d = (_c = newComponent.node) === null || _c === void 0 ? void 0 : _c.setAttribute) === null || _d === void 0 ? void 0 : _d.call(_c, 'data-focused', 'true');
|
|
1051
|
+
newComponent.onUpdateFocus(true);
|
|
1052
|
+
newComponent.onFocus(this.getNodeLayoutByFocusKey(this.focusKey), focusDetails);
|
|
1053
|
+
this.log('setCurrentFocusedKey', 'onFocus', newComponent);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
SpatialNavigationService.prototype.updateParentsHasFocusedChild = function (focusKey, focusDetails) {
|
|
1057
|
+
var _this = this;
|
|
1058
|
+
var parents = [];
|
|
1059
|
+
var currentComponent = this.focusableComponents[focusKey];
|
|
1060
|
+
/**
|
|
1061
|
+
* Recursively iterate the tree up and find all the parents' focus keys
|
|
1062
|
+
*/
|
|
1063
|
+
while (currentComponent) {
|
|
1064
|
+
var parentFocusKey = currentComponent.parentFocusKey;
|
|
1065
|
+
var parentComponent = this.focusableComponents[parentFocusKey];
|
|
1066
|
+
if (parentComponent) {
|
|
1067
|
+
var currentParentFocusKey = parentComponent.focusKey;
|
|
1068
|
+
parents.push(currentParentFocusKey);
|
|
1069
|
+
}
|
|
1070
|
+
currentComponent = parentComponent;
|
|
1071
|
+
}
|
|
1072
|
+
var parentsToRemoveFlag = lodashEs.difference(this.parentsHavingFocusedChild, parents);
|
|
1073
|
+
var parentsToAddFlag = lodashEs.difference(parents, this.parentsHavingFocusedChild);
|
|
1074
|
+
lodashEs.forEach(parentsToRemoveFlag, function (parentFocusKey) {
|
|
1075
|
+
var parentComponent = _this.focusableComponents[parentFocusKey];
|
|
1076
|
+
if (parentComponent && parentComponent.trackChildren) {
|
|
1077
|
+
parentComponent.onUpdateHasFocusedChild(false);
|
|
1078
|
+
}
|
|
1079
|
+
_this.onIntermediateNodeBecameBlurred(parentFocusKey, focusDetails);
|
|
1080
|
+
});
|
|
1081
|
+
lodashEs.forEach(parentsToAddFlag, function (parentFocusKey) {
|
|
1082
|
+
var parentComponent = _this.focusableComponents[parentFocusKey];
|
|
1083
|
+
if (parentComponent && parentComponent.trackChildren) {
|
|
1084
|
+
parentComponent.onUpdateHasFocusedChild(true);
|
|
1085
|
+
}
|
|
1086
|
+
_this.onIntermediateNodeBecameFocused(parentFocusKey, focusDetails);
|
|
1087
|
+
});
|
|
1088
|
+
this.parentsHavingFocusedChild = parents;
|
|
1089
|
+
};
|
|
1090
|
+
SpatialNavigationService.prototype.updateParentsLastFocusedChild = function (focusKey) {
|
|
1091
|
+
var currentComponent = this.focusableComponents[focusKey];
|
|
1092
|
+
/**
|
|
1093
|
+
* Recursively iterate the tree up and update all the parent's lastFocusedChild
|
|
1094
|
+
*/
|
|
1095
|
+
while (currentComponent) {
|
|
1096
|
+
var parentFocusKey = currentComponent.parentFocusKey;
|
|
1097
|
+
var parentComponent = this.focusableComponents[parentFocusKey];
|
|
1098
|
+
if (parentComponent) {
|
|
1099
|
+
this.saveLastFocusedChildKey(parentComponent, currentComponent.focusKey);
|
|
1100
|
+
}
|
|
1101
|
+
currentComponent = parentComponent;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
SpatialNavigationService.prototype.getKeyMap = function () {
|
|
1105
|
+
return this.keyMap;
|
|
1106
|
+
};
|
|
1107
|
+
SpatialNavigationService.prototype.setKeyMap = function (keyMap) {
|
|
1108
|
+
this.keyMap = __assign(__assign({}, this.getKeyMap()), normalizeKeyMap(keyMap));
|
|
1109
|
+
};
|
|
1110
|
+
SpatialNavigationService.prototype.isFocusableComponent = function (focusKey) {
|
|
1111
|
+
return !!this.focusableComponents[focusKey];
|
|
1112
|
+
};
|
|
1113
|
+
/**
|
|
1114
|
+
* Checks whether the focusableComponent is actually participating in spatial navigation (in other words, is a
|
|
1115
|
+
* 'focusable' focusableComponent). Seems less confusing than calling it isFocusableFocusableComponent()
|
|
1116
|
+
*/
|
|
1117
|
+
SpatialNavigationService.prototype.isParticipatingFocusableComponent = function (focusKey) {
|
|
1118
|
+
return (this.isFocusableComponent(focusKey) &&
|
|
1119
|
+
this.focusableComponents[focusKey].focusable);
|
|
1120
|
+
};
|
|
1121
|
+
SpatialNavigationService.prototype.onIntermediateNodeBecameFocused = function (focusKey, focusDetails) {
|
|
1122
|
+
if (this.isParticipatingFocusableComponent(focusKey)) {
|
|
1123
|
+
this.focusableComponents[focusKey].onFocus(this.getNodeLayoutByFocusKey(focusKey), focusDetails);
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
SpatialNavigationService.prototype.onIntermediateNodeBecameBlurred = function (focusKey, focusDetails) {
|
|
1127
|
+
if (this.isParticipatingFocusableComponent(focusKey)) {
|
|
1128
|
+
this.focusableComponents[focusKey].onBlur(this.getNodeLayoutByFocusKey(focusKey), focusDetails);
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
SpatialNavigationService.prototype.pause = function () {
|
|
1132
|
+
this.paused = true;
|
|
1133
|
+
};
|
|
1134
|
+
SpatialNavigationService.prototype.resume = function () {
|
|
1135
|
+
this.paused = false;
|
|
1136
|
+
};
|
|
1137
|
+
SpatialNavigationService.prototype.setFocus = function (focusKey, focusDetails) {
|
|
1138
|
+
if (focusDetails === void 0) { focusDetails = {}; }
|
|
1139
|
+
// Cancel any pending auto-restore focus calls if we are setting focus manually
|
|
1140
|
+
this.setFocusDebounced.cancel();
|
|
1141
|
+
if (!this.enabled) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
this.log('setFocus', 'focusKey', focusKey);
|
|
1145
|
+
/**
|
|
1146
|
+
* When focusKey is not provided or is equal to `ROOT_FOCUS_KEY`, an attempt is made,
|
|
1147
|
+
* to force focus one of the Focusable Containers, that have "forceFocus" flag enabled.
|
|
1148
|
+
* A component closest to the top left viewport corner (0,0) is force-focused.
|
|
1149
|
+
*/
|
|
1150
|
+
if (!focusKey || focusKey === ROOT_FOCUS_KEY) {
|
|
1151
|
+
// eslint-disable-next-line no-param-reassign
|
|
1152
|
+
focusKey = this.getForcedFocusKey();
|
|
1153
|
+
}
|
|
1154
|
+
var newFocusKey = this.getNextFocusKey(focusKey);
|
|
1155
|
+
this.log('setFocus', 'newFocusKey', newFocusKey);
|
|
1156
|
+
this.setCurrentFocusedKey(newFocusKey, focusDetails);
|
|
1157
|
+
this.updateParentsHasFocusedChild(newFocusKey, focusDetails);
|
|
1158
|
+
this.updateParentsLastFocusedChild(newFocusKey);
|
|
1159
|
+
};
|
|
1160
|
+
SpatialNavigationService.prototype.updateAllLayouts = function () {
|
|
1161
|
+
var _this = this;
|
|
1162
|
+
if (!this.enabled || this.nativeMode) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
lodashEs.forOwn(this.focusableComponents, function (component, focusKey) {
|
|
1166
|
+
_this.updateLayout(focusKey);
|
|
1167
|
+
});
|
|
1168
|
+
};
|
|
1169
|
+
SpatialNavigationService.prototype.updateLayout = function (focusKey) {
|
|
1170
|
+
var component = this.focusableComponents[focusKey];
|
|
1171
|
+
if (!component || this.nativeMode || component.layoutUpdated) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
var node = component.node;
|
|
1175
|
+
var layout = this.useGetBoundingClientRect
|
|
1176
|
+
? getBoundingClientRect(node)
|
|
1177
|
+
: measureLayout(node);
|
|
1178
|
+
component.layout = __assign(__assign({}, layout), { node: node });
|
|
1179
|
+
};
|
|
1180
|
+
SpatialNavigationService.prototype.updateFocusable = function (focusKey, _a) {
|
|
1181
|
+
var node = _a.node, preferredChildFocusKey = _a.preferredChildFocusKey, focusable = _a.focusable, isFocusBoundary = _a.isFocusBoundary, focusBoundaryDirections = _a.focusBoundaryDirections, onEnterPress = _a.onEnterPress, onEnterRelease = _a.onEnterRelease, onArrowPress = _a.onArrowPress, onFocus = _a.onFocus, onBlur = _a.onBlur;
|
|
1182
|
+
if (this.nativeMode) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
var component = this.focusableComponents[focusKey];
|
|
1186
|
+
if (component) {
|
|
1187
|
+
component.preferredChildFocusKey = preferredChildFocusKey;
|
|
1188
|
+
component.focusable = focusable;
|
|
1189
|
+
component.isFocusBoundary = isFocusBoundary;
|
|
1190
|
+
component.focusBoundaryDirections = focusBoundaryDirections;
|
|
1191
|
+
component.onEnterPress = onEnterPress;
|
|
1192
|
+
component.onEnterRelease = onEnterRelease;
|
|
1193
|
+
component.onArrowPress = onArrowPress;
|
|
1194
|
+
component.onFocus = onFocus;
|
|
1195
|
+
component.onBlur = onBlur;
|
|
1196
|
+
if (node) {
|
|
1197
|
+
component.node = node;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
SpatialNavigationService.prototype.isNativeMode = function () {
|
|
1202
|
+
return this.nativeMode;
|
|
1203
|
+
};
|
|
1204
|
+
SpatialNavigationService.prototype.doesFocusableExist = function (focusKey) {
|
|
1205
|
+
return !!this.focusableComponents[focusKey];
|
|
1206
|
+
};
|
|
1207
|
+
/**
|
|
1208
|
+
* This function updates the writing direction
|
|
1209
|
+
* @param rtl whether the writing direction is right-to-left
|
|
1210
|
+
*/
|
|
1211
|
+
SpatialNavigationService.prototype.updateRtl = function (rtl) {
|
|
1212
|
+
this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR;
|
|
1213
|
+
};
|
|
1214
|
+
return SpatialNavigationService;
|
|
1215
|
+
}());
|
|
1216
|
+
/**
|
|
1217
|
+
* Export singleton
|
|
1218
|
+
*/
|
|
1219
|
+
var SpatialNavigation = new SpatialNavigationService();
|
|
1220
|
+
var init = SpatialNavigation.init, setThrottle = SpatialNavigation.setThrottle, destroy = SpatialNavigation.destroy, setKeyMap = SpatialNavigation.setKeyMap, setFocus = SpatialNavigation.setFocus, navigateByDirection = SpatialNavigation.navigateByDirection, pause = SpatialNavigation.pause, resume = SpatialNavigation.resume, updateAllLayouts = SpatialNavigation.updateAllLayouts, getCurrentFocusKey = SpatialNavigation.getCurrentFocusKey, doesFocusableExist = SpatialNavigation.doesFocusableExist, updateRtl = SpatialNavigation.updateRtl;
|
|
1221
|
+
|
|
1222
|
+
exports.ROOT_FOCUS_KEY = ROOT_FOCUS_KEY;
|
|
1223
|
+
exports.SpatialNavigation = SpatialNavigation;
|
|
1224
|
+
exports.destroy = destroy;
|
|
1225
|
+
exports.doesFocusableExist = doesFocusableExist;
|
|
1226
|
+
exports.getCurrentFocusKey = getCurrentFocusKey;
|
|
1227
|
+
exports.init = init;
|
|
1228
|
+
exports.navigateByDirection = navigateByDirection;
|
|
1229
|
+
exports.pause = pause;
|
|
1230
|
+
exports.resume = resume;
|
|
1231
|
+
exports.setFocus = setFocus;
|
|
1232
|
+
exports.setKeyMap = setKeyMap;
|
|
1233
|
+
exports.setThrottle = setThrottle;
|
|
1234
|
+
exports.updateAllLayouts = updateAllLayouts;
|
|
1235
|
+
exports.updateRtl = updateRtl;
|