diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json
index 75573dd5ea1337..e36e868ac92e99 100644
--- a/docs/manifest-devhub.json
+++ b/docs/manifest-devhub.json
@@ -731,6 +731,12 @@
"markdown_source": "../packages/components/src/form-token-field/README.md",
"parent": "components"
},
+ {
+ "title": "Guide",
+ "slug": "guide",
+ "markdown_source": "../packages/components/src/guide/README.md",
+ "parent": "components"
+ },
{
"title": "NavigateRegions",
"slug": "navigate-regions",
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 00cd959951484e..28dd4971244937 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 8.3.0 (Unreleased)
+
+### New Features
+
+- Added a new `Guide` component which allows developers to easily present a user guide.
+
## 8.2.0 (2019-08-29)
### New Features
diff --git a/packages/components/src/guide/README.md b/packages/components/src/guide/README.md
new file mode 100644
index 00000000000000..0c9fdacaf784d4
--- /dev/null
+++ b/packages/components/src/guide/README.md
@@ -0,0 +1,56 @@
+Guide
+========
+
+`Guide` is a React component that renders a _user guide_ in a modal. The guide consists of several `GuidePage` components which the user can step through one by one. The guide is finished when the modal is closed or when the user clicks _Finish_ on the last page of the guide.
+
+## Usage
+
+```jsx
+function MyTutorial() {
+ const [ isOpen, setIsOpen ] = useState( true );
+
+ if ( ! isOpen ) {
+ return null;
+ }
+
+ setIsOpen( false ) }>
+
+
Welcome to the ACME Store! Select a category to begin browsing our wares.
+
+
+
When you find something you love, click Add to Cart to add the product to your shopping cart.
+
+
+}
+```
+
+## Props
+
+The component accepts the following props:
+
+### onFinish
+
+A function which is called when the guide is finished. The guide is finished when the modal is closed or when the user clicks _Finish_ on the last page of the guide.
+
+- Type: `function`
+- Required: Yes
+
+### children
+
+A list of `GuidePage` components. One page is shown at a time.
+
+- Required: Yes
+
+### className
+
+A custom class to add to the modal.
+
+- Type: `string`
+- Required: No
+
+### finishButtonText
+
+Use this to customize the label of the _Finish_ button shown at the end of the guide.
+
+- Type: `string`
+- Required: No
diff --git a/packages/components/src/guide/finish-button.js b/packages/components/src/guide/finish-button.js
new file mode 100644
index 00000000000000..baf1fccaff480a
--- /dev/null
+++ b/packages/components/src/guide/finish-button.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { useRef, useLayoutEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import Button from '../button';
+
+export default function FinishButton( { className, onClick, children } ) {
+ const button = useRef( null );
+
+ // Focus the button on mount if nothing else is focused. This prevents a
+ // focus loss when the 'Next' button is swapped out.
+ useLayoutEffect( () => {
+ if ( document.activeElement === document.body ) {
+ button.current.focus();
+ }
+ }, [ button ] );
+
+ return (
+
+ );
+}
diff --git a/packages/components/src/guide/icons.js b/packages/components/src/guide/icons.js
new file mode 100644
index 00000000000000..02638ff58ec331
--- /dev/null
+++ b/packages/components/src/guide/icons.js
@@ -0,0 +1,24 @@
+/**
+ * Internal dependencies
+ */
+import { SVG, Path, Circle } from '../primitives/svg';
+
+export const BackButtonIcon = () => (
+
+);
+
+export const ForwardButtonIcon = () => (
+
+);
+
+export const PageControlIcon = ( { isSelected } ) => (
+
+);
diff --git a/packages/components/src/guide/index.js b/packages/components/src/guide/index.js
new file mode 100644
index 00000000000000..3d0a905741e14b
--- /dev/null
+++ b/packages/components/src/guide/index.js
@@ -0,0 +1,107 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { useState, Children } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import Modal from '../modal';
+import KeyboardShortcuts from '../keyboard-shortcuts';
+import IconButton from '../icon-button';
+import PageControl from './page-control';
+import { BackButtonIcon, ForwardButtonIcon } from './icons';
+import FinishButton from './finish-button';
+
+export default function Guide( { children, className, finishButtonText, onFinish } ) {
+ const [ currentPage, setCurrentPage ] = useState( 0 );
+
+ const numberOfPages = Children.count( children );
+ const canGoBack = currentPage > 0;
+ const canGoForward = currentPage < numberOfPages - 1;
+
+ const goBack = () => {
+ if ( canGoBack ) {
+ setCurrentPage( currentPage - 1 );
+ }
+ };
+
+ const goForward = () => {
+ if ( canGoForward ) {
+ setCurrentPage( currentPage + 1 );
+ }
+ };
+
+ if ( numberOfPages === 0 ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ }
+ /* translators: %1$d: current page number %2$d: total number of pages */
+ aria-label={ sprintf( __( 'Page %1$d of %2$d' ), page + 1, numberOfPages ) }
+ onClick={ () => setCurrentPage( page ) }
+ />
+
+ ) ) }
+
+ );
+}
diff --git a/packages/components/src/guide/page.js b/packages/components/src/guide/page.js
new file mode 100644
index 00000000000000..d8f0b582e8f9b7
--- /dev/null
+++ b/packages/components/src/guide/page.js
@@ -0,0 +1,3 @@
+export default function GuidePage( props ) {
+ return ;
+}
diff --git a/packages/components/src/guide/stories/index.js b/packages/components/src/guide/stories/index.js
new file mode 100644
index 00000000000000..3a96e1031e3968
--- /dev/null
+++ b/packages/components/src/guide/stories/index.js
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { times } from 'lodash';
+import { text, number } from '@storybook/addon-knobs';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import Button from '../../button';
+import Guide from '../';
+import GuidePage from '../page';
+
+export default { title: 'Components|Guide', component: Guide };
+
+const ModalExample = ( { numberOfPages, ...props } ) => {
+ const [ isOpen, setOpen ] = useState( false );
+
+ const openGuide = () => setOpen( true );
+ const closeGuide = () => setOpen( false );
+
+ const loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
+
+ return (
+ <>
+
+ { isOpen && (
+
+ { times( numberOfPages, ( page ) => (
+
+
Page { page + 1 } of { numberOfPages }
+
{ loremIpsum }
+
+ ) ) }
+
+ ) }
+ >
+ );
+};
+
+export const _default = () => {
+ const finishButtonText = text( 'finishButtonText', 'Finish' );
+ const numberOfPages = number( 'numberOfPages', 3, { range: true, min: 1, max: 10, step: 1 } );
+
+ const modalProps = {
+ finishButtonText,
+ numberOfPages,
+ };
+
+ return ;
+};
diff --git a/packages/components/src/guide/style.scss b/packages/components/src/guide/style.scss
new file mode 100644
index 00000000000000..92044211ce3cd4
--- /dev/null
+++ b/packages/components/src/guide/style.scss
@@ -0,0 +1,119 @@
+.components-guide {
+ .components-modal__header {
+ background: none;
+ border-bottom: none;
+
+ .components-icon-button {
+ align-self: flex-start;
+ margin-top: $grid-size-xlarge;
+ position: static;
+ }
+ }
+
+ &__container {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ margin-top: -$header-height;
+ min-height: 100%;
+ }
+
+ &__footer {
+ align-content: center;
+ display: flex;
+ height: 30px;
+ justify-content: center;
+ margin: auto 0 $grid-size-xlarge 0;
+ position: relative;
+ width: 100%;
+
+ @include break-small() {
+ margin: $grid-size-xlarge 0 0;
+ }
+ }
+
+ &__page-control {
+ margin: 0;
+
+ li {
+ display: inline-block;
+ margin: 0 2px;
+ }
+
+ .components-icon-button {
+ height: 30px;
+ }
+ }
+}
+
+.components-modal__frame.components-guide {
+ @media (max-width: $break-small) {
+ bottom: 15%;
+ left: $grid-size-xlarge;
+ right: $grid-size-xlarge;
+ top: 15%;
+ }
+}
+
+.components-button {
+ &.components-guide__back-button,
+ &.components-guide__forward-button,
+ &.components-guide__finish-button {
+ height: 30px;
+ position: absolute;
+ }
+
+ &.components-guide__back-button,
+ &.components-guide__forward-button {
+ font-size: 0;
+ padding: 4px 2px;
+
+ &.has-text svg {
+ margin: 0;
+ }
+
+ @include break-small() {
+ font-size: $default-font-size;
+ }
+ }
+
+ &.components-guide__back-button {
+ left: 0;
+
+ @include break-small() {
+ padding: 4px 8px 4px 2px;
+
+ &.has-text svg {
+ margin-right: 4px;
+ }
+ }
+ }
+
+ &.components-guide__forward-button {
+ right: 0;
+
+ @include break-small() {
+ padding: 4px 2px 4px 8px;
+
+ &.has-text svg {
+ margin-left: 4px;
+ order: 1;
+ }
+ }
+ }
+
+ &.components-guide__finish-button {
+ display: none;
+ right: 0;
+
+ @include break-small() {
+ display: unset;
+ }
+ }
+
+ &.components-guide__inline-finish-button {
+ @include break-small() {
+ display: none;
+ }
+ }
+}
diff --git a/packages/components/src/guide/test/finish-button.js b/packages/components/src/guide/test/finish-button.js
new file mode 100644
index 00000000000000..86e2d659f21b10
--- /dev/null
+++ b/packages/components/src/guide/test/finish-button.js
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+import { shallow } from 'enzyme';
+import { create } from 'react-test-renderer';
+
+/**
+ * WordPress dependencies
+ */
+import { Button } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import FinishButton from '../finish-button';
+
+describe( 'FinishButton', () => {
+ it( 'renders a button', () => {
+ const wrapper = shallow( );
+ expect( wrapper.find( Button ) ).toHaveLength( 1 );
+ } );
+
+ it( 'calls onClick when the button is clicked', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow( );
+ wrapper.find( Button ).prop( 'onClick' )();
+ expect( onClick ).toHaveBeenCalled();
+ } );
+
+ it( 'receives focus on mount when nothing is focused', () => {
+ const focus = jest.fn();
+ create( , {
+ createNodeMock: () => ( { focus } ),
+ } );
+ expect( focus ).toHaveBeenCalled();
+ } );
+
+ it( 'does not receive focus on mount when something is already focused', () => {
+ const button = document.createElement( 'button' );
+ document.body.append( button );
+ button.focus();
+
+ const focus = jest.fn();
+ create( , {
+ createNodeMock: () => ( { focus } ),
+ } );
+ expect( focus ).not.toHaveBeenCalled();
+ } );
+} );
diff --git a/packages/components/src/guide/test/index.js b/packages/components/src/guide/test/index.js
new file mode 100644
index 00000000000000..bb1fad16aa3a7a
--- /dev/null
+++ b/packages/components/src/guide/test/index.js
@@ -0,0 +1,78 @@
+/**
+ * External dependencies
+ */
+import { shallow } from 'enzyme';
+
+/**
+ * Internal dependencies
+ */
+import Guide from '../';
+import GuidePage from '../page';
+import PageControl from '../page-control';
+import Modal from '../../modal';
+
+describe( 'Guide', () => {
+ it( 'renders nothing when there are no pages', () => {
+ const wrapper = shallow( );
+ expect( wrapper.isEmptyRender() ).toBe( true );
+ } );
+
+ it( 'renders one page at a time', () => {
+ const wrapper = shallow(
+
+ Page 1
+ Page 2
+
+ );
+ expect( wrapper.find( GuidePage ) ).toHaveLength( 1 );
+ } );
+
+ it( 'hides back button and shows forward button on the first page', () => {
+ const wrapper = shallow(
+
+ Page 1
+ Page 2
+
+ );
+ expect( wrapper.find( PageControl ).prop( 'currentPage' ) ).toBe( 0 );
+ expect( wrapper.find( '.components-guide__back-button' ) ).toHaveLength( 0 );
+ expect( wrapper.find( '.components-guide__forward-button' ) ).toHaveLength( 1 );
+ expect( wrapper.find( '.components-guide__finish-button' ) ).toHaveLength( 0 );
+ } );
+
+ it( 'shows back button and shows finish button on the last page', () => {
+ const wrapper = shallow(
+
+ Page 1
+ Page 2
+
+ );
+ wrapper.find( '.components-guide__forward-button' ).simulate( 'click' );
+ expect( wrapper.find( PageControl ).prop( 'currentPage' ) ).toBe( 1 );
+ expect( wrapper.find( '.components-guide__back-button' ) ).toHaveLength( 1 );
+ expect( wrapper.find( '.components-guide__forward-button' ) ).toHaveLength( 0 );
+ expect( wrapper.find( '.components-guide__finish-button' ) ).toHaveLength( 1 );
+ } );
+
+ it( 'calls onFinish when the finish button is clicked', () => {
+ const onFinish = jest.fn();
+ const wrapper = shallow(
+
+ Page 1
+
+ );
+ wrapper.find( '.components-guide__finish-button' ).simulate( 'click' );
+ expect( onFinish ).toHaveBeenCalled();
+ } );
+
+ it( 'calls onFinish when the modal is closed', () => {
+ const onFinish = jest.fn();
+ const wrapper = shallow(
+
+ Page 1
+
+ );
+ wrapper.find( Modal ).prop( 'onRequestClose' )();
+ expect( onFinish ).toHaveBeenCalled();
+ } );
+} );
diff --git a/packages/components/src/guide/test/page-control.js b/packages/components/src/guide/test/page-control.js
new file mode 100644
index 00000000000000..6490dc1cfaa021
--- /dev/null
+++ b/packages/components/src/guide/test/page-control.js
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { shallow } from 'enzyme';
+
+/**
+ * WordPress dependencies
+ */
+import { IconButton } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import PageControl from '../page-control';
+
+describe( 'PageControl', () => {
+ it( 'renders an empty list when there are no pages', () => {
+ const wrapper = shallow( );
+ expect( wrapper.find( IconButton ) ).toHaveLength( 0 );
+ } );
+
+ it( 'renders a button for each page', () => {
+ const wrapper = shallow( );
+ expect( wrapper.find( IconButton ) ).toHaveLength( 5 );
+ } );
+
+ it( 'sets the current page when a button is clicked', () => {
+ const setCurrentPage = jest.fn();
+ const wrapper = shallow(
+
+ );
+ wrapper.find( IconButton ).at( 1 ).simulate( 'click' );
+ expect( setCurrentPage ).toHaveBeenCalledWith( 1 );
+ } );
+} );
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index ff4e4f65b85dc5..eef936406b81b9 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -34,6 +34,8 @@ export { default as FormFileUpload } from './form-file-upload';
export { default as FormToggle } from './form-toggle';
export { default as FormTokenField } from './form-token-field';
export { default as __experimentalGradientPicker } from './gradient-picker';
+export { default as Guide } from './guide';
+export { default as GuidePage } from './guide/page';
export { default as Icon } from './icon';
export { default as IconButton } from './icon-button';
export { default as KeyboardShortcuts } from './keyboard-shortcuts';
diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss
index a21839bd27b3fb..27f6bf2b47a0dc 100644
--- a/packages/components/src/style.scss
+++ b/packages/components/src/style.scss
@@ -22,6 +22,7 @@
@import "./form-file-upload/style.scss";
@import "./form-toggle/style.scss";
@import "./form-token-field/style.scss";
+@import "./guide/style.scss";
@import "./higher-order/navigate-regions/style.scss";
@import "./icon-button/style.scss";
@import "./menu-group/style.scss";
diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md
index 37d5f86d06cb95..6b2af5c5c6e1ed 100644
--- a/packages/e2e-test-utils/README.md
+++ b/packages/e2e-test-utils/README.md
@@ -137,6 +137,10 @@ _Parameters_
- _slug_ `string`: Plugin slug.
+# **disableFocusLossObservation**
+
+Removes the focus loss listener that `enableFocusLossObservation()` adds.
+
# **disablePrePublishChecks**
Disables Pre-publish checks.
@@ -156,6 +160,11 @@ _Returns_
- `Promise`: Promise resolving when drag completes.
+# **enableFocusLossObservation**
+
+Adds an event listener to the document which throws an error if there is a
+loss of focus.
+
# **enablePageDialogAccept**
Enables even listener which accepts a page dialog which
@@ -319,11 +328,6 @@ _Returns_
- `Promise`: Promise that uses `mockCheck` to see if a request should be mocked with `mock`, and optionally transforms the response with `responseObjectTransform`.
-# **observeFocusLoss**
-
-Binds to the document on page load which throws an error if a `focusout`
-event occurs without a related target (i.e. focus loss).
-
# **openAllBlockInserterCategories**
Opens all block inserter categories.
diff --git a/packages/e2e-test-utils/src/create-new-post.js b/packages/e2e-test-utils/src/create-new-post.js
index 91a95bf5216a45..3abf89d0e8e63b 100644
--- a/packages/e2e-test-utils/src/create-new-post.js
+++ b/packages/e2e-test-utils/src/create-new-post.js
@@ -26,14 +26,17 @@ export async function createNewPost( {
content,
excerpt,
} ).slice( 1 );
+
await visitAdminPage( 'post-new.php', query );
- await page.evaluate( ( _enableTips ) => {
- const action = _enableTips ? 'enableTips' : 'disableTips';
- wp.data.dispatch( 'core/nux' )[ action ]();
- }, enableTips );
+ const areTipsEnabled = await page.evaluate( () => wp.data.select( 'core/nux' ).areTipsEnabled() );
+
+ if ( enableTips !== areTipsEnabled ) {
+ await page.evaluate( ( _enableTips ) => {
+ const action = _enableTips ? 'enableTips' : 'disableTips';
+ wp.data.dispatch( 'core/nux' )[ action ]();
+ }, enableTips );
- if ( enableTips ) {
await page.reload();
}
}
diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js
index 4d78447dec0872..adb2b2867f737d 100644
--- a/packages/e2e-test-utils/src/index.js
+++ b/packages/e2e-test-utils/src/index.js
@@ -27,7 +27,7 @@ export { installPlugin } from './install-plugin';
export { isCurrentURL } from './is-current-url';
export { isInDefaultBlock } from './is-in-default-block';
export { loginUser } from './login-user';
-export { observeFocusLoss } from './observe-focus-loss';
+export { enableFocusLossObservation, disableFocusLossObservation } from './observe-focus-loss';
export { openAllBlockInserterCategories } from './open-all-block-inserter-categories';
export { openDocumentSettingsSidebar } from './open-document-settings-sidebar';
export { openGlobalBlockInserter } from './open-global-block-inserter';
diff --git a/packages/e2e-test-utils/src/observe-focus-loss.js b/packages/e2e-test-utils/src/observe-focus-loss.js
index 13e541de27a8fe..1266d0997c1f29 100644
--- a/packages/e2e-test-utils/src/observe-focus-loss.js
+++ b/packages/e2e-test-utils/src/observe-focus-loss.js
@@ -1,15 +1,30 @@
/**
- * Binds to the document on page load which throws an error if a `focusout`
- * event occurs without a related target (i.e. focus loss).
+ * Adds an event listener to the document which throws an error if there is a
+ * loss of focus.
*/
-export function observeFocusLoss() {
- page.on( 'load', () => {
- page.evaluate( () => {
- document.body.addEventListener( 'focusout', ( event ) => {
- if ( ! event.relatedTarget ) {
- throw new Error( 'Unexpected focus loss' );
- }
- } );
- } );
+export async function enableFocusLossObservation() {
+ await page.evaluate( () => {
+ if ( window._detectFocusLoss ) {
+ document.body.removeEventListener( 'focusout', window._detectFocusLoss );
+ }
+
+ window._detectFocusLoss = ( event ) => {
+ if ( ! event.relatedTarget ) {
+ throw new Error( 'Unexpected focus loss' );
+ }
+ };
+
+ document.body.addEventListener( 'focusout', window._detectFocusLoss );
+ } );
+}
+
+/**
+ * Removes the focus loss listener that `enableFocusLossObservation()` adds.
+ */
+export async function disableFocusLossObservation() {
+ await page.evaluate( () => {
+ if ( window._detectFocusLoss ) {
+ document.body.removeEventListener( 'focusout', window._detectFocusLoss );
+ }
} );
}
diff --git a/packages/e2e-tests/specs/editor/various/nux.test.js b/packages/e2e-tests/specs/editor/various/nux.test.js
index d3ca54af511763..d172f138cb50f2 100644
--- a/packages/e2e-tests/specs/editor/various/nux.test.js
+++ b/packages/e2e-tests/specs/editor/various/nux.test.js
@@ -1,110 +1,105 @@
/**
* WordPress dependencies
*/
-import {
- createNewPost,
- toggleScreenOption,
-} from '@wordpress/e2e-test-utils';
+import { createNewPost, clickOnMoreMenuItem } from '@wordpress/e2e-test-utils';
describe( 'New User Experience (NUX)', () => {
- async function clickAllTips( page ) {
- // Click through all available tips.
- const tips = await getTips( page );
- const numberOfTips = tips.tipIds.length;
+ it( 'should show the guide to first-time users', async () => {
+ let welcomeGuideText, welcomeGuide;
- for ( let i = 1; i < numberOfTips; i++ ) {
- await page.click( '.nux-dot-tip .components-button.is-link' );
- }
-
- return { numberOfTips, tips };
- }
-
- async function getTips( page ) {
- return await page.evaluate( () => {
- return wp.data.select( 'core/nux' ).getAssociatedGuide( 'core/editor.inserter' );
- } );
- }
-
- async function getTipsEnabled( page ) {
- return await page.evaluate( () => {
- return wp.data.select( 'core/nux' ).areTipsEnabled();
- } );
- }
-
- beforeEach( async () => {
+ // Create a new post as a first-time user
await createNewPost( { enableTips: true } );
- } );
-
- it( 'should show tips to a first-time user', async () => {
- const firstTipText = await page.$eval( '.nux-dot-tip', ( element ) => element.innerText );
- expect( firstTipText ).toContain( 'Welcome to the wonderful world of blocks!' );
-
- const [ nextTipButton ] = await page.$x( "//button[contains(text(), 'See next tip')]" );
- await nextTipButton.click();
- const secondTipText = await page.$eval( '.nux-dot-tip', ( element ) => element.innerText );
- expect( secondTipText ).toContain( 'You’ll find more settings for your page and blocks in the sidebar.' );
- } );
-
- it( 'should show "Got it" once all tips have been displayed', async () => {
- await clickAllTips( page );
-
- // Make sure "Got it" button appears on the last tip.
- const gotItButton = await page.$x( "//button[contains(text(), 'Got it')]" );
- expect( gotItButton ).toHaveLength( 1 );
+ // Guide should be on page 1 of 3
+ welcomeGuideText = await page.$eval( '.edit-post-welcome-guide', ( element ) => element.innerText );
+ expect( welcomeGuideText ).toContain( 'Welcome to the Block Editor' );
+
+ // Click on the 'Next' button
+ const [ nextButton ] = await page.$x( '//button[contains(text(), "Next")]' );
+ await nextButton.click();
+
+ // Guide should be on page 2 of 3
+ welcomeGuideText = await page.$eval( '.edit-post-welcome-guide', ( element ) => element.innerText );
+ expect( welcomeGuideText ).toContain( 'Make each block your own' );
+
+ // Click on the 'Previous' button
+ const [ previousButton ] = await page.$x( '//button[contains(text(), "Previous")]' );
+ await previousButton.click();
+
+ // Guide should be on page 1 of 3
+ welcomeGuideText = await page.$eval( '.edit-post-welcome-guide', ( element ) => element.innerText );
+ expect( welcomeGuideText ).toContain( 'Welcome to the Block Editor' );
+
+ // Press the button for Page 2
+ await page.click( 'button[aria-label="Page 2 of 3"]' );
+
+ // Press the right arrow key
+ await page.keyboard.press( 'ArrowRight' );
+
+ // Guide should be on page 3 of 3
+ welcomeGuideText = await page.$eval( '.edit-post-welcome-guide', ( element ) => element.innerText );
+ expect( welcomeGuideText ).toContain( 'Get to know the Block Library' );
+
+ // Click on the *visible* 'Get started' button. There are two in the DOM
+ // but only one is shown depending on viewport size
+ let getStartedButton;
+ for ( const buttonHandle of await page.$x( '//button[contains(text(), "Get started")]' ) ) {
+ if (
+ await page.evaluate( ( button ) => button.style.display !== 'none', buttonHandle )
+ ) {
+ getStartedButton = buttonHandle;
+ }
+ }
+ await getStartedButton.click();
- // Click the "Got it button".
- await page.click( '.nux-dot-tip .components-button.is-link' );
+ // Guide should be closed
+ welcomeGuide = await page.$( '.edit-post-welcome-guide' );
+ expect( welcomeGuide ).toBeNull();
- // Verify no more tips are visible on the page.
- const nuxTipElements = await page.$$( '.nux-dot-tip' );
- expect( nuxTipElements ).toHaveLength( 0 );
+ // Reload the editor
+ await page.reload();
- // Tips should not be marked as disabled, but when the user has seen all
- // of the available tips, they will not appear.
- const areTipsEnabled = await getTipsEnabled( page );
- expect( areTipsEnabled ).toEqual( true );
+ // Guide should be closed
+ welcomeGuide = await page.$( '.edit-post-welcome-guide' );
+ expect( welcomeGuide ).toBeNull();
} );
- it( 'should hide and disable tips if "disable tips" button is clicked', async () => {
- await page.click( '.nux-dot-tip__disable' );
+ it( 'should not show the welcome guide again if it is dismissed', async () => {
+ let welcomeGuide;
+
+ // Create a new post as a first-time user
+ await createNewPost( { enableTips: true } );
- // Verify no more tips are visible on the page.
- let nuxTipElements = await page.$$( '.nux-dot-tip' );
- expect( nuxTipElements ).toHaveLength( 0 );
+ // Guide should be open
+ welcomeGuide = await page.$( '.edit-post-welcome-guide' );
+ expect( welcomeGuide ).not.toBeNull();
- // We should be disabling the tips so they don't appear again.
- const areTipsEnabled = await getTipsEnabled( page );
- expect( areTipsEnabled ).toEqual( false );
+ // Close the guide
+ await page.click( 'button[aria-label="Close dialog"]' );
- // Refresh the page; tips should not show because they were disabled.
+ // Reload the editor
await page.reload();
- nuxTipElements = await page.$$( '.nux-dot-tip' );
- expect( nuxTipElements ).toHaveLength( 0 );
+ // Guide should be closed
+ welcomeGuide = await page.$( '.edit-post-welcome-guide' );
+ expect( welcomeGuide ).toBeNull();
} );
- it( 'should enable tips when the "Tips" option is toggled on', async () => {
- // Start by disabling tips.
- await page.click( '.nux-dot-tip__disable' );
-
- // Verify no more tips are visible on the page.
- let nuxTipElements = await page.$$( '.nux-dot-tip' );
- expect( nuxTipElements ).toHaveLength( 0 );
+ it( 'should show the welcome guide if it is manually opened', async () => {
+ let welcomeGuide;
- // Tips should be disabled in localStorage as well.
- let areTipsEnabled = await getTipsEnabled( page );
- expect( areTipsEnabled ).toEqual( false );
+ // Create a new post as a returning user
+ await createNewPost();
- // Toggle the 'Tips' option to enable.
- await toggleScreenOption( 'Tips' );
+ // Guide should be closed
+ welcomeGuide = await page.$( '.edit-post-welcome-guide' );
+ expect( welcomeGuide ).toBeNull();
- // Tips should once again appear.
- nuxTipElements = await page.$$( '.nux-dot-tip' );
- expect( nuxTipElements ).toHaveLength( 1 );
+ // Manually open the guide
+ await clickOnMoreMenuItem( 'Welcome Guide' );
- // Tips should be enabled in localStorage as well.
- areTipsEnabled = await getTipsEnabled( page );
- expect( areTipsEnabled ).toEqual( true );
+ // Guide should be open
+ welcomeGuide = await page.$( '.edit-post-welcome-guide' );
+ expect( welcomeGuide ).not.toBeNull();
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js
index d0768f916f0f63..90dc69789de644 100644
--- a/packages/e2e-tests/specs/editor/various/sidebar.test.js
+++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js
@@ -5,7 +5,8 @@ import {
clearLocalStorage,
createNewPost,
findSidebarPanelWithTitle,
- observeFocusLoss,
+ enableFocusLossObservation,
+ disableFocusLossObservation,
openDocumentSettingsSidebar,
pressKeyWithModifier,
setBrowserViewport,
@@ -16,14 +17,15 @@ const ACTIVE_SIDEBAR_TAB_SELECTOR = '.edit-post-sidebar__panel-tab.is-active';
const ACTIVE_SIDEBAR_BUTTON_TEXT = 'Document';
describe( 'Sidebar', () => {
- beforeAll( () => {
- observeFocusLoss();
+ afterEach( () => {
+ disableFocusLossObservation();
} );
it( 'should have sidebar visible at the start with document sidebar active on desktop', async () => {
await setBrowserViewport( 'large' );
await clearLocalStorage();
await createNewPost();
+ await enableFocusLossObservation();
const { nodesCount, content, height, width } = await page.$$eval( ACTIVE_SIDEBAR_TAB_SELECTOR, ( nodes ) => {
const firstNode = nodes[ 0 ];
return {
@@ -49,6 +51,7 @@ describe( 'Sidebar', () => {
await setBrowserViewport( 'small' );
await clearLocalStorage();
await createNewPost();
+ await enableFocusLossObservation();
const sidebar = await page.$( SIDEBAR_SELECTOR );
expect( sidebar ).toBeNull();
} );
@@ -57,6 +60,7 @@ describe( 'Sidebar', () => {
await setBrowserViewport( 'large' );
await clearLocalStorage();
await createNewPost();
+ await enableFocusLossObservation();
const sidebars = await page.$$( SIDEBAR_SELECTOR );
expect( sidebars ).toHaveLength( 1 );
@@ -72,6 +76,7 @@ describe( 'Sidebar', () => {
await setBrowserViewport( 'large' );
await clearLocalStorage();
await createNewPost();
+ await enableFocusLossObservation();
await setBrowserViewport( 'small' );
const sidebarsMobile = await page.$$( SIDEBAR_SELECTOR );
@@ -85,6 +90,7 @@ describe( 'Sidebar', () => {
it( 'should preserve tab order while changing active tab', async () => {
await createNewPost();
+ await enableFocusLossObservation();
// Region navigate to Sidebar.
await pressKeyWithModifier( 'ctrl', '`' );
@@ -112,6 +118,7 @@ describe( 'Sidebar', () => {
it( 'should be possible to programmatically remove Document Settings panels', async () => {
await createNewPost();
+ await enableFocusLossObservation();
await openDocumentSettingsSidebar();
diff --git a/packages/edit-post/src/components/editor-initialization/index.js b/packages/edit-post/src/components/editor-initialization/index.js
index a1327684f06b36..a4695fa01aa3b4 100644
--- a/packages/edit-post/src/components/editor-initialization/index.js
+++ b/packages/edit-post/src/components/editor-initialization/index.js
@@ -1,9 +1,3 @@
-/**
- * WordPress dependencies
- */
-import { useEffect } from '@wordpress/element';
-import { useDispatch } from '@wordpress/data';
-
/**
* Internal dependencies
*/
@@ -24,14 +18,5 @@ export default function( { postId } ) {
useBlockSelectionListener( postId );
useAdjustSidebarListener( postId );
useUpdatePostLinkListener( postId );
- const { triggerGuide } = useDispatch( 'core/nux' );
- useEffect( () => {
- triggerGuide( [
- 'core/editor.inserter',
- 'core/editor.settings',
- 'core/editor.preview',
- 'core/editor.publish',
- ] );
- }, [ triggerGuide ] );
return null;
}
diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js
index 92b877e7626cfc..ad6612d7a54195 100644
--- a/packages/edit-post/src/components/header/header-toolbar/index.js
+++ b/packages/edit-post/src/components/header/header-toolbar/index.js
@@ -4,7 +4,6 @@
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import { withViewportMatch } from '@wordpress/viewport';
-import { DotTip } from '@wordpress/nux';
import { __ } from '@wordpress/i18n';
import {
Inserter,
@@ -31,12 +30,7 @@ function HeaderToolbar( { hasFixedToolbar, isLargeViewport, showInserter, isText
className="edit-post-header-toolbar"
aria-label={ toolbarAriaLabel }
>
-
-
-
- { __( 'Welcome to the wonderful world of blocks! Click the “+” (“Add block”) button to add a new block. There are blocks available for all kinds of content: you can insert text, headings, images, lists, and lots more!' ) }
-
-
+
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index 8515b6ba372a00..52d9245be0a157 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -9,7 +9,6 @@ import {
} from '@wordpress/editor';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
-import { DotTip } from '@wordpress/nux';
/**
* Internal dependencies
@@ -57,19 +56,14 @@ function Header( {
forceIsDirty={ hasActiveMetaboxes }
forceIsSaving={ isSaving }
/>
-
-
-
- { __( 'You’ll find more settings for your page and blocks in the sidebar. Click the cog icon to toggle the sidebar open and closed.' ) }
-
-