Skip to content
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: add basic location history #37416

Merged
merged 9 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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( [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add a limit to this array (like 10 items or so)

Copy link
Contributor Author

@ciampo ciampo Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the motivation for it?

Adding a limit would have a few consequences. Let's say the limit is 10 — it means that, when we navigate to the 11th location, we would either:

  1. prevent any further navigation (since the stack is full). This approach doesn't seem very viable in terms of UX, since it would suddenly prevent the user from navigating to another screen. OR
  2. drop the initial root location, and make the second location in the array the new "root". This would introduce further complexity in the code (shifting the array, re-setting the isInitial prop, ...) and it would also mean that the user wouldn't be able to navigate back all the way to the initial route.

I personally think that, rather than enforcing a hard limit on the array, we should tackle this potential issue by making sure that our UIs are not designed to require too many levels of "screen nesting" in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @youknowriad , do you have any additional thoughts on my previous answer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't reply here. I was just thinking about memory leaks but as you said, it might not grow much so fine by me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, thank you!

...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