diff --git a/README.md b/README.md index 2c72a78..bdfb8f8 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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. diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index b9f1f62..c8a64c5 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -77,6 +77,7 @@ interface FocusableComponent { isFocusBoundary: boolean; focusBoundaryDirections?: string[]; autoRestoreFocus: boolean; + forceFocus: boolean; lastFocusedChildKey?: string; layout?: FocusableComponentLayout; layoutUpdated?: boolean; @@ -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); @@ -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', @@ -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, @@ -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 @@ -1133,6 +1167,7 @@ class SpatialNavigationService { onUpdateHasFocusedChild, preferredChildFocusKey, autoRestoreFocus, + forceFocus, focusable, isFocusBoundary, focusBoundaryDirections @@ -1155,6 +1190,7 @@ class SpatialNavigationService { isFocusBoundary, focusBoundaryDirections, autoRestoreFocus, + forceFocus, lastFocusedChildKey: null, layout: { x: 0, @@ -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); diff --git a/src/useFocusable.ts b/src/useFocusable.ts index 4bf3943..0af1c28 100644 --- a/src/useFocusable.ts +++ b/src/useFocusable.ts @@ -46,6 +46,7 @@ export interface UseFocusableConfig

{ saveLastFocusedChild?: boolean; trackChildren?: boolean; autoRestoreFocus?: boolean; + forceFocus?: boolean; isFocusBoundary?: boolean; focusBoundaryDirections?: string[]; focusKey?: string; @@ -77,6 +78,7 @@ const useFocusableHook =

({ saveLastFocusedChild = true, trackChildren = false, autoRestoreFocus = true, + forceFocus = false, isFocusBoundary = false, focusBoundaryDirections, focusKey: propFocusKey, @@ -162,6 +164,7 @@ const useFocusableHook =

({ isFocusBoundary, focusBoundaryDirections, autoRestoreFocus, + forceFocus, focusable });