Skip to content

Commit

Permalink
Navigator: simplify backwards navigation APIs (#63317)
Browse files Browse the repository at this point in the history
* NavigatorProvider: make goBack an alias for goToParent

* NavigatorBackButton: deprecate and ignore `goToParent` prop

* NavigatorBackButton: always call `goBack`

* Navigator: deprecate `goToParent` method

* NavigatorToParentButton: deprecate the component, make it an alias for NavigatorGoBackButton

* Add missing JSDocs for Navigator types

* Update README

* More docs additions

* Fix tests by assuming that even an invalid HTML path starts with '/'

* Add emphasis on the need for `path` to start with `/`

* Add deprecation warnings

* Update unit tests

* Add back README for deprecated component

* Add deprecation warning for `goToParent` function

* CHANGELOG

* Add more docs

* Typos

* Remove entirely the `goToParent` prop on `useNavigatorBackButton`

This can be done because its usage was actually only internal to the component

* Add deprecated APIs tests

* Remove extra import (thank you autocomplete)

---

Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>
  • Loading branch information
3 people authored Aug 16, 2024
1 parent a00448a commit 42db13f
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 114 deletions.
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
16 changes: 6 additions & 10 deletions packages/components/src/navigator/navigator-back-button/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,27 @@ import type { WordPressComponentProps } from '../../context';
import { useContextSystem } from '../../context';
import Button from '../../button';
import useNavigator from '../use-navigator';
import type { NavigatorBackButtonHookProps } from '../types';
import type { NavigatorBackButtonProps } from '../types';

export function useNavigatorBackButton(
props: WordPressComponentProps< NavigatorBackButtonHookProps, 'button' >
props: WordPressComponentProps< NavigatorBackButtonProps, 'button' >
) {
const {
onClick,
as = Button,
goToParent: goToParentProp = false,

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

const { goBack, goToParent } = useNavigator();
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
74 changes: 38 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 `/`) is found.

## Props

Expand All @@ -65,28 +69,26 @@ 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 manage focus themselves;
- `replace`: `boolean`. An optional property used to cause the new location to replace the current location in the stack.

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 paths you define are hierarchical (see note above).

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, except for the `isBack` property, which is not available for the `goBack` method.

### `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
20 changes: 13 additions & 7 deletions packages/components/src/navigator/navigator-provider/component.tsx
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 `/`) is found.

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

1 comment on commit 42db13f

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 42db13f.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/10422464517
📝 Reported issues:

Please sign in to comment.