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