Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hot-falcons-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@noriginmedia/norigin-spatial-navigation-core": patch
"@noriginmedia/norigin-spatial-navigation-react": patch
---

- Add `nextFocusResolver` to override default behavior
162 changes: 103 additions & 59 deletions packages/core/src/SpatialNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@

export type Direction = 'up' | 'down' | 'left' | 'right';

export type NextFocusResolver = (
direction: Direction,
focusKey: string,
siblings: FocusableComponent[]
) => FocusableComponent | null;

type DistanceCalculationMethod = 'center' | 'edges' | 'corners';

type DistanceCalculationFunction = (
Expand Down Expand Up @@ -112,6 +118,7 @@
focusBoundaryDirections?: Direction[];
autoRestoreFocus: boolean;
forceFocus: boolean;
nextFocusResolver?: NextFocusResolver;
lastFocusedChildKey?: string;
layout?: FocusableComponentLayout;
layoutUpdatedAt?: number;
Expand All @@ -131,6 +138,7 @@
onFocus: (layout: FocusableComponentLayout, details: FocusDetails) => void;
onBlur: (layout: FocusableComponentLayout, details: FocusDetails) => void;
accessibilityLabel?: string;
nextFocusResolver?: NextFocusResolver;
}

interface FocusableComponentRemovePayload {
Expand Down Expand Up @@ -760,7 +768,7 @@
this.layoutAdapter = new LayoutAdapterClass(this);
} else {
if (useGetBoundingClientRect) {
console.warn(

Check warning on line 771 in packages/core/src/SpatialNavigation.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected console statement
'useGetBoundingClientRect is deprecated. Please use layoutAdapter API instead.'
);
this.layoutAdapter = new GetBoundingClientRectAdapter(this);
Expand Down Expand Up @@ -1133,74 +1141,106 @@
.map((component) => this.updateLayout(component.focusKey))
);

const siblings = filter(this.focusableComponents, (component) => {
if (
component.parentFocusKey === parentFocusKey &&
component.focusKey !== currentComponent.focusKey &&
component.focusable &&
component.layout
) {
const siblingCutoffCoordinate =
SpatialNavigationService.getCutoffCoordinate(
isVerticalDirection,
isIncrementalDirection,
true,
component.layout,
this.writingDirection
let nextComponent: FocusableComponent | null = null;

const { nextFocusResolver } =
this.focusableComponents[parentFocusKey] ?? {};

if (nextFocusResolver) {
const siblings = filter(
this.focusableComponents,
(component) =>
component.parentFocusKey === parentFocusKey && component.focusable
);
nextComponent = nextFocusResolver(
direction as Direction,
focusKey,
siblings
);

if (this.debug) {
if (nextComponent != null && !siblings.includes(nextComponent)) {
console.warn(

Check warning on line 1163 in packages/core/src/SpatialNavigation.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected console statement
`nextFocusResolver returned an invalid component. This will result in lost focus. Check the "nextFocusResolver" implementation in component with focusKey: ${parentFocusKey}`
);
}

return isVerticalDirection
? isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate // vertical next
: siblingCutoffCoordinate <= currentCutoffCoordinate // vertical previous
: this.writingDirection === WritingDirection.LTR
? isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate // horizontal LTR next
: siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal LTR previous
: isIncrementalDirection
? siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal RTL next
: siblingCutoffCoordinate >= currentCutoffCoordinate; // horizontal RTL previous
this.log(
'smartNavigate',
'navigation overrided by nextFocusResolver',
Comment thread
xavi160 marked this conversation as resolved.
Outdated
nextComponent
);
}
} else {
const siblings = filter(this.focusableComponents, (component) => {
if (
component.parentFocusKey === parentFocusKey &&
component.focusKey !== currentComponent.focusKey &&
component.focusable &&
component.layout
) {
const siblingCutoffCoordinate =
SpatialNavigationService.getCutoffCoordinate(
isVerticalDirection,
isIncrementalDirection,
true,
component.layout,
this.writingDirection
);

return false;
});
return isVerticalDirection
? isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate // vertical next
: siblingCutoffCoordinate <= currentCutoffCoordinate // vertical previous
: this.writingDirection === WritingDirection.LTR
? isIncrementalDirection
? siblingCutoffCoordinate >= currentCutoffCoordinate // horizontal LTR next
: siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal LTR previous
: isIncrementalDirection
? siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal RTL next
: siblingCutoffCoordinate >= currentCutoffCoordinate; // horizontal RTL previous
}

return false;
});

if (this.debug) {
this.log(
'smartNavigate',
'currentCutoffCoordinate',
currentCutoffCoordinate
);
this.log(
'smartNavigate',
'siblings',
`${siblings.length} elements:`,
siblings.map((sibling) => sibling.focusKey).join(', '),
siblings.map((sibling) => sibling.node),
siblings.map((sibling) => sibling)
);
}
if (this.debug) {
this.log(
'smartNavigate',
'currentCutoffCoordinate',
currentCutoffCoordinate
);
this.log(
'smartNavigate',
'siblings',
`${siblings.length} elements:`,
siblings.map((sibling) => sibling.focusKey).join(', '),
siblings.map((sibling) => sibling.node),
siblings.map((sibling) => sibling)
);
}

if (this.visualDebugger) {
const refCorners = SpatialNavigationService.getRefCorners(
if (this.visualDebugger) {
const refCorners = SpatialNavigationService.getRefCorners(
direction,
false,
layout
);

this.visualDebugger.drawPoint(refCorners.a.x, refCorners.a.y);
this.visualDebugger.drawPoint(refCorners.b.x, refCorners.b.y);
}

const sortedSiblings = this.sortSiblingsByPriority(
siblings,
layout,
direction,
false,
layout
focusKey
);

this.visualDebugger.drawPoint(refCorners.a.x, refCorners.a.y);
this.visualDebugger.drawPoint(refCorners.b.x, refCorners.b.y);
nextComponent = first(sortedSiblings);
}

const sortedSiblings = this.sortSiblingsByPriority(
siblings,
layout,
direction,
focusKey
);

const nextComponent = first(sortedSiblings);

this.log(
'smartNavigate',
'nextComponent',
Expand Down Expand Up @@ -1405,7 +1445,8 @@
focusable,
isFocusBoundary,
focusBoundaryDirections,
accessibilityLabel
accessibilityLabel,
nextFocusResolver
}: FocusableComponent) {
this.focusableComponents[focusKey] = {
focusKey,
Expand All @@ -1419,6 +1460,7 @@
onBlur,
onUpdateFocus,
onUpdateHasFocusedChild,
nextFocusResolver,
saveLastFocusedChild,
trackChildren,
preferredChildFocusKey,
Expand Down Expand Up @@ -1857,7 +1899,8 @@
onArrowPress,
onFocus,
onBlur,
accessibilityLabel
accessibilityLabel,
nextFocusResolver
}: FocusableComponentUpdatePayload
) {
const component = this.focusableComponents[focusKey];
Expand All @@ -1873,6 +1916,7 @@
component.onFocus = onFocus;
component.onBlur = onBlur;
component.accessibilityLabel = accessibilityLabel;
component.nextFocusResolver = nextFocusResolver;
// Reset layout updated at to force a layout update
component.layoutUpdatedAt = 0;

Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/useFocusable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
FocusableComponentLayout,
FocusDetails,
KeyPressDetails,
Direction
Direction,
NextFocusResolver
} from '@noriginmedia/norigin-spatial-navigation-core';
import { useFocusContext } from './useFocusContext';

Expand Down Expand Up @@ -56,6 +57,7 @@ export interface UseFocusableConfig<P = object> {
focusBoundaryDirections?: Direction[];
focusKey?: string;
preferredChildFocusKey?: string;
nextFocusResolver?: NextFocusResolver;
onEnterPress?: EnterPressHandler<P>;
onEnterRelease?: EnterReleaseHandler<P>;
onArrowPress?: ArrowPressHandler<P>;
Expand Down Expand Up @@ -90,6 +92,7 @@ const useFocusableHook = <P, E = any>({
focusBoundaryDirections,
focusKey: propFocusKey,
preferredChildFocusKey,
nextFocusResolver,
onEnterPress = noop,
onEnterRelease = noop,
onArrowPress = () => true,
Expand Down Expand Up @@ -167,6 +170,7 @@ const useFocusableHook = <P, E = any>({
node,
parentFocusKey,
preferredChildFocusKey,
nextFocusResolver,
onEnterPress: onEnterPressHandler,
onEnterRelease: onEnterReleaseHandler,
onArrowPress: onArrowPressHandler,
Expand Down Expand Up @@ -202,6 +206,7 @@ const useFocusableHook = <P, E = any>({
focusable,
isFocusBoundary,
focusBoundaryDirections,
nextFocusResolver,
onEnterPress: onEnterPressHandler,
onEnterRelease: onEnterReleaseHandler,
onArrowPress: onArrowPressHandler,
Expand All @@ -216,6 +221,7 @@ const useFocusableHook = <P, E = any>({
focusable,
isFocusBoundary,
focusBoundaryDirections,
nextFocusResolver,
onEnterPressHandler,
onEnterReleaseHandler,
onArrowPressHandler,
Expand Down
Loading