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: simplify backwards navigation APIs #63317

Merged
merged 20 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- `ToggleControl`
- `ToggleGroupControl`
- `TreeSelect`
- Deprecate `NavigatorToParentButton` and `useNavigator().goToParent()` in favor of `NavigatorBackButton` and `useNavigator().goBack()` ([#63317](https://github.com/WordPress/gutenberg/pull/63317)).

### Enhancements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function UnconnectedNavigatorBackButton(
* <NavigatorScreen path="/child">
* <p>This is the child screen.</p>
* <NavigatorBackButton>
* Go back
* Go back (to parent)
* </NavigatorBackButton>
* </NavigatorScreen>
* </NavigatorProvider>
Expand Down
24 changes: 16 additions & 8 deletions packages/components/src/navigator/navigator-back-button/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
Expand All @@ -18,23 +19,30 @@ export function useNavigatorBackButton(
const {
onClick,
as = Button,
goToParent: goToParentProp = false,

// Deprecated
goToParent,

...otherProps
} = useContextSystem( props, 'NavigatorBackButton' );

const { goBack, goToParent } = useNavigator();
if ( goToParent !== undefined ) {
deprecated( '`goToParent` prop in wp.components.NavigatorBackButton', {
since: '6.7',
alternative:
'"back" navigations are always treated as going to the parent screen',
Copy link
Member

Choose a reason for hiding this comment

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

Should we just suggest goBack()? For someone attempting to use goToParent, it's not immediately clear in their IDE that goBack is the recommended alternative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The NavigatorBackButton doesn't have a goBack prop, since "going back" was already the default behavior. The goToParent prop was changing that behaviour, but now that the default behaviour of "going back" is the same as "going to parent", that prop basically became a no-op and can just be omitted.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, got it, makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually thanks to your comment, I realised that this deprecation was not necessary because that was a code path that could not be hit anymore. I just removed the code instead: da79e35

Copy link
Member

Choose a reason for hiding this comment

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

Perfect 🧹

} );
}

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

return {
Expand Down
73 changes: 37 additions & 36 deletions packages/components/src/navigator/navigator-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,42 @@ The `NavigatorProvider` component allows rendering nested views/panels/menus (vi

```jsx
import {
__experimentalNavigatorProvider as NavigatorProvider,
__experimentalNavigatorScreen as NavigatorScreen,
__experimentalNavigatorButton as NavigatorButton,
__experimentalNavigatorToParentButton as NavigatorToParentButton,
__experimentalNavigatorProvider as NavigatorProvider,
__experimentalNavigatorScreen as NavigatorScreen,
__experimentalNavigatorButton as NavigatorButton,
__experimentalNavigatorBackButton as NavigatorBackButton,
} from '@wordpress/components';

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

<NavigatorScreen path="/child">
<p>This is the child screen.</p>
<NavigatorToParentButton>
Go back
</NavigatorToParentButton>
</NavigatorScreen>
</NavigatorProvider>
<NavigatorProvider initialPath="/">
<NavigatorScreen path="/">
<p>This is the home screen.</p>
<NavigatorButton path="/child">
Navigate to child screen.
</NavigatorButton>
</NavigatorScreen>

<NavigatorScreen path="/child">
<p>This is the child screen.</p>
<NavigatorBackButton>Go back</NavigatorBackButton>
</NavigatorScreen>
</NavigatorProvider>
);
```

**Important note**

Parent/child navigation only works if the path you define are hierarchical, following a URL-like scheme where each path segment is separated by the `/` character.
`Navigator` assumes that screens are organized hierarchically according to their `path`, which should follow a URL-like scheme where each path segment starts with and is separated by the `/` character.

`Navigator` will treat "back" navigations as going to the parent screen — it is therefore responsibility of the consumer of the component to create the correct screen hierarchy.

For example:
- `/` is the root of all paths. There should always be a screen with `path="/"`.
- `/parent/child` is a child of `/parent`.
- `/parent/child/grand-child` is a child of `/parent/child`.
- `/parent/:param` is a child of `/parent` as well.

- `/` is the root of all paths. There should always be a screen with `path="/"`.
- `/parent/child` is a child of `/parent`.
- `/parent/child/grand-child` is a child of `/parent/child`.
- `/parent/:param` is a child of `/parent` as well.
- if the current screen has a `path` with value `/parent/child/grand-child`, when going "back" Navigator will try to recursively navigate the path hierarchy until a matching screen (or the root `/`) are found.
ciampo marked this conversation as resolved.
Show resolved Hide resolved

## Props

Expand All @@ -65,28 +69,25 @@ The `goTo` function allows navigating to a given path. The second argument can a

The available options are:

- `focusTargetSelector`: `string`. An optional property used to specify the CSS selector used to restore focus on the matching element when navigating back.
- `isBack`: `boolean`. An optional property used to specify whether the navigation should be considered as backwards (thus enabling focus restoration when possible, and causing the animation to be backwards too)

### `goToParent`: `() => void;`
- `focusTargetSelector`: `string`. An optional property used to specify the CSS selector used to restore focus on the matching element when navigating back.
- `isBack`: `boolean`. An optional property used to specify whether the navigation should be considered as backwards (thus enabling focus restoration when possible, and causing the animation to be backwards too)
- `skipFocus`: `boolean`. An optional property used to opt out of `Navigator`'s focus management, useful when the consumer of the component wants to implement their own custom focus management.

The `goToParent` function allows navigating to the parent screen.
### `goBack`: `( path: string, options: NavigateOptions ) => void`

Parent/child navigation only works if the path you define are hierarchical (see note above).
The `goBack` function allows navigating to the parent screen. Parent/child navigation only works if the path you define are hierarchical (see note above).
ciampo marked this conversation as resolved.
Show resolved Hide resolved

When a match is not found, the function will try to recursively navigate the path hierarchy until a matching screen (or the root `/`) are found.

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

The `goBack` function allows navigating to the previous path.
The available options are the same as for the `goTo` method minus the `isBack` property, which is not available for the `goBack` method.
ciampo marked this conversation as resolved.
Show resolved Hide resolved

### `location`: `NavigatorLocation`

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

- `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.
- `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 history.
- `isInitial`: `boolean`. A flag that is `true` only for the first (root) location in the location history.

### `params`: `Record< string, string | string[] >`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import type {
Screen,
NavigateToParentOptions,
} from '../types';
import deprecated from '@wordpress/deprecated';

type MatchedPath = ReturnType< typeof patternMatch >;

type RouterAction =
| { type: 'add' | 'remove'; screen: Screen }
| { type: 'goback' }
| { type: 'goto'; path: string; options?: NavigateOptions }
| { type: 'gotoparent'; options?: NavigateToParentOptions };

Expand Down Expand Up @@ -160,9 +160,6 @@ function routerReducer(
case 'remove':
screens = removeScreen( state, action.screen );
break;
case 'goback':
locationHistory = goBack( state );
break;
case 'goto':
locationHistory = goTo( state, action.path, action.options );
break;
Expand Down Expand Up @@ -223,11 +220,20 @@ function UnconnectedNavigatorProvider(
// The methods are constant forever, create stable references to them.
const methods = useMemo(
() => ( {
goBack: () => dispatch( { type: 'goback' } ),
// Note: calling goBack calls `goToParent` internally, as it was established
// that `goBack` should behave like `goToParent`, and `goToParent` should
// be marked as deprecated.
goBack: ( options: NavigateToParentOptions | undefined ) =>
dispatch( { type: 'gotoparent', options } ),
goTo: ( path: string, options?: NavigateOptions ) =>
dispatch( { type: 'goto', path, options } ),
goToParent: ( options: NavigateToParentOptions | undefined ) =>
dispatch( { type: 'gotoparent', options } ),
goToParent: ( options: NavigateToParentOptions | undefined ) => {
deprecated( `wp.components.useNavigator().goToParent`, {
since: '6.7',
alternative: 'wp.components.useNavigator().goBack',
} );
dispatch( { type: 'gotoparent', options } );
},
addScreen: ( screen: Screen ) =>
dispatch( { type: 'add', screen } ),
removeScreen: ( screen: Screen ) =>
Expand Down
14 changes: 13 additions & 1 deletion packages/components/src/navigator/navigator-screen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ The component accepts the following props:

### `path`: `string`

The screen's path, matched against the current path stored in the navigator.
The screen&quot;s path, matched against the current path stored in the navigator.

`Navigator` assumes that screens are organized hierarchically according to their `path`, which should follow a URL-like scheme where each path segment starts with and is separated by the `/` character.

`Navigator` will treat "back" navigations as going to the parent screen — it is therefore responsibility of the consumer of the component to create the correct screen hierarchy.

For example:

- `/` is the root of all paths. There should always be a screen with `path="/"`.
- `/parent/child` is a child of `/parent`.
- `/parent/child/grand-child` is a child of `/parent/child`.
- `/parent/:param` is a child of `/parent` as well.
- if the current screen has a `path` with value `/parent/child/grand-child`, when going "back" Navigator will try to recursively navigate the path hierarchy until a matching screen (or the root `/`) are found.
ciampo marked this conversation as resolved.
Show resolved Hide resolved

- Required: Yes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
Copy link
Member

Choose a reason for hiding this comment

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

Sounds like we can remove the experimental callout now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was planning on removing those experimental callouts and Storybook tags once we also export the component without the experimental prefix (in a follow-up PR)

</div>

This component is deprecated. Please use the [`NavigatorBackButton`](/packages/components/src/navigator/navigator-back-button/README.md) component instead.

The `NavigatorToParentButton` component can be used to navigate to a screen and should be used in combination with the [`NavigatorProvider`](/packages/components/src/navigator/navigator-provider/README.md), the [`NavigatorScreen`](/packages/components/src/navigator/navigator-screen/README.md) and the [`NavigatorButton`](/packages/components/src/navigator/navigator-button/README.md) components (or the `useNavigator` hook).

## Usage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,62 +1,33 @@
/**
* External dependencies
* WordPress dependencies
*/
import type { ForwardedRef } from 'react';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
*/
import { NavigatorBackButton } from '../navigator-back-button';
import type { WordPressComponentProps } from '../../context';
import { contextConnect } from '../../context';
import { View } from '../../view';
import { useNavigatorBackButton } from '../navigator-back-button/hook';
import type { NavigatorToParentButtonProps } from '../types';
import type { NavigatorBackButtonProps } from '../types';

function UnconnectedNavigatorToParentButton(
props: WordPressComponentProps< NavigatorToParentButtonProps, 'button' >,
forwardedRef: ForwardedRef< any >
props: WordPressComponentProps< NavigatorBackButtonProps, 'button' >,
forwardedRef: React.ForwardedRef< any >
) {
const navigatorToParentButtonProps = useNavigatorBackButton( {
...props,
goToParent: true,
deprecated( 'wp.components.NavigatorToParentButton', {
since: '6.7',
alternative: 'wp.components.NavigatorBackButton',
} );

return <View ref={ forwardedRef } { ...navigatorToParentButtonProps } />;
return <NavigatorBackButton ref={ forwardedRef } { ...props } />;
}

/*
* The `NavigatorToParentButton` component can be used to navigate to a screen and
* should be used in combination with the `NavigatorProvider`, the
* `NavigatorScreen` and the `NavigatorButton` components (or the `useNavigator`
* hook).
*
* @example
* ```jsx
* import {
* __experimentalNavigatorProvider as NavigatorProvider,
* __experimentalNavigatorScreen as NavigatorScreen,
* __experimentalNavigatorButton as NavigatorButton,
* __experimentalNavigatorToParentButton as NavigatorToParentButton,
* } from '@wordpress/components';
*
* const MyNavigation = () => (
* <NavigatorProvider initialPath="/">
* <NavigatorScreen path="/">
* <p>This is the home screen.</p>
* <NavigatorButton path="/child">
* Navigate to child screen.
* </NavigatorButton>
* </NavigatorScreen>
/**
* _Note: this component is deprecated. Please use the `NavigatorBackButton`
* component instead._
*
* <NavigatorScreen path="/child">
* <p>This is the child screen.</p>
* <NavigatorToParentButton>
* Go to parent
* </NavigatorToParentButton>
* </NavigatorScreen>
* </NavigatorProvider>
* );
* ```
* @deprecated
*/
export const NavigatorToParentButton = contextConnect(
UnconnectedNavigatorToParentButton,
Expand Down
30 changes: 6 additions & 24 deletions packages/components/src/navigator/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ import {
NavigatorScreen,
NavigatorButton,
NavigatorBackButton,
NavigatorToParentButton,
useNavigator,
} from '..';
import type { NavigateOptions } from '../types';

const INVALID_HTML_ATTRIBUTE = {
raw: ' "\'><=invalid_path',
escaped: " &quot;'&gt;<=invalid_path",
raw: '/ "\'><=invalid_path',
escaped: "/ &quot;'&gt;<=invalid_path",
Comment on lines +28 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of "going back" always behaving like "go to parent", all paths need to start with / in order to work well with the component's pattern matching logic

};

const PATHS = {
Expand Down Expand Up @@ -148,23 +147,6 @@ function CustomNavigatorBackButton( {
);
}

function CustomNavigatorToParentButton( {
onClick,
...props
}: Omit< ComponentPropsWithoutRef< typeof NavigatorBackButton >, 'onClick' > & {
onClick?: CustomTestOnClickHandler;
} ) {
return (
<NavigatorToParentButton
onClick={ () => {
// Used to spy on the values passed to `navigator.goBack`.
onClick?.( { type: 'goToParent' } );
} }
{ ...props }
/>
);
}
ciampo marked this conversation as resolved.
Show resolved Hide resolved

const ProductScreen = ( {
onBackButtonClick,
}: {
Expand Down Expand Up @@ -344,20 +326,20 @@ const MyHierarchicalNavigation = ( {
>
{ BUTTON_TEXT.toNestedScreen }
</CustomNavigatorButton>
<CustomNavigatorToParentButton
<CustomNavigatorBackButton
onClick={ onNavigatorButtonClick }
>
{ BUTTON_TEXT.back }
</CustomNavigatorToParentButton>
</CustomNavigatorBackButton>
</NavigatorScreen>

<NavigatorScreen path={ PATHS.NESTED }>
<p>{ SCREEN_TEXT.nested }</p>
<CustomNavigatorToParentButton
<CustomNavigatorBackButton
onClick={ onNavigatorButtonClick }
>
{ BUTTON_TEXT.back }
</CustomNavigatorToParentButton>
</CustomNavigatorBackButton>
<CustomNavigatorGoToBackButton
path={ PATHS.CHILD }
onClick={ onNavigatorButtonClick }
Expand Down
Loading
Loading