-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Try controlled navigator behavior #51915
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -32,6 +32,7 @@ import type { | |||||
Screen, | ||||||
} from '../types'; | ||||||
import { patternMatch, findParent } from '../utils/router'; | ||||||
import { useControlledValue } from '../../utils'; | ||||||
|
||||||
type MatchedPath = ReturnType< typeof patternMatch >; | ||||||
type ScreenAction = { type: string; screen: Screen }; | ||||||
|
@@ -56,16 +57,23 @@ function UnconnectedNavigatorProvider( | |||||
props: WordPressComponentProps< NavigatorProviderProps, 'div' >, | ||||||
forwardedRef: ForwardedRef< any > | ||||||
) { | ||||||
const { initialPath, children, className, ...otherProps } = | ||||||
useContextSystem( props, 'NavigatorProvider' ); | ||||||
const { | ||||||
initialPath, | ||||||
location: locationProp, | ||||||
onChange, | ||||||
children, | ||||||
className, | ||||||
...otherProps | ||||||
} = useContextSystem( props, 'NavigatorProvider' ); | ||||||
const [ location, updateLocation ] = useControlledValue( { | ||||||
onChange, | ||||||
value: locationProp, | ||||||
defaultValue: { path: initialPath }, | ||||||
} ); | ||||||
|
||||||
const [ locationHistory, setLocationHistory ] = useState< | ||||||
NavigatorLocation[] | ||||||
>( [ | ||||||
{ | ||||||
path: initialPath, | ||||||
}, | ||||||
] ); | ||||||
>( [ location ?? { path: initialPath } ] ); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that
Suggested change
|
||||||
const currentLocationHistory = useRef< NavigatorLocation[] >( [] ); | ||||||
const [ screens, dispatch ] = useReducer( screensReducer, [] ); | ||||||
const currentScreens = useRef< Screen[] >( [] ); | ||||||
|
@@ -75,6 +83,90 @@ function UnconnectedNavigatorProvider( | |||||
useEffect( () => { | ||||||
currentLocationHistory.current = locationHistory; | ||||||
}, [ locationHistory ] ); | ||||||
useEffect( () => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There seems to be a bug that causes the component to add the initial location twice to the location history when loading. This also causes the |
||||||
if ( ! location ) { | ||||||
return; | ||||||
} | ||||||
|
||||||
const { | ||||||
focusTargetSelector, | ||||||
isBack = false, | ||||||
skipFocus = false, | ||||||
replace = false, | ||||||
path: destinationPath, | ||||||
...restOptions | ||||||
} = location; | ||||||
|
||||||
const isNavigatingToPreviousPath = | ||||||
isBack && | ||||||
currentLocationHistory.current.length > 1 && | ||||||
currentLocationHistory.current[ | ||||||
currentLocationHistory.current.length - 2 | ||||||
].path === destinationPath; | ||||||
|
||||||
if ( isNavigatingToPreviousPath ) { | ||||||
// Navigating back to previous location | ||||||
setLocationHistory( ( prevLocationHistory ) => { | ||||||
if ( prevLocationHistory.length <= 1 ) { | ||||||
return prevLocationHistory; | ||||||
} | ||||||
return [ | ||||||
...prevLocationHistory.slice( 0, -2 ), | ||||||
{ | ||||||
...prevLocationHistory[ | ||||||
prevLocationHistory.length - 2 | ||||||
], | ||||||
isBack: true, | ||||||
hasRestoredFocus: false, | ||||||
}, | ||||||
]; | ||||||
} ); | ||||||
} else { | ||||||
// Navigating to a new location | ||||||
setLocationHistory( ( prevLocationHistory ) => { | ||||||
const newLocation = { | ||||||
...restOptions, | ||||||
path: destinationPath, | ||||||
isBack, | ||||||
hasRestoredFocus: false, | ||||||
skipFocus, | ||||||
}; | ||||||
|
||||||
if ( prevLocationHistory.length === 0 ) { | ||||||
return replace ? [] : [ newLocation ]; | ||||||
} | ||||||
|
||||||
// Form the new location history array. | ||||||
// Start by picking all previous history items, apart from the last one. | ||||||
// A check is in place to make sure that the array doesn't grow | ||||||
// beyond a max length. | ||||||
const newLocationHistory = prevLocationHistory.slice( | ||||||
prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 ? 1 : 0, | ||||||
-1 | ||||||
); | ||||||
|
||||||
// If we're not replacing history, add the last location history item (the | ||||||
// one what was just navigated from). We also assign it a | ||||||
// `focusTargetSelector` for enhanced focus restoration when navigating | ||||||
// back to it. | ||||||
if ( ! replace ) { | ||||||
newLocationHistory.push( { | ||||||
...prevLocationHistory[ | ||||||
prevLocationHistory.length - 1 | ||||||
], | ||||||
focusTargetSelector, | ||||||
} ); | ||||||
} | ||||||
|
||||||
// In any case, append the new location to the array (the one that | ||||||
// was just navigated to) | ||||||
newLocationHistory.push( newLocation ); | ||||||
|
||||||
return newLocationHistory; | ||||||
} ); | ||||||
} | ||||||
}, [ location ] ); | ||||||
|
||||||
const currentMatch = useRef< MatchedPath >(); | ||||||
const matchedPath = useMemo( () => { | ||||||
let currentPath: string | undefined; | ||||||
|
@@ -88,8 +180,8 @@ function UnconnectedNavigatorProvider( | |||||
return undefined; | ||||||
} | ||||||
|
||||||
const resolvePath = ( path: string ) => { | ||||||
const newMatch = patternMatch( path, screens ); | ||||||
const resolvePath = ( pathToResolve: string ) => { | ||||||
const newMatch = patternMatch( pathToResolve, screens ); | ||||||
|
||||||
// If the new match is the same as the current match, | ||||||
// return the previous one for performance reasons. | ||||||
|
@@ -124,80 +216,26 @@ function UnconnectedNavigatorProvider( | |||||
); | ||||||
|
||||||
const goBack: NavigatorContextType[ 'goBack' ] = useCallback( () => { | ||||||
setLocationHistory( ( prevLocationHistory ) => { | ||||||
if ( prevLocationHistory.length <= 1 ) { | ||||||
return prevLocationHistory; | ||||||
} | ||||||
return [ | ||||||
...prevLocationHistory.slice( 0, -2 ), | ||||||
{ | ||||||
...prevLocationHistory[ prevLocationHistory.length - 2 ], | ||||||
isBack: true, | ||||||
hasRestoredFocus: false, | ||||||
}, | ||||||
]; | ||||||
if ( currentLocationHistory.current.length < 2 ) { | ||||||
return; | ||||||
} | ||||||
|
||||||
updateLocation( { | ||||||
isBack: true, | ||||||
path: currentLocationHistory.current[ | ||||||
currentLocationHistory.current.length - 2 | ||||||
].path, | ||||||
} ); | ||||||
}, [] ); | ||||||
}, [ updateLocation ] ); | ||||||
|
||||||
const goTo: NavigatorContextType[ 'goTo' ] = useCallback( | ||||||
( path, options = {} ) => { | ||||||
const { | ||||||
focusTargetSelector, | ||||||
isBack = false, | ||||||
skipFocus = false, | ||||||
replace = false, | ||||||
...restOptions | ||||||
} = options; | ||||||
|
||||||
const isNavigatingToPreviousPath = | ||||||
isBack && | ||||||
currentLocationHistory.current.length > 1 && | ||||||
currentLocationHistory.current[ | ||||||
currentLocationHistory.current.length - 2 | ||||||
].path === path; | ||||||
|
||||||
if ( isNavigatingToPreviousPath ) { | ||||||
goBack(); | ||||||
return; | ||||||
} | ||||||
|
||||||
setLocationHistory( ( prevLocationHistory ) => { | ||||||
const newLocation = { | ||||||
...restOptions, | ||||||
path, | ||||||
isBack, | ||||||
hasRestoredFocus: false, | ||||||
skipFocus, | ||||||
}; | ||||||
|
||||||
if ( prevLocationHistory.length === 0 ) { | ||||||
return replace ? [] : [ newLocation ]; | ||||||
} | ||||||
|
||||||
const newLocationHistory = prevLocationHistory.slice( | ||||||
prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 ? 1 : 0, | ||||||
-1 | ||||||
); | ||||||
|
||||||
if ( ! replace ) { | ||||||
newLocationHistory.push( | ||||||
// Assign `focusTargetSelector` to the previous location in history | ||||||
// (the one we just navigated from). | ||||||
{ | ||||||
...prevLocationHistory[ | ||||||
prevLocationHistory.length - 1 | ||||||
], | ||||||
focusTargetSelector, | ||||||
} | ||||||
); | ||||||
} | ||||||
|
||||||
newLocationHistory.push( newLocation ); | ||||||
|
||||||
return newLocationHistory; | ||||||
( destinationPath, options = {} ) => { | ||||||
updateLocation( { | ||||||
...options, | ||||||
path: destinationPath, | ||||||
} ); | ||||||
}, | ||||||
[ goBack ] | ||||||
[ updateLocation ] | ||||||
); | ||||||
|
||||||
const goToParent: NavigatorContextType[ 'goToParent' ] = useCallback( | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,11 +44,24 @@ export type NavigatorProviderProps = { | |
/** | ||
* The initial active path. | ||
*/ | ||
initialPath: string; | ||
initialPath?: string; | ||
|
||
/** | ||
* The children elements. | ||
*/ | ||
children: ReactNode; | ||
|
||
/** | ||
* The current path. When provided the navigator will be controlled. | ||
*/ | ||
location?: NavigatorLocation; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably change |
||
|
||
/** | ||
* Navigates to a new path. | ||
* | ||
* @param path The path to navigate to. | ||
*/ | ||
onChange?: ( location: NavigatorLocation ) => void; | ||
}; | ||
|
||
export type NavigatorScreenProps = { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to keep both
location
andlocationHistory
internal states? I'm afraid it would cause extra re-renders and potentially some race conditions too. Maybe we can just keep the location history as a ref, and updatenavigatorContextValue
accordingly? (I know that this would probably also require refactoring the logic around the newuseEffect
)