Skip to content

Commit 8fb1ac6

Browse files
xavi160asgvard
andauthored
Add nextFocusResolver to override default behavior (#231)
* feat: add navigationStrategy to FocusableComponent and update SpatialNavigationService for custom navigation handling * refactor: rename navigationStrategy to nextFocusResolver * Create hot-falcons-begin.md * fix: add warning for invalid component in nextFocusResolver to prevent lost focus * Update packages/core/src/SpatialNavigation.ts Co-authored-by: Dmitriy Bryokhin <2877657+asgvard@users.noreply.github.com> --------- Co-authored-by: Dmitriy Bryokhin <2877657+asgvard@users.noreply.github.com>
1 parent e123be0 commit 8fb1ac6

3 files changed

Lines changed: 116 additions & 60 deletions

File tree

.changeset/hot-falcons-begin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@noriginmedia/norigin-spatial-navigation-core": patch
3+
"@noriginmedia/norigin-spatial-navigation-react": patch
4+
---
5+
6+
- Add `nextFocusResolver` to override default behavior

packages/core/src/SpatialNavigation.ts

Lines changed: 103 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export type NodeType = NodeTypeOverrides extends { node: infer N }
3737

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

40+
export type NextFocusResolver = (
41+
direction: Direction,
42+
focusKey: string,
43+
siblings: FocusableComponent[]
44+
) => FocusableComponent | null;
45+
4046
type DistanceCalculationMethod = 'center' | 'edges' | 'corners';
4147

4248
type DistanceCalculationFunction = (
@@ -112,6 +118,7 @@ export interface FocusableComponent {
112118
focusBoundaryDirections?: Direction[];
113119
autoRestoreFocus: boolean;
114120
forceFocus: boolean;
121+
nextFocusResolver?: NextFocusResolver;
115122
lastFocusedChildKey?: string;
116123
layout?: FocusableComponentLayout;
117124
layoutUpdatedAt?: number;
@@ -131,6 +138,7 @@ interface FocusableComponentUpdatePayload {
131138
onFocus: (layout: FocusableComponentLayout, details: FocusDetails) => void;
132139
onBlur: (layout: FocusableComponentLayout, details: FocusDetails) => void;
133140
accessibilityLabel?: string;
141+
nextFocusResolver?: NextFocusResolver;
134142
}
135143

136144
interface FocusableComponentRemovePayload {
@@ -1133,74 +1141,106 @@ export class SpatialNavigationService {
11331141
.map((component) => this.updateLayout(component.focusKey))
11341142
);
11351143

1136-
const siblings = filter(this.focusableComponents, (component) => {
1137-
if (
1138-
component.parentFocusKey === parentFocusKey &&
1139-
component.focusKey !== currentComponent.focusKey &&
1140-
component.focusable &&
1141-
component.layout
1142-
) {
1143-
const siblingCutoffCoordinate =
1144-
SpatialNavigationService.getCutoffCoordinate(
1145-
isVerticalDirection,
1146-
isIncrementalDirection,
1147-
true,
1148-
component.layout,
1149-
this.writingDirection
1144+
let nextComponent: FocusableComponent | null = null;
1145+
1146+
const { nextFocusResolver } =
1147+
this.focusableComponents[parentFocusKey] ?? {};
1148+
1149+
if (nextFocusResolver) {
1150+
const siblings = filter(
1151+
this.focusableComponents,
1152+
(component) =>
1153+
component.parentFocusKey === parentFocusKey && component.focusable
1154+
);
1155+
nextComponent = nextFocusResolver(
1156+
direction as Direction,
1157+
focusKey,
1158+
siblings
1159+
);
1160+
1161+
if (this.debug) {
1162+
if (nextComponent != null && !siblings.includes(nextComponent)) {
1163+
console.warn(
1164+
`nextFocusResolver returned an invalid component. This will result in lost focus. Check the "nextFocusResolver" implementation in component with focusKey: ${parentFocusKey}`
11501165
);
1166+
}
11511167

1152-
return isVerticalDirection
1153-
? isIncrementalDirection
1154-
? siblingCutoffCoordinate >= currentCutoffCoordinate // vertical next
1155-
: siblingCutoffCoordinate <= currentCutoffCoordinate // vertical previous
1156-
: this.writingDirection === WritingDirection.LTR
1157-
? isIncrementalDirection
1158-
? siblingCutoffCoordinate >= currentCutoffCoordinate // horizontal LTR next
1159-
: siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal LTR previous
1160-
: isIncrementalDirection
1161-
? siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal RTL next
1162-
: siblingCutoffCoordinate >= currentCutoffCoordinate; // horizontal RTL previous
1168+
this.log(
1169+
'smartNavigate',
1170+
'navigation overridden by nextFocusResolver',
1171+
nextComponent
1172+
);
11631173
}
1174+
} else {
1175+
const siblings = filter(this.focusableComponents, (component) => {
1176+
if (
1177+
component.parentFocusKey === parentFocusKey &&
1178+
component.focusKey !== currentComponent.focusKey &&
1179+
component.focusable &&
1180+
component.layout
1181+
) {
1182+
const siblingCutoffCoordinate =
1183+
SpatialNavigationService.getCutoffCoordinate(
1184+
isVerticalDirection,
1185+
isIncrementalDirection,
1186+
true,
1187+
component.layout,
1188+
this.writingDirection
1189+
);
11641190

1165-
return false;
1166-
});
1191+
return isVerticalDirection
1192+
? isIncrementalDirection
1193+
? siblingCutoffCoordinate >= currentCutoffCoordinate // vertical next
1194+
: siblingCutoffCoordinate <= currentCutoffCoordinate // vertical previous
1195+
: this.writingDirection === WritingDirection.LTR
1196+
? isIncrementalDirection
1197+
? siblingCutoffCoordinate >= currentCutoffCoordinate // horizontal LTR next
1198+
: siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal LTR previous
1199+
: isIncrementalDirection
1200+
? siblingCutoffCoordinate <= currentCutoffCoordinate // horizontal RTL next
1201+
: siblingCutoffCoordinate >= currentCutoffCoordinate; // horizontal RTL previous
1202+
}
1203+
1204+
return false;
1205+
});
11671206

1168-
if (this.debug) {
1169-
this.log(
1170-
'smartNavigate',
1171-
'currentCutoffCoordinate',
1172-
currentCutoffCoordinate
1173-
);
1174-
this.log(
1175-
'smartNavigate',
1176-
'siblings',
1177-
`${siblings.length} elements:`,
1178-
siblings.map((sibling) => sibling.focusKey).join(', '),
1179-
siblings.map((sibling) => sibling.node),
1180-
siblings.map((sibling) => sibling)
1181-
);
1182-
}
1207+
if (this.debug) {
1208+
this.log(
1209+
'smartNavigate',
1210+
'currentCutoffCoordinate',
1211+
currentCutoffCoordinate
1212+
);
1213+
this.log(
1214+
'smartNavigate',
1215+
'siblings',
1216+
`${siblings.length} elements:`,
1217+
siblings.map((sibling) => sibling.focusKey).join(', '),
1218+
siblings.map((sibling) => sibling.node),
1219+
siblings.map((sibling) => sibling)
1220+
);
1221+
}
11831222

1184-
if (this.visualDebugger) {
1185-
const refCorners = SpatialNavigationService.getRefCorners(
1223+
if (this.visualDebugger) {
1224+
const refCorners = SpatialNavigationService.getRefCorners(
1225+
direction,
1226+
false,
1227+
layout
1228+
);
1229+
1230+
this.visualDebugger.drawPoint(refCorners.a.x, refCorners.a.y);
1231+
this.visualDebugger.drawPoint(refCorners.b.x, refCorners.b.y);
1232+
}
1233+
1234+
const sortedSiblings = this.sortSiblingsByPriority(
1235+
siblings,
1236+
layout,
11861237
direction,
1187-
false,
1188-
layout
1238+
focusKey
11891239
);
11901240

1191-
this.visualDebugger.drawPoint(refCorners.a.x, refCorners.a.y);
1192-
this.visualDebugger.drawPoint(refCorners.b.x, refCorners.b.y);
1241+
nextComponent = first(sortedSiblings);
11931242
}
11941243

1195-
const sortedSiblings = this.sortSiblingsByPriority(
1196-
siblings,
1197-
layout,
1198-
direction,
1199-
focusKey
1200-
);
1201-
1202-
const nextComponent = first(sortedSiblings);
1203-
12041244
this.log(
12051245
'smartNavigate',
12061246
'nextComponent',
@@ -1405,7 +1445,8 @@ export class SpatialNavigationService {
14051445
focusable,
14061446
isFocusBoundary,
14071447
focusBoundaryDirections,
1408-
accessibilityLabel
1448+
accessibilityLabel,
1449+
nextFocusResolver
14091450
}: FocusableComponent) {
14101451
this.focusableComponents[focusKey] = {
14111452
focusKey,
@@ -1419,6 +1460,7 @@ export class SpatialNavigationService {
14191460
onBlur,
14201461
onUpdateFocus,
14211462
onUpdateHasFocusedChild,
1463+
nextFocusResolver,
14221464
saveLastFocusedChild,
14231465
trackChildren,
14241466
preferredChildFocusKey,
@@ -1857,7 +1899,8 @@ export class SpatialNavigationService {
18571899
onArrowPress,
18581900
onFocus,
18591901
onBlur,
1860-
accessibilityLabel
1902+
accessibilityLabel,
1903+
nextFocusResolver
18611904
}: FocusableComponentUpdatePayload
18621905
) {
18631906
const component = this.focusableComponents[focusKey];
@@ -1873,6 +1916,7 @@ export class SpatialNavigationService {
18731916
component.onFocus = onFocus;
18741917
component.onBlur = onBlur;
18751918
component.accessibilityLabel = accessibilityLabel;
1919+
component.nextFocusResolver = nextFocusResolver;
18761920
// Reset layout updated at to force a layout update
18771921
component.layoutUpdatedAt = 0;
18781922

packages/react/src/useFocusable.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
FocusableComponentLayout,
1313
FocusDetails,
1414
KeyPressDetails,
15-
Direction
15+
Direction,
16+
NextFocusResolver
1617
} from '@noriginmedia/norigin-spatial-navigation-core';
1718
import { useFocusContext } from './useFocusContext';
1819

@@ -56,6 +57,7 @@ export interface UseFocusableConfig<P = object> {
5657
focusBoundaryDirections?: Direction[];
5758
focusKey?: string;
5859
preferredChildFocusKey?: string;
60+
nextFocusResolver?: NextFocusResolver;
5961
onEnterPress?: EnterPressHandler<P>;
6062
onEnterRelease?: EnterReleaseHandler<P>;
6163
onArrowPress?: ArrowPressHandler<P>;
@@ -90,6 +92,7 @@ const useFocusableHook = <P, E = any>({
9092
focusBoundaryDirections,
9193
focusKey: propFocusKey,
9294
preferredChildFocusKey,
95+
nextFocusResolver,
9396
onEnterPress = noop,
9497
onEnterRelease = noop,
9598
onArrowPress = () => true,
@@ -167,6 +170,7 @@ const useFocusableHook = <P, E = any>({
167170
node,
168171
parentFocusKey,
169172
preferredChildFocusKey,
173+
nextFocusResolver,
170174
onEnterPress: onEnterPressHandler,
171175
onEnterRelease: onEnterReleaseHandler,
172176
onArrowPress: onArrowPressHandler,
@@ -202,6 +206,7 @@ const useFocusableHook = <P, E = any>({
202206
focusable,
203207
isFocusBoundary,
204208
focusBoundaryDirections,
209+
nextFocusResolver,
205210
onEnterPress: onEnterPressHandler,
206211
onEnterRelease: onEnterReleaseHandler,
207212
onArrowPress: onArrowPressHandler,
@@ -216,6 +221,7 @@ const useFocusableHook = <P, E = any>({
216221
focusable,
217222
isFocusBoundary,
218223
focusBoundaryDirections,
224+
nextFocusResolver,
219225
onEnterPressHandler,
220226
onEnterReleaseHandler,
221227
onArrowPressHandler,

0 commit comments

Comments
 (0)