Skip to content

Commit

Permalink
Add parent navigation support for the navigator component
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Feb 9, 2023
1 parent 300a608 commit f5ac3ad
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 12 deletions.
1 change: 1 addition & 0 deletions packages/components/src/navigator/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const initialContextValue: NavigatorContextType = {
location: {},
goTo: () => {},
goBack: () => {},
goToParent: () => {},
addScreen: () => {},
removeScreen: () => {},
params: {},
Expand Down
11 changes: 8 additions & 3 deletions packages/components/src/navigator/navigator-back-button/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ export function useNavigatorBackButton(
const {
onClick,
as = Button,
goToParent: goToParentProp = false,
...otherProps
} = useContextSystem( props, 'NavigatorBackButton' );

const { goBack } = useNavigator();
const { goBack, goToParent } = useNavigator();
const handleClick: React.MouseEventHandler< HTMLButtonElement > =
useCallback(
( e ) => {
e.preventDefault();
goBack();
if ( goToParentProp ) {
goToParent();
} else {
goBack();
}
onClick?.( e );
},
[ goBack, onClick ]
[ goToParentProp, goToParent, goBack, onClick ]
);

return {
Expand Down
58 changes: 54 additions & 4 deletions packages/components/src/navigator/navigator-provider/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useCallback,
useReducer,
useRef,
useEffect,
} from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';

Expand All @@ -33,7 +34,7 @@ import type {
NavigatorContext as NavigatorContextType,
Screen,
} from '../types';
import { patternMatch } from '../utils/router';
import { patternMatch, findParent } from '../utils/router';

type MatchedPath = ReturnType< typeof patternMatch >;
type ScreenAction = { type: string; screen: Screen };
Expand Down Expand Up @@ -66,7 +67,15 @@ function UnconnectedNavigatorProvider(
path: initialPath,
},
] );
const currentLocationHistory = useRef< NavigatorLocation[] >( [] );
const [ screens, dispatch ] = useReducer( screensReducer, [] );
const currentScreens = useRef< Screen[] >( [] );
useEffect( () => {
currentScreens.current = screens;
}, [ screens ] );
useEffect( () => {
currentLocationHistory.current = locationHistory;
}, [ locationHistory ] );
const currentMatch = useRef< MatchedPath >();
const matchedPath = useMemo( () => {
let currentPath: string | undefined;
Expand Down Expand Up @@ -118,12 +127,16 @@ function UnconnectedNavigatorProvider(
const goTo: NavigatorContextType[ 'goTo' ] = useCallback(
( path, options = {} ) => {
setLocationHistory( ( prevLocationHistory ) => {
const { focusTargetSelector, ...restOptions } = options;
const {
focusTargetSelector,
isBack = false,
...restOptions
} = options;

const newLocation = {
...restOptions,
path,
isBack: false,
isBack,
hasRestoredFocus: false,
};

Expand Down Expand Up @@ -164,6 +177,34 @@ function UnconnectedNavigatorProvider(
} );
}, [] );

const goToParent: NavigatorContextType[ 'goToParent' ] =
useCallback( () => {
const currentPath =
currentLocationHistory.current[
currentLocationHistory.current.length - 1
].path;
if ( currentPath === undefined ) {
return;
}
const parentPath = findParent(
currentPath,
currentScreens.current
);
if ( parentPath === undefined ) {
return;
}
const isBack =
currentLocationHistory.current.length > 1 &&
currentLocationHistory.current[
currentLocationHistory.current.length - 2
].path === parentPath;
if ( isBack ) {
goBack();
} else {
goTo( parentPath, { isBack: true } );
}
}, [ goBack, goTo ] );

const navigatorContextValue: NavigatorContextType = useMemo(
() => ( {
location: {
Expand All @@ -174,10 +215,19 @@ function UnconnectedNavigatorProvider(
match: matchedPath ? matchedPath.id : undefined,
goTo,
goBack,
goToParent,
addScreen,
removeScreen,
} ),
[ locationHistory, matchedPath, goTo, goBack, addScreen, removeScreen ]
[
locationHistory,
matchedPath,
goTo,
goBack,
goToParent,
addScreen,
removeScreen,
]
);

const cx = useCx();
Expand Down
61 changes: 61 additions & 0 deletions packages/components/src/navigator/stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,64 @@ function ProductDetails() {
</Card>
);
}

const NestedNavigatorTemplate: ComponentStory< typeof NavigatorProvider > = ( {
style,
} ) => (
<NavigatorProvider
style={ { ...style, height: '100vh', maxHeight: '450px' } }
initialPath="/"
>
<NavigatorScreen path="/">
<Card>
<CardBody>
<NavigatorButton variant="secondary" path="/child1">
Go to first child.
</NavigatorButton>
<NavigatorButton variant="secondary" path="/child2">
Go to second child.
</NavigatorButton>
</CardBody>
</Card>
</NavigatorScreen>
<NavigatorScreen path="/child1">
<Card>
<CardBody>
This is the first child
<NavigatorBackButton variant="secondary" goToParent>
Go back to parent
</NavigatorBackButton>
</CardBody>
</Card>
</NavigatorScreen>
<NavigatorScreen path="/child2">
<Card>
<CardBody>
This is the second child
<NavigatorBackButton variant="secondary" goToParent>
Go back to parent
</NavigatorBackButton>
<NavigatorButton
variant="secondary"
path="/child2/grandchild"
>
Go to grand child.
</NavigatorButton>
</CardBody>
</Card>
</NavigatorScreen>
<NavigatorScreen path="/child2/grandchild">
<Card>
<CardBody>
This is the grand child
<NavigatorBackButton variant="secondary" goToParent>
Go back to parent
</NavigatorBackButton>
</CardBody>
</Card>
</NavigatorScreen>
</NavigatorProvider>
);

export const NestedNavigator: ComponentStory< typeof NavigatorProvider > =
NestedNavigatorTemplate.bind( {} );
50 changes: 49 additions & 1 deletion packages/components/src/navigator/test/router.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { patternMatch } from '../utils/router';
import { patternMatch, findParent } from '../utils/router';

describe( 'patternMatch', () => {
it( 'should return undefined if not pattern is matched', () => {
Expand Down Expand Up @@ -48,3 +48,51 @@ describe( 'patternMatch', () => {
} );
} );
} );

describe( 'findParent', () => {
it( 'should return undefined if no parent is found', () => {
const result = findParent( '/test', [
{ id: 'route', path: '/test' },
] );
expect( result ).toBeUndefined();
} );

it( 'should return the parent path', () => {
const result = findParent( '/test', [
{ id: 'route1', path: '/test' },
{ id: 'route2', path: '/' },
] );
expect( result ).toEqual( '/' );
} );

it( 'should return to another parent path', () => {
const result = findParent( '/test/123', [
{ id: 'route1', path: '/test/:id' },
{ id: 'route2', path: '/test' },
] );
expect( result ).toEqual( '/test' );
} );

it( 'should return the parent path with params', () => {
const result = findParent( '/test/123/456', [
{ id: 'route1', path: '/test/:id/:subId' },
{ id: 'route2', path: '/test/:id' },
] );
expect( result ).toEqual( '/test/123' );
} );

it( 'should return the parent path with optional params', () => {
const result = findParent( '/test/123', [
{ id: 'route', path: '/test/:id?' },
] );
expect( result ).toEqual( '/test' );
} );

it( 'should return the grand parent if no parent found', () => {
const result = findParent( '/test/123/456', [
{ id: 'route1', path: '/test/:id/:subId' },
{ id: 'route2', path: '/test' },
] );
expect( result ).toEqual( '/test' );
} );
} );
12 changes: 10 additions & 2 deletions packages/components/src/navigator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export type MatchParams = Record< string, string | string[] >;

type NavigateOptions = {
focusTargetSelector?: string;
isBack?: boolean;
};

export type NavigatorLocation = NavigateOptions & {
isInitial?: boolean;
isBack?: boolean;
path?: string;
hasRestoredFocus?: boolean;
};
Expand All @@ -27,6 +27,7 @@ export type Navigator = {
params: MatchParams;
goTo: ( path: string, options?: NavigateOptions ) => void;
goBack: () => void;
goToParent: () => void;
};

export type NavigatorContext = Navigator & {
Expand Down Expand Up @@ -57,7 +58,14 @@ export type NavigatorScreenProps = {
children: ReactNode;
};

export type NavigatorBackButtonProps = ButtonAsButtonProps;
export type NavigatorBackButtonProps = ButtonAsButtonProps & {
/**
* Whether we should navigate to the parent screen.
*
* @default 'false'
*/
goToParent?: boolean;
};

export type NavigatorButtonProps = NavigatorBackButtonProps & {
/**
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/navigator/use-navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import type { Navigator } from './types';
* Retrieves a `navigator` instance.
*/
function useNavigator(): Navigator {
const { location, params, goTo, goBack } = useContext( NavigatorContext );
const { location, params, goTo, goBack, goToParent } =
useContext( NavigatorContext );

return {
location,
goTo,
goBack,
goToParent,
params,
};
}
Expand Down
25 changes: 25 additions & 0 deletions packages/components/src/navigator/utils/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,28 @@ export function patternMatch( path: string, screens: Screen[] ) {

return undefined;
}

export function findParent( path: string, screens: Screen[] ) {
if ( path[ 0 ] !== '/' ) {
return undefined;
}
const pathParts = path.split( '/' );
let parentPath;
while ( pathParts.length > 1 && ! parentPath ) {
pathParts.pop();
const potentialParentPath =
pathParts.join( '/' ) === '' ? '/' : pathParts.join( '/' );
if (
screens.find( ( screen ) => {
const matchingFunction = match( screen.path, {
decode: decodeURIComponent,
} );
return matchingFunction( potentialParentPath ) !== false;
} )
) {
parentPath = potentialParentPath;
}
}

return parentPath;
}
1 change: 1 addition & 0 deletions packages/edit-site/src/components/global-styles/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function ScreenHeader( { title, description } ) {
icon={ isRTL() ? chevronRight : chevronLeft }
isSmall
aria-label={ __( 'Navigate to the previous view' ) }
goToParent
/>
<Spacer>
<Heading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ function NavigationButtonAsItem( props ) {
}

function NavigationBackButtonAsItem( props ) {
return <NavigatorBackButton as={ GenericNavigationButton } { ...props } />;
return (
<NavigatorBackButton
as={ GenericNavigationButton }
{ ...props }
goToParent
/>
);
}

export { NavigationButtonAsItem, NavigationBackButtonAsItem };
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function SidebarNavigationScreen( {
__( 'Navigate to the previous view: %s' ),
parentTitle
) }
goToParent
/>
) : (
<div className="edit-site-sidebar-navigation-screen__icon-placeholder" />
Expand Down

0 comments on commit f5ac3ad

Please sign in to comment.