-
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
Navigator: remove location history, simplify internal logic #64675
Changes from all commits
f5476f7
30cebaf
819c52a
51c78f5
0466772
5a7448c
b3d460b
89c3f4d
77fb59f
7e159f8
29e0fb2
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 |
---|---|---|
|
@@ -37,110 +37,97 @@ type RouterAction = | |
| { type: 'gotoparent'; options?: NavigateToParentOptions }; | ||
|
||
type RouterState = { | ||
initialPath: string; | ||
screens: Screen[]; | ||
locationHistory: NavigatorLocation[]; | ||
currentLocation: NavigatorLocation; | ||
matchedPath: MatchedPath; | ||
focusSelectors: Map< string, string >; | ||
}; | ||
|
||
const MAX_HISTORY_LENGTH = 50; | ||
|
||
function addScreen( { screens }: RouterState, screen: Screen ) { | ||
if ( screens.some( ( s ) => s.path === screen.path ) ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
`Navigator: a screen with path ${ screen.path } already exists. | ||
The screen with id ${ screen.id } will not be added.` | ||
); | ||
return screens; | ||
} | ||
return [ ...screens, screen ]; | ||
} | ||
|
||
function removeScreen( { screens }: RouterState, screen: Screen ) { | ||
return screens.filter( ( s ) => s.id !== screen.id ); | ||
} | ||
|
||
function goBack( { locationHistory }: RouterState ) { | ||
if ( locationHistory.length <= 1 ) { | ||
return locationHistory; | ||
} | ||
return [ | ||
...locationHistory.slice( 0, -2 ), | ||
{ | ||
...locationHistory[ locationHistory.length - 2 ], | ||
isBack: true, | ||
hasRestoredFocus: false, | ||
}, | ||
]; | ||
} | ||
|
||
function goTo( | ||
state: RouterState, | ||
path: string, | ||
options: NavigateOptions = {} | ||
) { | ||
const { locationHistory } = state; | ||
const { currentLocation, focusSelectors } = state; | ||
|
||
const { | ||
focusTargetSelector, | ||
// Default assignments | ||
isBack = false, | ||
skipFocus = false, | ||
replace = false, | ||
// Extract to avoid forwarding | ||
replace, | ||
focusTargetSelector, | ||
// Rest | ||
...restOptions | ||
} = options; | ||
|
||
const isNavigatingToSamePath = | ||
locationHistory.length > 0 && | ||
locationHistory[ locationHistory.length - 1 ].path === path; | ||
if ( isNavigatingToSamePath ) { | ||
return locationHistory; | ||
} | ||
|
||
const isNavigatingToPreviousPath = | ||
isBack && | ||
locationHistory.length > 1 && | ||
locationHistory[ locationHistory.length - 2 ].path === path; | ||
|
||
if ( isNavigatingToPreviousPath ) { | ||
return goBack( state ); | ||
if ( currentLocation?.path === path ) { | ||
return { currentLocation, focusSelectors }; | ||
} | ||
|
||
const newLocation = { | ||
...restOptions, | ||
path, | ||
isBack, | ||
hasRestoredFocus: false, | ||
skipFocus, | ||
}; | ||
let focusSelectorsCopy; | ||
|
||
if ( locationHistory.length === 0 ) { | ||
return replace ? [] : [ newLocation ]; | ||
// Set a focus selector that will be used when navigating | ||
// back to the current location. | ||
if ( focusTargetSelector && currentLocation?.path ) { | ||
if ( ! focusSelectorsCopy ) { | ||
focusSelectorsCopy = new Map( state.focusSelectors ); | ||
} | ||
focusSelectorsCopy.set( currentLocation.path, focusTargetSelector ); | ||
} | ||
|
||
const newLocationHistory = locationHistory.slice( | ||
locationHistory.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). | ||
{ | ||
...locationHistory[ locationHistory.length - 1 ], | ||
focusTargetSelector, | ||
} | ||
); | ||
// Get the focus selector for the new location. | ||
let currentFocusSelector; | ||
if ( isBack ) { | ||
if ( ! focusSelectorsCopy ) { | ||
focusSelectorsCopy = new Map( state.focusSelectors ); | ||
} | ||
currentFocusSelector = focusSelectorsCopy.get( path ); | ||
focusSelectorsCopy.delete( path ); | ||
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. Here I would enhance the logic for the case when we navigate to the Also, a little optimization opportunity: create the copy only when you really found a selector to be deleted. Currently, if |
||
} | ||
|
||
newLocationHistory.push( newLocation ); | ||
|
||
return newLocationHistory; | ||
return { | ||
currentLocation: { | ||
...restOptions, | ||
path, | ||
isBack, | ||
hasRestoredFocus: false, | ||
focusTargetSelector: currentFocusSelector, | ||
skipFocus, | ||
}, | ||
ciampo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
focusSelectors: focusSelectorsCopy ?? focusSelectors, | ||
}; | ||
} | ||
|
||
function goToParent( | ||
state: RouterState, | ||
options: NavigateToParentOptions = {} | ||
) { | ||
const { locationHistory, screens } = state; | ||
const currentPath = locationHistory[ locationHistory.length - 1 ].path; | ||
const { currentLocation, screens, focusSelectors } = state; | ||
const currentPath = currentLocation?.path; | ||
if ( currentPath === undefined ) { | ||
return locationHistory; | ||
return { currentLocation, focusSelectors }; | ||
} | ||
const parentPath = findParent( currentPath, screens ); | ||
if ( parentPath === undefined ) { | ||
return locationHistory; | ||
return { currentLocation, focusSelectors }; | ||
} | ||
return goTo( state, parentPath, { | ||
...options, | ||
|
@@ -152,7 +139,13 @@ function routerReducer( | |
state: RouterState, | ||
action: RouterAction | ||
): RouterState { | ||
let { screens, locationHistory, matchedPath } = state; | ||
let { | ||
screens, | ||
currentLocation, | ||
matchedPath, | ||
focusSelectors, | ||
...restState | ||
} = state; | ||
switch ( action.type ) { | ||
case 'add': | ||
screens = addScreen( state, action.screen ); | ||
|
@@ -161,26 +154,31 @@ function routerReducer( | |
screens = removeScreen( state, action.screen ); | ||
break; | ||
case 'goto': | ||
locationHistory = goTo( state, action.path, action.options ); | ||
const goToNewState = goTo( state, action.path, action.options ); | ||
currentLocation = goToNewState.currentLocation; | ||
focusSelectors = goToNewState.focusSelectors; | ||
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. You can do a destructuring assignment in only you wrap it in parens so that it's not confused with a block: ( { currentLocation, focusSelection } = goTo() ); |
||
break; | ||
case 'gotoparent': | ||
locationHistory = goToParent( state, action.options ); | ||
const goToParentNewState = goToParent( state, action.options ); | ||
currentLocation = goToParentNewState.currentLocation; | ||
focusSelectors = goToParentNewState.focusSelectors; | ||
break; | ||
} | ||
|
||
if ( currentLocation?.path === state.initialPath ) { | ||
currentLocation = { ...currentLocation, isInitial: true }; | ||
} | ||
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. Do you really want to do this? 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.
No, the logic is wrong. I had already noticed it while working on #64777. I'm going to extract and refine the fix, and apply it in a separate PR |
||
|
||
// Return early in case there is no change | ||
if ( | ||
screens === state.screens && | ||
locationHistory === state.locationHistory | ||
currentLocation === state.currentLocation | ||
) { | ||
return state; | ||
} | ||
|
||
// Compute the matchedPath | ||
const currentPath = | ||
locationHistory.length > 0 | ||
? locationHistory[ locationHistory.length - 1 ].path | ||
: undefined; | ||
const currentPath = currentLocation?.path; | ||
matchedPath = | ||
currentPath !== undefined | ||
? patternMatch( currentPath, screens ) | ||
|
@@ -197,23 +195,35 @@ function routerReducer( | |
matchedPath = state.matchedPath; | ||
} | ||
|
||
return { screens, locationHistory, matchedPath }; | ||
return { | ||
...restState, | ||
screens, | ||
currentLocation, | ||
matchedPath, | ||
focusSelectors, | ||
}; | ||
} | ||
|
||
function UnconnectedNavigatorProvider( | ||
props: WordPressComponentProps< NavigatorProviderProps, 'div' >, | ||
forwardedRef: ForwardedRef< any > | ||
) { | ||
const { initialPath, children, className, ...otherProps } = | ||
useContextSystem( props, 'NavigatorProvider' ); | ||
const { | ||
initialPath: initialPathProp, | ||
children, | ||
className, | ||
...otherProps | ||
} = useContextSystem( props, 'NavigatorProvider' ); | ||
|
||
const [ routerState, dispatch ] = useReducer( | ||
routerReducer, | ||
initialPath, | ||
initialPathProp, | ||
( path ) => ( { | ||
screens: [], | ||
locationHistory: [ { path } ], | ||
currentLocation: { path }, | ||
matchedPath: undefined, | ||
focusSelectors: new Map(), | ||
initialPath: initialPathProp, | ||
} ) | ||
); | ||
|
||
|
@@ -242,19 +252,16 @@ function UnconnectedNavigatorProvider( | |
[] | ||
); | ||
|
||
const { locationHistory, matchedPath } = routerState; | ||
const { currentLocation, matchedPath } = routerState; | ||
|
||
const navigatorContextValue: NavigatorContextType = useMemo( | ||
() => ( { | ||
location: { | ||
...locationHistory[ locationHistory.length - 1 ], | ||
isInitial: locationHistory.length === 1, | ||
}, | ||
location: currentLocation, | ||
params: matchedPath?.params ?? {}, | ||
match: matchedPath?.id, | ||
...methods, | ||
} ), | ||
[ locationHistory, matchedPath, methods ] | ||
[ currentLocation, matchedPath, methods ] | ||
); | ||
|
||
const cx = useCx(); | ||
|
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.
Let's use
@wordpress/warning
for this, already used widely in the components package.