From 73e4691addd858ecd27b5fcd76c23d9f2b683689 Mon Sep 17 00:00:00 2001
From: Robert Anderson
Date: Thu, 31 May 2018 16:10:02 +1000
Subject: [PATCH 01/21] Implement New User Guide
Adds a series of floating modal 'tips' which introduces a new user to
the editor. These tips can be advanced one by one or dismissed
alltogether. If dismissed, they will never show again.
---
.eslintrc.js | 4 +
edit-post/assets/stylesheets/_z-index.scss | 3 +
edit-post/components/header/index.js | 7 +-
edit-post/index.js | 8 ++
editor/components/block-list/block.js | 14 ++-
.../default-block-appender/index.js | 22 ++--
.../default-block-appender/style.scss | 14 +--
.../test/__snapshots__/index.js.snap | 24 +++-
.../components/post-preview-button/index.js | 6 +-
.../components/post-publish-panel/toggle.js | 4 +
lib/client-assets.php | 18 ++-
nux/README.md | 86 +++++++++++++++
nux/components/dot-tip/README.md | 31 ++++++
nux/components/dot-tip/index.js | 92 ++++++++++++++++
nux/components/dot-tip/style.scss | 74 +++++++++++++
.../dot-tip/test/__snapshots__/index.js.snap | 32 ++++++
nux/components/dot-tip/test/index.js | 51 +++++++++
nux/index.js | 6 +
nux/store/actions.js | 41 +++++++
nux/store/index.js | 24 ++++
nux/store/reducer.js | 41 +++++++
nux/store/selectors.js | 57 ++++++++++
nux/store/test/actions.js | 32 ++++++
nux/store/test/reducer.js | 51 +++++++++
nux/store/test/selectors.js | 104 ++++++++++++++++++
test/e2e/support/utils.js | 5 +
test/unit/jest.config.json | 4 +-
webpack.config.js | 1 +
28 files changed, 832 insertions(+), 24 deletions(-)
create mode 100644 nux/README.md
create mode 100644 nux/components/dot-tip/README.md
create mode 100644 nux/components/dot-tip/index.js
create mode 100644 nux/components/dot-tip/style.scss
create mode 100644 nux/components/dot-tip/test/__snapshots__/index.js.snap
create mode 100644 nux/components/dot-tip/test/index.js
create mode 100644 nux/index.js
create mode 100644 nux/store/actions.js
create mode 100644 nux/store/index.js
create mode 100644 nux/store/reducer.js
create mode 100644 nux/store/selectors.js
create mode 100644 nux/store/test/actions.js
create mode 100644 nux/store/test/reducer.js
create mode 100644 nux/store/test/selectors.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 198d6dbf0b06ad..ce9869d8d79109 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -98,6 +98,10 @@ module.exports = {
"selector": "ImportDeclaration[source.value=/^core-blocks$/]",
"message": "Use @wordpress/core-blocks as import path instead."
},
+ {
+ "selector": "ImportDeclaration[source.value=/^nux$/]",
+ "message": "Use @wordpress/nux as import path instead."
+ },
{
selector: 'CallExpression[callee.name="deprecated"] Property[key.name="version"][value.value=/' + majorMinorRegExp + '/]',
message: 'Deprecated functions must be removed before releasing this version.',
diff --git a/edit-post/assets/stylesheets/_z-index.scss b/edit-post/assets/stylesheets/_z-index.scss
index 180c5a94774104..26e526aa019b57 100644
--- a/edit-post/assets/stylesheets/_z-index.scss
+++ b/edit-post/assets/stylesheets/_z-index.scss
@@ -77,6 +77,9 @@ $z-layers: (
'.components-autocomplete__results': 1000000,
'.skip-to-selected-block': 100000,
+
+ // Show NUX tips above popovers, wp-admin menus, submenus, and sidebar:
+ '.nux-dot-tip': 1000001,
);
@function z-index( $key ) {
diff --git a/edit-post/components/header/index.js b/edit-post/components/header/index.js
index fdaa63dc777a67..72066bfdfc3fa4 100644
--- a/edit-post/components/header/index.js
+++ b/edit-post/components/header/index.js
@@ -10,6 +10,7 @@ import {
} from '@wordpress/editor';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/element';
+import { DotTip } from '@wordpress/nux';
/**
* Internal dependencies
@@ -57,7 +58,11 @@ function Header( {
onClick={ toggleGeneralSidebar }
isToggled={ isEditorSidebarOpened }
aria-expanded={ isEditorSidebarOpened }
- />
+ >
+
+ { __( 'You’ll find more settings for your page and blocks in the sidebar. Click ‘Settings’ to open it.' ) }
+
+
diff --git a/edit-post/index.js b/edit-post/index.js
index fcdd285684dbd2..b77b0aa2a9a4bd 100644
--- a/edit-post/index.js
+++ b/edit-post/index.js
@@ -3,6 +3,7 @@
*/
import { registerCoreBlocks } from '@wordpress/core-blocks';
import { render, unmountComponentAtNode } from '@wordpress/element';
+import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
@@ -73,6 +74,13 @@ export function initializeEditor( id, postType, postId, settings, overridePost )
registerCoreBlocks();
+ dispatch( 'core/nux' ).triggerGuide( [
+ 'core/editor.inserter',
+ 'core/editor.settings',
+ 'core/editor.preview',
+ 'core/editor.publish',
+ ] );
+
render(
,
target
diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js
index 70eb8f82088c9e..5e9db9ccec6b7f 100644
--- a/editor/components/block-list/block.js
+++ b/editor/components/block-list/block.js
@@ -28,6 +28,7 @@ import { withFilters } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { withDispatch, withSelect } from '@wordpress/data';
import { withViewportMatch } from '@wordpress/viewport';
+import { DotTip } from '@wordpress/nux';
/**
* Internal dependencies
@@ -414,6 +415,7 @@ export class BlockListBlock extends Component {
isEmptyDefaultBlock,
isPreviousBlockADefaultEmptyBlock,
hasSelectedInnerBlock,
+ hasTip,
} = this.props;
const isHovered = this.state.isHovered && ! isMultiSelecting;
const { name: blockName, isValid } = block;
@@ -426,7 +428,7 @@ export class BlockListBlock extends Component {
// If the block is selected and we're typing the block should not appear.
// Empty paragraph blocks should always show up as unselected.
const showEmptyBlockSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock;
- const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock;
+ const showSideInserter = ( isSelected || isHovered || hasTip ) && isEmptyDefaultBlock;
const shouldAppearSelected = ! showSideInserter && ( isSelected || hasSelectedInnerBlock ) && ! isTypingWithinBlock;
// We render block movers and block settings to keep them tabbale even if hidden
const shouldRenderMovers = ( isSelected || hoverArea === 'left' ) && ! showEmptyBlockSideInserter && ! isMultiSelecting && ! isMultiSelected && ! isTypingWithinBlock;
@@ -595,7 +597,11 @@ export class BlockListBlock extends Component {
+ >
+
+ { __( 'Welcome to the wonderful world of blocks! Click ‘Add block’ to insert different kinds of content—text, images, quotes, video, lists, and much more.' ) }
+
+
) }
@@ -623,6 +629,9 @@ const applyWithSelect = withSelect( ( select, { uid, rootUID } ) => {
getEditorSettings,
hasSelectedInnerBlock,
} = select( 'core/editor' );
+
+ const { isTipVisible } = select( 'core/nux' );
+
const isSelected = isBlockSelected( uid );
const isParentOfSelectedBlock = hasSelectedInnerBlock( uid );
const { templateLock, hasFixedToolbar } = getEditorSettings();
@@ -651,6 +660,7 @@ const applyWithSelect = withSelect( ( select, { uid, rootUID } ) => {
block,
isSelected,
hasFixedToolbar,
+ hasTip: isTipVisible( 'core/editor.inserter' ),
};
} );
diff --git a/editor/components/default-block-appender/index.js b/editor/components/default-block-appender/index.js
index d2de14c4876b9b..ad5c4e14d1d7d4 100644
--- a/editor/components/default-block-appender/index.js
+++ b/editor/components/default-block-appender/index.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import classnames from 'classnames';
import { get } from 'lodash';
/**
@@ -11,6 +12,7 @@ import { compose } from '@wordpress/element';
import { getDefaultBlockName } from '@wordpress/blocks';
import { decodeEntities } from '@wordpress/utils';
import { withSelect, withDispatch } from '@wordpress/data';
+import { DotTip } from '@wordpress/nux';
/**
* Internal dependencies
@@ -28,6 +30,7 @@ export function DefaultBlockAppender( {
placeholder,
layout,
rootUID,
+ hasTip,
} ) {
if ( isLocked || ! isVisible ) {
return null;
@@ -38,7 +41,9 @@ export function DefaultBlockAppender( {
return (
+ className={ classnames( 'editor-default-block-appender', {
+ 'has-tip': hasTip,
+ } ) }>
-
+
+
+ { __( 'Welcome to the wonderful world of blocks! Click ‘Add block’ to insert different kinds of content—text, images, quotes, video, lists, and much more.' ) }
+
+
);
}
export default compose(
withSelect( ( select, ownProps ) => {
- const {
- getBlockCount,
- getBlock,
- getEditorSettings,
- } = select( 'core/editor' );
+ const { getBlockCount, getBlock, getEditorSettings } = select( 'core/editor' );
+ const { isTipVisible } = select( 'core/nux' );
+
const isEmpty = ! getBlockCount( ownProps.rootUID );
const lastBlock = getBlock( ownProps.lastBlockUID );
const isLastBlockDefault = get( lastBlock, [ 'name' ] ) === getDefaultBlockName();
@@ -73,6 +80,7 @@ export default compose(
showPrompt: isEmpty,
isLocked: !! templateLock,
placeholder: bodyPlaceholder,
+ hasTip: isTipVisible( 'core/editor.inserter' ),
};
} ),
withDispatch( ( dispatch, ownProps ) => {
diff --git a/editor/components/default-block-appender/style.scss b/editor/components/default-block-appender/style.scss
index 8a20f01ec63c03..68c229dfaa9ccb 100644
--- a/editor/components/default-block-appender/style.scss
+++ b/editor/components/default-block-appender/style.scss
@@ -45,17 +45,17 @@ $empty-paragraph-height: $text-editor-font-size * 4;
}
}
- // Don't show inserter until mousing
- .editor-inserter__toggle:not( [aria-expanded="true"] ) {
- opacity: 0;
+ &:hover .editor-inserter-with-shortcuts {
+ opacity: 1;
}
- &:hover {
- .editor-inserter-with-shortcuts {
- opacity: 1;
+ // Show the inserter if mousing over or there is a tip
+ &:not( .has-tip ) {
+ .editor-inserter__toggle:not( [aria-expanded="true"] ) {
+ opacity: 0;
}
- .editor-inserter__toggle {
+ &:hover .editor-inserter__toggle {
opacity: 1;
}
}
diff --git a/editor/components/default-block-appender/test/__snapshots__/index.js.snap b/editor/components/default-block-appender/test/__snapshots__/index.js.snap
index aa218fa220a27a..9fee0c9ce14b73 100644
--- a/editor/components/default-block-appender/test/__snapshots__/index.js.snap
+++ b/editor/components/default-block-appender/test/__snapshots__/index.js.snap
@@ -38,7 +38,13 @@ exports[`DefaultBlockAppender should append a default block when input focused 1
+ >
+
+ Welcome to the wonderful world of blocks! Click ‘Add block’ to insert different kinds of content—text, images, quotes, video, lists, and much more.
+
+
`;
@@ -62,7 +68,13 @@ exports[`DefaultBlockAppender should match snapshot 1`] = `
+ >
+
+ Welcome to the wonderful world of blocks! Click ‘Add block’ to insert different kinds of content—text, images, quotes, video, lists, and much more.
+
+
`;
@@ -86,6 +98,12 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = `
+ >
+
+ Welcome to the wonderful world of blocks! Click ‘Add block’ to insert different kinds of content—text, images, quotes, video, lists, and much more.
+
+
`;
diff --git a/editor/components/post-preview-button/index.js b/editor/components/post-preview-button/index.js
index 00244ec04afd16..acb2ae41a3fac2 100644
--- a/editor/components/post-preview-button/index.js
+++ b/editor/components/post-preview-button/index.js
@@ -8,8 +8,9 @@ import { get } from 'lodash';
*/
import { Component, compose } from '@wordpress/element';
import { Button, ifCondition } from '@wordpress/components';
-import { _x } from '@wordpress/i18n';
+import { __, _x } from '@wordpress/i18n';
import { withSelect, withDispatch } from '@wordpress/data';
+import { DotTip } from '@wordpress/nux';
export class PostPreviewButton extends Component {
constructor() {
@@ -107,6 +108,9 @@ export class PostPreviewButton extends Component {
disabled={ ! isSaveable }
>
{ _x( 'Preview', 'imperative verb' ) }
+
+ { __( 'Click ‘Preview’ to load a preview of this page, so you can make sure you’re happy with your blocks.' ) }
+
);
}
diff --git a/editor/components/post-publish-panel/toggle.js b/editor/components/post-publish-panel/toggle.js
index 5345cb4759c772..736b08f67a48a3 100644
--- a/editor/components/post-publish-panel/toggle.js
+++ b/editor/components/post-publish-panel/toggle.js
@@ -10,6 +10,7 @@ import { Button } from '@wordpress/components';
import { compose } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withSelect } from '@wordpress/data';
+import { DotTip } from '@wordpress/nux';
/**
* Internal Dependencies
@@ -50,6 +51,9 @@ function PostPublishPanelToggle( {
isBusy={ isSaving && isPublished }
>
{ isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ) }
+
+ { __( 'Finished writing? That’s great, let’s get this published right now. Just click ‘Publish’ and you’re good to go.' ) }
+
);
}
diff --git a/lib/client-assets.php b/lib/client-assets.php
index 98648c0ce961a3..d4df0564ff4658 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -265,6 +265,13 @@ function gutenberg_register_scripts_and_styles() {
filemtime( gutenberg_dir_path() . 'build/core-blocks/index.js' ),
true
);
+ wp_register_script(
+ 'wp-nux',
+ gutenberg_url( 'build/nux/index.js' ),
+ array( 'wp-element', 'wp-components', 'wp-data', 'wp-i18n', 'lodash' ),
+ filemtime( gutenberg_dir_path() . 'build/nux/index.js' ),
+ true
+ );
// Loading the old editor and its config to ensure the classic block works as expected.
wp_add_inline_script(
'editor', 'window.wp.oldEditor = window.wp.editor;', 'after'
@@ -361,6 +368,7 @@ function gutenberg_register_scripts_and_styles() {
'tinymce-latest-lists',
'tinymce-latest-paste',
'tinymce-latest-table',
+ 'wp-nux',
),
filemtime( gutenberg_dir_path() . 'build/editor/index.js' )
);
@@ -408,7 +416,7 @@ function gutenberg_register_scripts_and_styles() {
wp_register_style(
'wp-editor',
gutenberg_url( 'build/editor/style.css' ),
- array( 'wp-components', 'wp-editor-font' ),
+ array( 'wp-components', 'wp-editor-font', 'wp-nux' ),
filemtime( gutenberg_dir_path() . 'build/editor/style.css' )
);
wp_style_add_data( 'wp-editor', 'rtl', 'replace' );
@@ -450,6 +458,14 @@ function gutenberg_register_scripts_and_styles() {
);
wp_style_add_data( 'wp-edit-blocks', 'rtl', 'replace' );
+ wp_register_style(
+ 'wp-nux',
+ gutenberg_url( 'build/nux/style.css' ),
+ array( 'wp-components' ),
+ filemtime( gutenberg_dir_path() . 'build/nux/style.css' )
+ );
+ wp_style_add_data( 'wp-nux', 'rtl', 'replace' );
+
wp_register_style(
'wp-core-blocks-theme',
gutenberg_url( 'build/core-blocks/theme.css' ),
diff --git a/nux/README.md b/nux/README.md
new file mode 100644
index 00000000000000..22630698c1f9b9
--- /dev/null
+++ b/nux/README.md
@@ -0,0 +1,86 @@
+NUX (New User eXperience)
+=========================
+
+The NUX module exposes components, and `wp.data` methods useful for onboarding a new user to the WordPress admin interface. Specifically, it exposes _tips_ and _guides_.
+
+A _tip_ is a component that points to an element in the UI and contains text that explains the element's functionality. The user can dismiss a tip, in which case it never shows again. The user can also disable tips entirely. Information about tips is persisted between sessions using `localStorage`.
+
+A _guide_ allows a series of of tips to be presented to the user one by one. When a user dismisses a tip that is in a guide, the next tip in the guide is shown.
+
+## DotTip
+
+`DotTip` is a React component that renders a single _tip_ on the screen. The tip will point to the React element that `DotTip` is nested within. Each tip is uniquely identified by a string passed to `id`.
+
+See [the component's README][dot-tip-readme] for more information.
+
+[dot-tip-readme]: https://github.com/WordPress/gutenberg/tree/master/nux/components/dot-tip/README.md
+
+```jsx
+
+}
+```
+
+## Determining if a tip is visible
+
+You can programmatically determine if a tip is visible using the `isTipVisible` select method.
+
+```jsx
+const isVisible = select( 'core/nux' ).isTipVisible( 'acme/add-to-cart' );
+console.log( isVisible ); // true or false
+```
+
+## Manually dismissing a tip
+
+`dismissTip` is a dispatch method that allows you to programmatically dismiss a tip.
+
+```jsx
+
+```
+
+## Manually disabling tips
+
+`disableTips` is a dispatch method that allows you to programmatically disable all tips.
+
+```jsx
+
+```
+
+## Triggering a guide
+
+You can group a series of tips into a guide by calling the `triggerGuide` dispatch method. The given tips will then appear one by one.
+
+A tip cannot be added to more than one guide.
+
+```jsx
+domReady(() => {
+ dispatch( 'core/nux' ).triggerGuide( [ 'acme/product-info', 'acme/add-to-cart', 'acme/checkout' ] );
+} );
+```
+
+## Getting information about a guide
+
+`getAssociatedGuide` is a select method that returns useful information about the state of the guide that a tip is associated with.
+
+```jsx
+const guide = select( 'core/nux' ).getAssociatedGuide( 'acme/add-to-cart' );
+console.log( 'Tips in this guide:', guide.tipIDs );
+console.log( 'Currently showing:', guide.currentTipID );
+console.log( 'Next to show:', guide.nextTipID );
+```
diff --git a/nux/components/dot-tip/README.md b/nux/components/dot-tip/README.md
new file mode 100644
index 00000000000000..3b830a259d8086
--- /dev/null
+++ b/nux/components/dot-tip/README.md
@@ -0,0 +1,31 @@
+DotTip
+========
+
+`DotTip` is a React component that renders a single _tip_ on the screen. The tip will point to the React element that `DotTip` is nested within. Each tip is uniquely identified by a string passed to `id`.
+
+## Usage
+
+```jsx
+
+}
+```
+
+## Props
+
+The component accepts the following props:
+
+### id
+
+An identifier that uniquely identifies the tip.
+
+- Type: `string`
+- Required: Yes
+
+### children
+
+Any React element or elements can be passed as children. They will be rendered within the tip bubble.
diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js
new file mode 100644
index 00000000000000..a0575edd31713e
--- /dev/null
+++ b/nux/components/dot-tip/index.js
@@ -0,0 +1,92 @@
+/**
+ * External dependencies
+ */
+import { defer, partial } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, createRef, Fragment, compose } from '@wordpress/element';
+import { Popover, Button, IconButton } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { withSelect, withDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+export class DotTip extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.popoverRef = createRef();
+ }
+
+ componentDidMount() {
+ if ( this.props.isVisible ) {
+ // Fix the tip not appearing next to the inserter toggle by forcing Popover
+ // to recalculate its size and position on the next frame
+ defer( () => {
+ const popover = this.popoverRef.current;
+ const popoverSize = popover.updatePopoverSize();
+ popover.computePopoverPosition( popoverSize );
+ } );
+ }
+ }
+
+ render() {
+ const { children, isVisible, hasNextTip, onDismiss, onDisable } = this.props;
+
+ if ( ! isVisible ) {
+ return null;
+ }
+
+ return (
+
+ event.stopPropagation() }
+ >
+
It looks like you’re writing a letter. Would you like help?
diff --git a/nux/components/dot-tip/test/index.js b/nux/components/dot-tip/test/index.js
index 31449168482362..2f8bf4af2948a0 100644
--- a/nux/components/dot-tip/test/index.js
+++ b/nux/components/dot-tip/test/index.js
@@ -38,14 +38,14 @@ describe( 'DotTip', () => {
expect( onDismiss ).toHaveBeenCalled();
} );
- it( 'should call onDisable when the disable button is clicked', () => {
- const onDisable = jest.fn();
+ it( 'should call onDismiss when the X button is clicked', () => {
+ const onDismiss = jest.fn();
const wrapper = shallow(
-
+
It looks like you’re writing a letter. Would you like help?
);
- wrapper.find( 'IconButton[label="Disable tips"]' ).first().simulate( 'click' );
- expect( onDisable ).toHaveBeenCalled();
+ wrapper.find( 'IconButton[label="Dismiss tip"]' ).first().simulate( 'click' );
+ expect( onDismiss ).toHaveBeenCalled();
} );
} );
From ff096434da8b7b07e9646402b63444327186b691 Mon Sep 17 00:00:00 2001
From: Robert Anderson
Date: Fri, 1 Jun 2018 13:04:39 +1000
Subject: [PATCH 20/21] Fix DotTip focus issue
The first DotTip should receive focus when the page loads, and focus
should not be on the X button.
Also improve the note explaining our temporary position workaround.
---
nux/components/dot-tip/index.js | 20 +++++++++++--------
.../dot-tip/test/__snapshots__/index.js.snap | 10 +++++-----
2 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js
index fcd9c2014511f1..8302a326626284 100644
--- a/nux/components/dot-tip/index.js
+++ b/nux/components/dot-tip/index.js
@@ -25,12 +25,16 @@ export class DotTip extends Component {
componentDidMount() {
if ( this.props.isVisible ) {
- // Fix the tip not appearing next to the inserter toggle by forcing Popover
- // to recalculate its size and position on the next frame
+ // Force the popover to recalculate its position on the next frame. This is a
+ // Temporary workaround to fix the tip not appearing next to the inserter
+ // toggle on page load. This happens because the popover calculates its
+ // position before is made visible, resulting in the position
+ // being too high on the page.
defer( () => {
const popover = this.popoverRef.current;
const popoverSize = popover.updatePopoverSize();
popover.computePopoverPosition( popoverSize );
+ popover.focus();
} );
}
}
@@ -54,18 +58,18 @@ export class DotTip extends Component {
onClose={ onDismiss }
onClick={ ( event ) => event.stopPropagation() }
>
-