Skip to content

Commit 77b4f7d

Browse files
Restore lost focus via directional navigation (#93)
1 parent b55179a commit 77b4f7d

3 files changed

Lines changed: 65 additions & 6 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ By default, when the currently focused component is unmounted (deleted), navigat
326326
on the nearest available sibling of that component. If this behavior is undesirable, you can disable it by setting this
327327
flag to `false`.
328328

329+
##### `forceFocus` (default: false)
330+
This flag makes the Focusable Container force-focusable. When there's more than one force-focusable component,
331+
the closest to the top left viewport corner (0,0) is force-focused. Such containers can be force-focused when there's
332+
no currently focused component (or `focusKey` points to not existing component) when navigating with arrows.
333+
Also, when `focusKey` provided to `setFocus` is not defined or equal to `ROOT_FOCUS_KEY`.
334+
In other words, if focus is lost, it can be restored to one of force-focusable components by navigating with arrows
335+
or by focusing `ROOT_FOCUS_KEY`.
336+
329337
##### `isFocusBoundary` (default: false)
330338
This flag makes the Focusable Container keep the focus inside its boundaries. It will only block the focus from leaving
331339
the Container via directional navigation. You can still set the focus manually anywhere via `setFocus`.
@@ -391,7 +399,9 @@ Method to set the focus on the current component. I.e. to set the focus to the P
391399
the Popup component when it is displayed.
392400

393401
##### `setFocus` (function) `(focusKey: string) => void`
394-
Method to manually set the focus to a component providing its `focusKey`.
402+
Method to manually set the focus to a component providing its `focusKey`. If `focusKey` is not provided or
403+
is equal to `ROOT_FOCUS_KEY`, an attempt of focusing one of the force-focusable components is made.
404+
See `useFocusable` hook [`forceFocus`](#forcefocus-default-false) parameter for more details.
395405

396406
##### `focused` (boolean)
397407
Flag that indicates that the current component is focused.

src/SpatialNavigation.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ interface FocusableComponent {
7777
isFocusBoundary: boolean;
7878
focusBoundaryDirections?: string[];
7979
autoRestoreFocus: boolean;
80+
forceFocus: boolean;
8081
lastFocusedChildKey?: string;
8182
layout?: FocusableComponentLayout;
8283
layoutUpdated?: boolean;
@@ -873,6 +874,11 @@ class SpatialNavigationService {
873874
return;
874875
}
875876

877+
const isVerticalDirection =
878+
direction === DIRECTION_DOWN || direction === DIRECTION_UP;
879+
const isIncrementalDirection =
880+
direction === DIRECTION_DOWN || direction === DIRECTION_RIGHT;
881+
876882
this.log('smartNavigate', 'direction', direction);
877883
this.log('smartNavigate', 'fromParentFocusKey', fromParentFocusKey);
878884
this.log('smartNavigate', 'this.focusKey', this.focusKey);
@@ -887,6 +893,15 @@ class SpatialNavigationService {
887893
const currentComponent =
888894
this.focusableComponents[fromParentFocusKey || this.focusKey];
889895

896+
/**
897+
* When there's no currently focused component, an attempt is made, to force focus one of
898+
* the Focusable Containers, that have "forceFocus" flag enabled.
899+
*/
900+
if (!fromParentFocusKey && !currentComponent) {
901+
this.setFocus(this.getForcedFocusKey());
902+
return;
903+
}
904+
890905
this.log(
891906
'smartNavigate',
892907
'currentComponent',
@@ -899,11 +914,6 @@ class SpatialNavigationService {
899914
this.updateLayout(currentComponent.focusKey);
900915
const { parentFocusKey, focusKey, layout } = currentComponent;
901916

902-
const isVerticalDirection =
903-
direction === DIRECTION_DOWN || direction === DIRECTION_UP;
904-
const isIncrementalDirection =
905-
direction === DIRECTION_DOWN || direction === DIRECTION_RIGHT;
906-
907917
const currentCutoffCoordinate =
908918
SpatialNavigationService.getCutoffCoordinate(
909919
isVerticalDirection,
@@ -1031,6 +1041,30 @@ class SpatialNavigationService {
10311041
return this.focusKey;
10321042
}
10331043

1044+
/**
1045+
* Returns the focus key to which focus can be forced if there are force-focusable components.
1046+
* A component closest to the top left viewport corner (0,0) is returned.
1047+
*/
1048+
getForcedFocusKey(): string | undefined {
1049+
const forceFocusableComponents = filter(
1050+
this.focusableComponents,
1051+
(component) => component.focusable && component.forceFocus
1052+
);
1053+
1054+
/**
1055+
* Searching of the top level component that is closest to the top left viewport corner (0,0).
1056+
* To achieve meaningful and coherent results, 'down' direction is forced.
1057+
*/
1058+
const sortedForceFocusableComponents = this.sortSiblingsByPriority(
1059+
forceFocusableComponents,
1060+
{ x:0, y:0, width:0, height: 0, left: 0, top:0, node: null },
1061+
'down',
1062+
ROOT_FOCUS_KEY
1063+
);
1064+
1065+
return first(sortedForceFocusableComponents)?.focusKey;
1066+
}
1067+
10341068
/**
10351069
* This function tries to determine the next component to Focus
10361070
* It's either the target node OR the one down by the Tree if node has children components
@@ -1133,6 +1167,7 @@ class SpatialNavigationService {
11331167
onUpdateHasFocusedChild,
11341168
preferredChildFocusKey,
11351169
autoRestoreFocus,
1170+
forceFocus,
11361171
focusable,
11371172
isFocusBoundary,
11381173
focusBoundaryDirections
@@ -1155,6 +1190,7 @@ class SpatialNavigationService {
11551190
isFocusBoundary,
11561191
focusBoundaryDirections,
11571192
autoRestoreFocus,
1193+
forceFocus,
11581194
lastFocusedChildKey: null,
11591195
layout: {
11601196
x: 0,
@@ -1453,6 +1489,16 @@ class SpatialNavigationService {
14531489

14541490
this.log('setFocus', 'focusKey', focusKey);
14551491

1492+
/**
1493+
* When focusKey is not provided or is equal to `ROOT_FOCUS_KEY`, an attempt is made,
1494+
* to force focus one of the Focusable Containers, that have "forceFocus" flag enabled.
1495+
* A component closest to the top left viewport corner (0,0) is force-focused.
1496+
*/
1497+
if (!focusKey || focusKey === ROOT_FOCUS_KEY) {
1498+
// eslint-disable-next-line no-param-reassign
1499+
focusKey = this.getForcedFocusKey();
1500+
}
1501+
14561502
const newFocusKey = this.getNextFocusKey(focusKey);
14571503

14581504
this.log('setFocus', 'newFocusKey', newFocusKey);

src/useFocusable.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface UseFocusableConfig<P = object> {
4646
saveLastFocusedChild?: boolean;
4747
trackChildren?: boolean;
4848
autoRestoreFocus?: boolean;
49+
forceFocus?: boolean;
4950
isFocusBoundary?: boolean;
5051
focusBoundaryDirections?: string[];
5152
focusKey?: string;
@@ -77,6 +78,7 @@ const useFocusableHook = <P>({
7778
saveLastFocusedChild = true,
7879
trackChildren = false,
7980
autoRestoreFocus = true,
81+
forceFocus = false,
8082
isFocusBoundary = false,
8183
focusBoundaryDirections,
8284
focusKey: propFocusKey,
@@ -162,6 +164,7 @@ const useFocusableHook = <P>({
162164
isFocusBoundary,
163165
focusBoundaryDirections,
164166
autoRestoreFocus,
167+
forceFocus,
165168
focusable
166169
});
167170

0 commit comments

Comments
 (0)