Skip to content

Commit

Permalink
Navigator: add basic location history (#37416)
Browse files Browse the repository at this point in the history
* `Navigator`: add basic location history

* Refactor `Navigator` usage across repository

* CHANGELOG

* Update docs and comments

* Update unit tests

* Update snapshots

* Fix README formatting

* Improve README

* Memoize `navigatorContextValue`
  • Loading branch information
ciampo authored Jan 21, 2022
1 parent 433bd23 commit dcf5fce
Show file tree
Hide file tree
Showing 26 changed files with 303 additions and 184 deletions.
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
- Mark `children` prop as optional in `SelectControl` ([#37872](https://github.com/WordPress/gutenberg/pull/37872))
- Add memoization of callbacks and context to prevent unnecessary rerenders of the `ToolsPanel` ([#38037](https://github.com/WordPress/gutenberg/pull/38037))

### Experimental

- Add basic history location support to `Navigator` ([#37416](https://github.com/WordPress/gutenberg/pull/37416)).

## 19.2.0 (2022-01-04)

### Experimental
Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/navigator/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ import { createContext } from '@wordpress/element';
*/
import type { NavigatorContext as NavigatorContextType } from './types';

const initialContextValue: NavigatorContextType = [ {}, () => {} ];
const initialContextValue: NavigatorContextType = {
location: {},
push: () => {},
pop: () => {},
};
export const NavigatorContext = createContext( initialContextValue );
50 changes: 29 additions & 21 deletions packages/components/src/navigator/navigator-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ This feature is still experimental. “Experimental” means this is an early im

The `NavigatorProvider` component allows rendering nested panels or menus (via the [`NavigatorScreen` component](/packages/components/src/navigator/navigator-screen/README.md)) and navigate between these different states (via the `useNavigator` hook). The Global Styles sidebar is an example of this.

The `Navigator*` family of components is _not_ opinionated in terms of UI, and can be composed with any UI components to navigate between the nested screens.

## Usage

```jsx
Expand All @@ -17,34 +15,34 @@ import {
__experimentalUseNavigator as useNavigator,
} from '@wordpress/components';

function NavigatorButton( {
path,
isBack = false,
...props
} ) {
const navigator = useNavigator();
function NavigatorButton( { path, ...props } ) {
const { push } = useNavigator();
return (
<Button
onClick={ () => navigator.push( path, { isBack } ) }
{ ...props }
/>
);
variant="primary"
onClick={ () => push( path ) }
{ ...props }
/>
);
}

function NavigatorBackButton( props ) {
const { pop } = useNavigator();
return <Button variant="secondary" onClick={ () => pop() } { ...props } />;
}

const MyNavigation = () => (
<NavigatorProvider initialPath="/">
<NavigatorScreen path="/">
<p>This is the home screen.</p>
<NavigatorButton isPrimary path="/child">
<NavigatorButton path="/child">
Navigate to child screen.
</NavigatorButton>
</NavigatorScreen>

<NavigatorScreen path="/child">
<p>This is the child screen.</p>
<NavigatorButton isPrimary path="/" isBack>
Go back
</NavigatorButton>
<NavigatorBackButton>Go back</NavigatorBackButton>
</NavigatorScreen>
</NavigatorProvider>
);
Expand All @@ -64,12 +62,22 @@ The initial active path.

You can retrieve a `navigator` instance by using the `useNavigator` hook.

The hook offers the following methods:
The `navigator` instance has a few properties:

### `push`: `( path: string, options: NavigateOptions ) => void`

The `push` function allows navigating to a given path. The second argument can augment the navigation operations with different options.

There currently aren't any available options.

### `pop`: `() => void`

### `push`: `( path: string, options ) => void`
The `pop` function allows navigating to the previous path.

The `push` function allows you to navigate to a given path. The second argument can augment the navigation operations with different options.
### `location`: `NavigatorLocation`

The available options are:
The `location` object represent the current location, and has a few properties:

- `isBack` (`boolean): A boolean flag indicating that we're moving back to a previous state. -->
- `path`: `string`. The path associated to the location.
- `isBack`: `boolean`. A flag that is `true` when the current location was reached by navigating backwards in the location stack.
- `isInitial`: `boolean`. A flag that is `true` only for the first (root) location in the location stack.
100 changes: 77 additions & 23 deletions packages/components/src/navigator/navigator-provider/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { css } from '@emotion/react';
/**
* WordPress dependencies
*/
import { useMemo, useState } from '@wordpress/element';
import { useMemo, useState, useCallback } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -20,7 +20,11 @@ import {
import { useCx } from '../../utils/hooks/use-cx';
import { View } from '../../view';
import { NavigatorContext } from '../context';
import type { NavigatorProviderProps, NavigatorPath } from '../types';
import type {
NavigatorProviderProps,
NavigatorLocation,
NavigatorContext as NavigatorContextType,
} from '../types';

function NavigatorProvider(
props: WordPressComponentProps< NavigatorProviderProps, 'div' >,
Expand All @@ -33,9 +37,60 @@ function NavigatorProvider(
...otherProps
} = useContextSystem( props, 'NavigatorProvider' );

const [ path, setPath ] = useState< NavigatorPath >( {
path: initialPath,
} );
const [ locationHistory, setLocationHistory ] = useState<
NavigatorLocation[]
>( [
{
path: initialPath,
isBack: false,
isInitial: true,
},
] );

const push: NavigatorContextType[ 'push' ] = useCallback(
( path, options ) => {
// Force the `isBack` flag to `false` when navigating forward on both the
// previous and the new location.
// Also force the `isInitial` flag to `false` for the new location, to make
// sure it doesn't get overridden by mistake.
setLocationHistory( [
...locationHistory.slice( 0, -1 ),
{
...locationHistory[ locationHistory.length - 1 ],
isBack: false,
},
{
...options,
path,
isBack: false,
isInitial: false,
},
] );
},
[ locationHistory ]
);

const pop: NavigatorContextType[ 'pop' ] = useCallback( () => {
if ( locationHistory.length > 1 ) {
// Force the `isBack` flag to `true` when navigating back.
setLocationHistory( [
...locationHistory.slice( 0, -2 ),
{
...locationHistory[ locationHistory.length - 2 ],
isBack: true,
},
] );
}
}, [ locationHistory ] );

const navigatorContextValue: NavigatorContextType = useMemo(
() => ( {
location: locationHistory[ locationHistory.length - 1 ],
push,
pop,
} ),
[ locationHistory, push, pop ]
);

const cx = useCx();
const classes = useMemo(
Expand All @@ -46,7 +101,7 @@ function NavigatorProvider(

return (
<View ref={ forwardedRef } className={ classes } { ...otherProps }>
<NavigatorContext.Provider value={ [ path, setPath ] }>
<NavigatorContext.Provider value={ navigatorContextValue }>
{ children }
</NavigatorContext.Provider>
</View>
Expand All @@ -55,7 +110,6 @@ function NavigatorProvider(

/**
* The `NavigatorProvider` component allows rendering nested panels or menus (via the `NavigatorScreen` component) and navigate between these different states (via the `useNavigator` hook).
* The Global Styles sidebar is an example of this. The `Navigator*` family of components is _not_ opinionated in terms of UI, and can be composed with any UI components to navigate between the nested screens.
*
* @example
* ```jsx
Expand All @@ -65,34 +119,34 @@ function NavigatorProvider(
* __experimentalUseNavigator as useNavigator,
* } from '@wordpress/components';
*
* function NavigatorButton( {
* path,
* isBack = false,
* ...props
* } ) {
* const navigator = useNavigator();
* return (
* <Button
* onClick={ () => navigator.push( path, { isBack } ) }
* { ...props }
* />
* );
* function NavigatorButton( { path, ...props } ) {
* const { push } = useNavigator();
* return (
* <Button
* variant="primary"
* onClick={ () => push( path ) }
* { ...props }
* />
* );
* }
*
* function NavigatorBackButton( props ) {
* const { pop } = useNavigator();
* return <Button variant="secondary" onClick={ () => pop() } { ...props } />;
* }
*
* const MyNavigation = () => (
* <NavigatorProvider initialPath="/">
* <NavigatorScreen path="/">
* <p>This is the home screen.</p>
* <NavigatorButton isPrimary path="/child">
* <NavigatorButton path="/child">
* Navigate to child screen.
* </NavigatorButton>
* </NavigatorScreen>
*
* <NavigatorScreen path="/child">
* <p>This is the child screen.</p>
* <NavigatorButton isPrimary path="/" isBack>
* Go back
* </NavigatorButton>
* <NavigatorBackButton>Go back</NavigatorBackButton>
* </NavigatorScreen>
* </NavigatorProvider>
* );
Expand Down
42 changes: 20 additions & 22 deletions packages/components/src/navigator/navigator-screen/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ function NavigatorScreen( props: Props, forwardedRef: Ref< any > ) {
);

const prefersReducedMotion = useReducedMotion();
const [ currentPath ] = useContext( NavigatorContext );
const isMatch = currentPath.path === path;
const { location } = useContext( NavigatorContext );
const isMatch = location.path === path;
const ref = useFocusOnMount();

const cx = useCx();
Expand Down Expand Up @@ -95,17 +95,15 @@ function NavigatorScreen( props: Props, forwardedRef: Ref< any > ) {
const initial = {
opacity: 0,
x:
( isRTL() && currentPath.isBack ) ||
( ! isRTL() && ! currentPath.isBack )
( isRTL() && location.isBack ) || ( ! isRTL() && ! location.isBack )
? 50
: -50,
};
const exit = {
delay: animationExitDelay,
opacity: 0,
x:
( ! isRTL() && currentPath.isBack ) ||
( isRTL() && ! currentPath.isBack )
( ! isRTL() && location.isBack ) || ( isRTL() && ! location.isBack )
? 50
: -50,
transition: {
Expand Down Expand Up @@ -143,34 +141,34 @@ function NavigatorScreen( props: Props, forwardedRef: Ref< any > ) {
* __experimentalUseNavigator as useNavigator,
* } from '@wordpress/components';
*
* function NavigatorButton( {
* path,
* isBack = false,
* ...props
* } ) {
* const navigator = useNavigator();
* return (
* <Button
* onClick={ () => navigator.push( path, { isBack } ) }
* { ...props }
* />
* );
* function NavigatorButton( { path, ...props } ) {
* const { push } = useNavigator();
* return (
* <Button
* variant="primary"
* onClick={ () => push( path ) }
* { ...props }
* />
* );
* }
*
* function NavigatorBackButton( props ) {
* const { pop } = useNavigator();
* return <Button variant="secondary" onClick={ () => pop() } { ...props } />;
* }
*
* const MyNavigation = () => (
* <NavigatorProvider initialPath="/">
* <NavigatorScreen path="/">
* <p>This is the home screen.</p>
* <NavigatorButton isPrimary path="/child">
* <NavigatorButton path="/child">
* Navigate to child screen.
* </NavigatorButton>
* </NavigatorScreen>
*
* <NavigatorScreen path="/child">
* <p>This is the child screen.</p>
* <NavigatorButton isPrimary path="/" isBack>
* Go back
* </NavigatorButton>
* <NavigatorBackButton>Go back</NavigatorBackButton>
* </NavigatorScreen>
* </NavigatorProvider>
* );
Expand Down
Loading

0 comments on commit dcf5fce

Please sign in to comment.