Skip to content

Commit

Permalink
Try controlled navigator behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Jun 26, 2023
1 parent 48b30f4 commit 1584505
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 74 deletions.
173 changes: 100 additions & 73 deletions packages/components/src/navigator/navigator-provider/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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 };
Expand All @@ -59,16 +60,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, updatedLocation ] = useControlledValue( {
onChange,
value: locationProp,
defaultValue: { path: initialPath },
} );

const [ locationHistory, setLocationHistory ] = useState<
NavigatorLocation[]
>( [
{
path: initialPath,
},
] );
>( [ location ?? { path: initialPath } ] );
const currentLocationHistory = useRef< NavigatorLocation[] >( [] );
const [ screens, dispatch ] = useReducer( screensReducer, [] );
const currentScreens = useRef< Screen[] >( [] );
Expand All @@ -78,6 +86,74 @@ function UnconnectedNavigatorProvider(
useEffect( () => {
currentLocationHistory.current = locationHistory;
}, [ locationHistory ] );
useEffect( () => {
if ( ! location ) {
return;
}

const {
focusTargetSelector,
isBack = false,
skipFocus = false,
path: destinationPath,
...restOptions
} = location;

const isNavigatingToPreviousPath =
isBack &&
currentLocationHistory.current.length > 1 &&
currentLocationHistory.current[
currentLocationHistory.current.length - 2
].path === destinationPath;

if ( isNavigatingToPreviousPath ) {
setLocationHistory( ( prevLocationHistory ) => {
if ( prevLocationHistory.length <= 1 ) {
return prevLocationHistory;
}
return [
...prevLocationHistory.slice( 0, -2 ),
{
...prevLocationHistory[
prevLocationHistory.length - 2
],
isBack: true,
hasRestoredFocus: false,
},
];
} );
return;
}

setLocationHistory( ( prevLocationHistory ) => {
const newLocation = {
...restOptions,
path: destinationPath,
isBack,
hasRestoredFocus: false,
skipFocus,
};

if ( prevLocationHistory.length < 1 ) {
return [ newLocation ];
}

return [
...prevLocationHistory.slice(
prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 ? 1 : 0,
-1
),
// Assign `focusTargetSelector` to the previous location in history
// (the one we just navigated from).
{
...prevLocationHistory[ prevLocationHistory.length - 1 ],
focusTargetSelector,
},
newLocation,
];
} );
}, [ location ] );

const currentMatch = useRef< MatchedPath >();
const matchedPath = useMemo( () => {
let currentPath: string | undefined;
Expand All @@ -91,8 +167,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.
Expand Down Expand Up @@ -127,75 +203,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;
}

updatedLocation( {
isBack: true,
path: currentLocationHistory.current[
currentLocationHistory.current.length - 2
].path,
} );
}, [] );
}, [ updatedLocation ] );

const goTo: NavigatorContextType[ 'goTo' ] = useCallback(
( path, options = {} ) => {
const {
focusTargetSelector,
isBack = false,
skipFocus = 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 < 1 ) {
return [ newLocation ];
}

return [
...prevLocationHistory.slice(
prevLocationHistory.length > MAX_HISTORY_LENGTH - 1
? 1
: 0,
-1
),
// Assign `focusTargetSelector` to the previous location in history
// (the one we just navigated from).
{
...prevLocationHistory[
prevLocationHistory.length - 1
],
focusTargetSelector,
},
newLocation,
];
( destinationPath, options = {} ) => {
updatedLocation( {
...options,
path: destinationPath,
} );
},
[ goBack ]
[ updatedLocation ]
);

const goToParent: NavigatorContextType[ 'goToParent' ] =
Expand Down
57 changes: 57 additions & 0 deletions packages/components/src/navigator/stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import type { ComponentMeta, ComponentStory } from '@storybook/react';

/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
Expand All @@ -18,6 +23,7 @@ import {
NavigatorToParentButton,
useNavigator,
} from '..';
import type { NavigatorLocation } from '../types';

const meta: ComponentMeta< typeof NavigatorProvider > = {
component: NavigatorProvider,
Expand Down Expand Up @@ -366,3 +372,54 @@ SkipFocus.args = {
</>
),
};

const ControlledNavigatorTemplate: ComponentStory<
typeof NavigatorProvider
> = ( { style } ) => {
const [ location, setLocation ] = useState< NavigatorLocation >( {
path: '/',
} );
return (
<NavigatorProvider
location={ location }
onChange={ setLocation }
style={ { ...style, height: '100vh', maxHeight: '450px' } }
>
<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
<NavigatorToParentButton variant="secondary">
Go back to parent
</NavigatorToParentButton>
</CardBody>
</Card>
</NavigatorScreen>
<NavigatorScreen path="/child2">
<Card>
<CardBody>
This is the second child
<NavigatorToParentButton variant="secondary">
Go back to parent
</NavigatorToParentButton>
</CardBody>
</Card>
</NavigatorScreen>
</NavigatorProvider>
);
};

export const ControlledNavigator: ComponentStory< typeof NavigatorProvider > =
ControlledNavigatorTemplate.bind( {} );
15 changes: 14 additions & 1 deletion packages/components/src/navigator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,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;

/**
* Navigates to a new path.
*
* @param path The path to navigate to.
*/
onChange?: ( location: NavigatorLocation ) => void;
};

export type NavigatorScreenProps = {
Expand Down

0 comments on commit 1584505

Please sign in to comment.