@@ -37,6 +37,12 @@ export type NodeType = NodeTypeOverrides extends { node: infer N }
3737
3838export type Direction = 'up' | 'down' | 'left' | 'right' ;
3939
40+ export type NextFocusResolver = (
41+ direction : Direction ,
42+ focusKey : string ,
43+ siblings : FocusableComponent [ ]
44+ ) => FocusableComponent | null ;
45+
4046type DistanceCalculationMethod = 'center' | 'edges' | 'corners' ;
4147
4248type 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
136144interface 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
0 commit comments