Skip to content

Commit

Permalink
Restore lost focus via directional navigation (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrian-wozniak authored Sep 11, 2023
1 parent b55179a commit 77b4f7d
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 6 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ By default, when the currently focused component is unmounted (deleted), navigat
on the nearest available sibling of that component. If this behavior is undesirable, you can disable it by setting this
flag to `false`.

##### `forceFocus` (default: false)
This flag makes the Focusable Container force-focusable. When there's more than one force-focusable component,
the closest to the top left viewport corner (0,0) is force-focused. Such containers can be force-focused when there's
no currently focused component (or `focusKey` points to not existing component) when navigating with arrows.
Also, when `focusKey` provided to `setFocus` is not defined or equal to `ROOT_FOCUS_KEY`.
In other words, if focus is lost, it can be restored to one of force-focusable components by navigating with arrows
or by focusing `ROOT_FOCUS_KEY`.

##### `isFocusBoundary` (default: false)
This flag makes the Focusable Container keep the focus inside its boundaries. It will only block the focus from leaving
the Container via directional navigation. You can still set the focus manually anywhere via `setFocus`.
Expand Down Expand Up @@ -391,7 +399,9 @@ Method to set the focus on the current component. I.e. to set the focus to the P
the Popup component when it is displayed.

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

##### `focused` (boolean)
Flag that indicates that the current component is focused.
Expand Down
56 changes: 51 additions & 5 deletions src/SpatialNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ interface FocusableComponent {
isFocusBoundary: boolean;
focusBoundaryDirections?: string[];
autoRestoreFocus: boolean;
forceFocus: boolean;
lastFocusedChildKey?: string;
layout?: FocusableComponentLayout;
layoutUpdated?: boolean;
Expand Down Expand Up @@ -873,6 +874,11 @@ class SpatialNavigationService {
return;
}

const isVerticalDirection =
direction === DIRECTION_DOWN || direction === DIRECTION_UP;
const isIncrementalDirection =
direction === DIRECTION_DOWN || direction === DIRECTION_RIGHT;

this.log('smartNavigate', 'direction', direction);
this.log('smartNavigate', 'fromParentFocusKey', fromParentFocusKey);
this.log('smartNavigate', 'this.focusKey', this.focusKey);
Expand All @@ -887,6 +893,15 @@ class SpatialNavigationService {
const currentComponent =
this.focusableComponents[fromParentFocusKey || this.focusKey];

/**
* When there's no currently focused component, an attempt is made, to force focus one of
* the Focusable Containers, that have "forceFocus" flag enabled.
*/
if (!fromParentFocusKey && !currentComponent) {
this.setFocus(this.getForcedFocusKey());
return;
}

this.log(
'smartNavigate',
'currentComponent',
Expand All @@ -899,11 +914,6 @@ class SpatialNavigationService {
this.updateLayout(currentComponent.focusKey);
const { parentFocusKey, focusKey, layout } = currentComponent;

const isVerticalDirection =
direction === DIRECTION_DOWN || direction === DIRECTION_UP;
const isIncrementalDirection =
direction === DIRECTION_DOWN || direction === DIRECTION_RIGHT;

const currentCutoffCoordinate =
SpatialNavigationService.getCutoffCoordinate(
isVerticalDirection,
Expand Down Expand Up @@ -1031,6 +1041,30 @@ class SpatialNavigationService {
return this.focusKey;
}

/**
* Returns the focus key to which focus can be forced if there are force-focusable components.
* A component closest to the top left viewport corner (0,0) is returned.
*/
getForcedFocusKey(): string | undefined {
const forceFocusableComponents = filter(
this.focusableComponents,
(component) => component.focusable && component.forceFocus
);

/**
* Searching of the top level component that is closest to the top left viewport corner (0,0).
* To achieve meaningful and coherent results, 'down' direction is forced.
*/
const sortedForceFocusableComponents = this.sortSiblingsByPriority(
forceFocusableComponents,
{ x:0, y:0, width:0, height: 0, left: 0, top:0, node: null },
'down',
ROOT_FOCUS_KEY
);

return first(sortedForceFocusableComponents)?.focusKey;
}

/**
* This function tries to determine the next component to Focus
* It's either the target node OR the one down by the Tree if node has children components
Expand Down Expand Up @@ -1133,6 +1167,7 @@ class SpatialNavigationService {
onUpdateHasFocusedChild,
preferredChildFocusKey,
autoRestoreFocus,
forceFocus,
focusable,
isFocusBoundary,
focusBoundaryDirections
Expand All @@ -1155,6 +1190,7 @@ class SpatialNavigationService {
isFocusBoundary,
focusBoundaryDirections,
autoRestoreFocus,
forceFocus,
lastFocusedChildKey: null,
layout: {
x: 0,
Expand Down Expand Up @@ -1453,6 +1489,16 @@ class SpatialNavigationService {

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

/**
* When focusKey is not provided or is equal to `ROOT_FOCUS_KEY`, an attempt is made,
* to force focus one of the Focusable Containers, that have "forceFocus" flag enabled.
* A component closest to the top left viewport corner (0,0) is force-focused.
*/
if (!focusKey || focusKey === ROOT_FOCUS_KEY) {
// eslint-disable-next-line no-param-reassign
focusKey = this.getForcedFocusKey();
}

const newFocusKey = this.getNextFocusKey(focusKey);

this.log('setFocus', 'newFocusKey', newFocusKey);
Expand Down
3 changes: 3 additions & 0 deletions src/useFocusable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface UseFocusableConfig<P = object> {
saveLastFocusedChild?: boolean;
trackChildren?: boolean;
autoRestoreFocus?: boolean;
forceFocus?: boolean;
isFocusBoundary?: boolean;
focusBoundaryDirections?: string[];
focusKey?: string;
Expand Down Expand Up @@ -77,6 +78,7 @@ const useFocusableHook = <P>({
saveLastFocusedChild = true,
trackChildren = false,
autoRestoreFocus = true,
forceFocus = false,
isFocusBoundary = false,
focusBoundaryDirections,
focusKey: propFocusKey,
Expand Down Expand Up @@ -162,6 +164,7 @@ const useFocusableHook = <P>({
isFocusBoundary,
focusBoundaryDirections,
autoRestoreFocus,
forceFocus,
focusable
});

Expand Down

0 comments on commit 77b4f7d

Please sign in to comment.