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

Add new Navigator components and use them in the global styles sidebar #34904

Merged
merged 11 commits into from
Sep 23, 2021
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,12 @@
"markdown_source": "../packages/components/src/navigation/README.md",
"parent": "components"
},
{
"title": "Navigator",
"slug": "navigator",
"markdown_source": "../packages/components/src/navigator/README.md",
"parent": "components"
},
{
"title": "Notice",
"slug": "notice",
Expand Down
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- Removed the deprecated `onClickOutside` prop from the `Popover` component ([#34537](https://github.com/WordPress/gutenberg/pull/34537)).
- Changed `RangeControl` component to not apply `shiftStep` to inputs from its `<input type="range"/>` ([35020](https://github.com/WordPress/gutenberg/pull/35020)).

### New Feature

- Add an experimental `Navigator` components ([#34904](https://github.com/WordPress/gutenberg/pull/34904)) as a replacement for the previous `Navigation` related components.

### Bug Fix

- Fixed rounding of value in `RangeControl` component when it loses focus while the `SHIFT` key is held. ([#35020](https://github.com/WordPress/gutenberg/pull/35020)).
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export { default as __experimentalNavigationBackButton } from './navigation/back
export { default as __experimentalNavigationGroup } from './navigation/group';
export { default as __experimentalNavigationItem } from './navigation/item';
export { default as __experimentalNavigationMenu } from './navigation/menu';
export {
Navigator as __experimentalNavigator,
NavigatorScreen as __experimentalNavigatorScreen,
useNavigator as __experimentalUseNavigator,
} from './navigator';
export { default as Notice } from './notice';
export { default as __experimentalNumberControl } from './number-control';
export { default as NoticeList } from './notice/list';
Expand Down Expand Up @@ -157,6 +162,7 @@ export {
useCustomUnits as __experimentalUseCustomUnits,
parseUnit as __experimentalParseUnit,
} from './unit-control';
export { View as __experimentalView } from './view';
export { VisuallyHidden } from './visually-hidden';
export { VStack as __experimentalVStack } from './v-stack';
export { default as IsolatedEventContainer } from './isolated-event-container';
Expand Down
88 changes: 88 additions & 0 deletions packages/components/src/navigator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Navigator

<div class="callout callout-alert">
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

The Navigator components allows rendering nested panels or menus (also called screens) and navigate between these different states. The Global Styles sidebar is an example of this.

The components is not opinionated in terms of UI, it lets compose any UI components to navigate between the nested screens.

## Usage

```jsx
import {
__experimentalNavigator as Navigator,
__experimentalNavigatorScreen as NavigatorScreen,
__experimentalUseNavigator as useNavigator,
} from '@wordpress/components';

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

const MyNavigation = () => (
<Navigator initialPath="/">
<NavigatorScreen path="/">
<p>This is the home screen.</p>
<NavigatorButton isPrimary 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>
</NavigatorScreen>
</Navigator>
);
```

## Navigator Props

`Navigator` supports the following props.

### `initialPath`

- Type: `string`
- Required: No

The initial active path.

## NavigatorScreen Props

`NavigatorScreen` supports the following props.

### `path`

- Type: `string`
- Required: Yes

The path of the current screen.

## The navigator object.

You can retrieve a `navigator` instance by using the `useNavigator` hook.
The navigator offers the following methods:

### `push`

- Type: `( path: string, options ) => void`

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

The available options are:

- `isBack` (`boolean): A boolean flag indicating that we're moving back to a previous state.
6 changes: 6 additions & 0 deletions packages/components/src/navigator/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';

export const NavigatorContext = createContext();
3 changes: 3 additions & 0 deletions packages/components/src/navigator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as Navigator } from './navigator';
export { default as NavigatorScreen } from './screen';
export { default as useNavigator } from './use-navigator';
21 changes: 21 additions & 0 deletions packages/components/src/navigator/navigator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { NavigatorContext } from './context';

function Navigator( { initialPath, children } ) {
const [ path, setPath ] = useState( { path: initialPath } );

return (
<NavigatorContext.Provider value={ [ path, setPath ] }>
{ children }
</NavigatorContext.Provider>
);
}

export default Navigator;
92 changes: 92 additions & 0 deletions packages/components/src/navigator/screen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import { motion } from 'framer-motion';

/**
* WordPress dependencies
*/
import { useContext, useEffect, useState } from '@wordpress/element';
import { useReducedMotion, useFocusOnMount } from '@wordpress/compose';
import { isRTL } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { NavigatorContext } from './context';

const animationEnterDelay = 0;
const animationEnterDuration = 0.14;
const animationExitDuration = 0.14;
const animationExitDelay = 0;

function NavigatorScreen( { children, path } ) {
const prefersReducedMotion = useReducedMotion();
const [ currentPath ] = useContext( NavigatorContext );
const isMatch = currentPath.path === path;
const ref = useFocusOnMount();

// This flag is used to only apply the focus on mount when the actual path changes.
// It avoids the focus to happen on the first render.
const [ hasPathChanged, setHasPathChanged ] = useState( false );
useEffect( () => {
setHasPathChanged( true );
}, [ path ] );

if ( ! isMatch ) {
return null;
}

if ( prefersReducedMotion ) {
return <div>{ children }</div>;
}

const animate = {
opacity: 1,
transition: {
delay: animationEnterDelay,
duration: animationEnterDuration,
ease: 'easeInOut',
},
x: 0,
};
const initial = {
opacity: 0,
x:
( isRTL() && currentPath.isBack ) ||
( ! isRTL() && ! currentPath.isBack )
? 50
: -50,
};
const exit = {
delay: animationExitDelay,
opacity: 0,
x:
( ! isRTL() && currentPath.isBack ) ||
( isRTL() && ! currentPath.isBack )
? 50
: -50,
transition: {
duration: animationExitDuration,
ease: 'easeInOut',
},
};

const animatedProps = {
animate,
exit,
initial,
};

return (
<motion.div
ref={ hasPathChanged ? ref : undefined }
{ ...animatedProps }
>
{ children }
</motion.div>
);
}

export default NavigatorScreen;
42 changes: 42 additions & 0 deletions packages/components/src/navigator/stories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Internal dependencies
*/
import Button from '../../button';
import { Navigator, NavigatorScreen, useNavigator } from '../';

export default {
title: 'Components (Experimental)/Navigator',
component: Navigator,
};

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

const MyNavigation = () => (
<Navigator initialPath="/">
<NavigatorScreen path="/">
<p>This is the home screen.</p>
<NavigatorButton isPrimary 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>
</NavigatorScreen>
</Navigator>
);

export const _default = () => {
return <MyNavigation />;
};
21 changes: 21 additions & 0 deletions packages/components/src/navigator/use-navigator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import { NavigatorContext } from './context';

function useNavigator() {
const [ , setPath ] = useContext( NavigatorContext );

return {
push( path, options ) {
setPath( { path, ...options } );
},
};
}

export default useNavigator;
Loading