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() } + > +

{ children }

+

+ +

+ +
+
+ ); + } +} + +export default compose( + withSelect( ( select, { id } ) => { + const { isTipVisible, getAssociatedGuide } = select( 'core/nux' ); + const associatedGuide = getAssociatedGuide( id ); + return { + isVisible: isTipVisible( id ), + hasNextTip: !! ( associatedGuide && associatedGuide.nextTipID ), + }; + } ), + withDispatch( ( dispatch, { id } ) => { + const { dismissTip, disableTips } = dispatch( 'core/nux' ); + return { + onDismiss: partial( dismissTip, id ), + onDisable: disableTips, + }; + } ), +)( DotTip ); diff --git a/nux/components/dot-tip/style.scss b/nux/components/dot-tip/style.scss new file mode 100644 index 00000000000000..ee40c63f90b96a --- /dev/null +++ b/nux/components/dot-tip/style.scss @@ -0,0 +1,74 @@ +$dot-size: 8px; // Size of the indicator dot +$dot-scale: 3; // How much the pulse animation should scale up by in size + +.nux-dot-tip { + &:before, + &:after { + border-radius: 100%; + content: ' '; + pointer-events: none; + position: absolute; + } + + &:before { + animation: nux-pulse 1.6s infinite cubic-bezier( 0.17, 0.67, 0.92, 0.62 ); + background: rgba( $blue-medium-800, 0.9 ); + height: $dot-size * $dot-scale; + left: -( $dot-size * $dot-scale ) / 2; + top: -( $dot-size * $dot-scale ) / 2; + transform: scale( ( 1 / $dot-scale ) ); + width: $dot-size * $dot-scale + } + + &:after { + background: $blue-medium-800; + height: $dot-size; + left: -$dot-size / 2; + top: -$dot-size / 2; + width: $dot-size; + } + + @keyframes nux-pulse { + 100% { + background: rgba( $blue-medium-800, 0 ); + transform: scale( 1 ); + } + } + + .components-popover__content { + padding: 5px ( 36px + 5px ) 5px 10px; + width: 350px; + + @include break-small { + width: 450px; + } + + .nux-dot-tip__disable { + position: absolute; + right: 0; + top: 0; + } + } + + &.is-left { + margin-left: -25px; + + .components-popover__content { + margin-right: 20px; + } + } + + &.is-right { + margin-left: 25px; + + .components-popover__content { + margin-left: 20px; + } + } +} + +// Need extra specificity to override the z-index set by .component-popover +.nux-dot-tip, +.nux-dot-tip:not( .is-mobile ).is-bottom { + z-index: z-index( '.nux-dot-tip' ); +} diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..7e5747db6e87c5 --- /dev/null +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DotTip should render correctly 1`] = ` + + +

+ 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 new file mode 100644 index 00000000000000..20eb849ecfba68 --- /dev/null +++ b/nux/components/dot-tip/test/index.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { DotTip } from '..'; + +describe( 'DotTip', () => { + it( 'should not render anything if invisible', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + expect( wrapper.isEmptyRender() ).toBe( true ); + } ); + + it( 'should render correctly', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'should call onDismiss when the dismiss button is clicked', () => { + const onDismiss = jest.fn(); + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + wrapper.find( 'Button[children="Got it"]' ).first().simulate( 'click' ); + expect( onDismiss ).toHaveBeenCalled(); + } ); + + it( 'should call onDisable when the disable button is clicked', () => { + const onDisable = jest.fn(); + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + wrapper.find( 'IconButton[label="Disable guide"]' ).first().simulate( 'click' ); + expect( onDisable ).toHaveBeenCalled(); + } ); +} ); diff --git a/nux/index.js b/nux/index.js new file mode 100644 index 00000000000000..a11d17bc96961a --- /dev/null +++ b/nux/index.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import './store'; + +export { default as DotTip } from './components/dot-tip'; diff --git a/nux/store/actions.js b/nux/store/actions.js new file mode 100644 index 00000000000000..a0918e86909af0 --- /dev/null +++ b/nux/store/actions.js @@ -0,0 +1,41 @@ +/** + * Returns an action object that, when dispatched, presents a guide that takes + * the user through a series of tips step by step. + * + * @param {string[]} tipIDs Which tips to show in the guide. + * + * @return {Object} Action object. + */ +export function triggerGuide( tipIDs ) { + return { + type: 'TRIGGER_GUIDE', + tipIDs, + }; +} + +/** + * Returns an action object that, when dispatched, dismisses the given tip. A + * dismissed tip will not show again. + * + * @param {string} id The tip to dismiss. + * + * @return {Object} Action object. + */ +export function dismissTip( id ) { + return { + type: 'DISMISS_TIP', + id, + }; +} + +/** + * Returns an action object that, when dispatched, prevents all tips from + * showing again. + * + * @return {Object} Action object. + */ +export function disableTips() { + return { + type: 'DISABLE_TIPS', + }; +} diff --git a/nux/store/index.js b/nux/store/index.js new file mode 100644 index 00000000000000..3d387c76f8f5fc --- /dev/null +++ b/nux/store/index.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { registerStore, withRehydration, loadAndPersist } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; + +const REDUCER_KEY = 'preferences'; +const STORAGE_KEY = `GUTENBERG_NUX_${ window.userSettings.uid }`; + +const store = registerStore( 'core/nux', { + reducer: withRehydration( reducer, REDUCER_KEY, STORAGE_KEY ), + actions, + selectors, +} ); + +loadAndPersist( store, reducer, REDUCER_KEY, STORAGE_KEY ); + +export default store; diff --git a/nux/store/reducer.js b/nux/store/reducer.js new file mode 100644 index 00000000000000..c1dec6135b133e --- /dev/null +++ b/nux/store/reducer.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +export function guides( state = [], action ) { + switch ( action.type ) { + case 'TRIGGER_GUIDE': + return [ + ...state, + action.tipIDs, + ]; + } + + return state; +} + +export function areTipsDisabled( state = false, action ) { + switch ( action.type ) { + case 'DISABLE_TIPS': + return true; + } + + return state; +} + +export function dismissedTips( state = {}, action ) { + switch ( action.type ) { + case 'DISMISS_TIP': + return { + ...state, + [ action.id ]: true, + }; + } + + return state; +} + +const preferences = combineReducers( { areTipsDisabled, dismissedTips } ); + +export default combineReducers( { guides, preferences } ); diff --git a/nux/store/selectors.js b/nux/store/selectors.js new file mode 100644 index 00000000000000..4271c55f38297a --- /dev/null +++ b/nux/store/selectors.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { includes, difference, keys } from 'lodash'; + +/** + * Returns an object describing the guide, if any, that the given tip is a part + * of. + * + * @param {Object} state Global application state. + * @param {string} tipID The tip to query. + * + * @typedef {Object} NUX.GuideInfo + * @property {string[]} tipIDs Which tips the guide contains. + * @property {?string} currentTipID The guide's currently showing tip. + * @property {?string} nextTipID The guide's next tip to show. + * + * @return {?NUX.GuideInfo} Information about the associated guide. + */ +export function getAssociatedGuide( state, tipID ) { + for ( const tipIDs of state.guides ) { + if ( includes( tipIDs, tipID ) ) { + const nonDismissedTips = difference( tipIDs, keys( state.preferences.dismissedTips ) ); + const [ currentTipID = null, nextTipID = null ] = nonDismissedTips; + return { tipIDs, currentTipID, nextTipID }; + } + } + + return null; +} + +/** + * Determines whether or not the given tip is showing. Tips are hidden if they + * are disabled, have been dismissed, or are not the current tip in any + * guide that they have been added to. + * + * @param {Object} state Global application state. + * @param {string} id The tip to query. + * + * @return {boolean} Whether or not the given tip is showing. + */ +export function isTipVisible( state, id ) { + if ( state.preferences.areTipsDisabled ) { + return false; + } + + if ( state.preferences.dismissedTips[ id ] ) { + return false; + } + + const associatedGuide = getAssociatedGuide( state, id ); + if ( associatedGuide && associatedGuide.currentTipID !== id ) { + return false; + } + + return true; +} diff --git a/nux/store/test/actions.js b/nux/store/test/actions.js new file mode 100644 index 00000000000000..4fbd1142525cf8 --- /dev/null +++ b/nux/store/test/actions.js @@ -0,0 +1,32 @@ +/** + * Internal dependencies + */ +import { triggerGuide, dismissTip, disableTips } from '../actions'; + +describe( 'actions', () => { + describe( 'triggerGuide', () => { + it( 'should return a TRIGGER_GUIDE action', () => { + expect( triggerGuide( [ 'test/tip-1', 'test/tip-2' ] ) ).toEqual( { + type: 'TRIGGER_GUIDE', + tipIDs: [ 'test/tip-1', 'test/tip-2' ], + } ); + } ); + } ); + + describe( 'dismissTip', () => { + it( 'should return an DISMISS_TIP action', () => { + expect( dismissTip( 'test/tip' ) ).toEqual( { + type: 'DISMISS_TIP', + id: 'test/tip', + } ); + } ); + } ); + + describe( 'disableTips', () => { + it( 'should return an DISABLE_TIPS action', () => { + expect( disableTips() ).toEqual( { + type: 'DISABLE_TIPS', + } ); + } ); + } ); +} ); diff --git a/nux/store/test/reducer.js b/nux/store/test/reducer.js new file mode 100644 index 00000000000000..b2560c4c1e780e --- /dev/null +++ b/nux/store/test/reducer.js @@ -0,0 +1,51 @@ +/** + * Internal dependencies + */ +import { guides, areTipsDisabled, dismissedTips } from '../reducer'; + +describe( 'reducer', () => { + describe( 'guides', () => { + it( 'should start out empty', () => { + expect( guides( undefined, {} ) ).toEqual( [] ); + } ); + + it( 'should add a guide when it is triggered', () => { + const state = guides( [], { + type: 'TRIGGER_GUIDE', + tipIDs: [ 'test/tip-1', 'test/tip-2' ], + } ); + expect( state ).toEqual( [ + [ 'test/tip-1', 'test/tip-2' ], + ] ); + } ); + } ); + + describe( 'areTipsDisabled', () => { + it( 'should default to false', () => { + expect( areTipsDisabled( undefined, {} ) ).toBe( false ); + } ); + + it( 'should flip when tips are disabled', () => { + const state = areTipsDisabled( false, { + type: 'DISABLE_TIPS', + } ); + expect( state ).toBe( true ); + } ); + } ); + + describe( 'dismissedTips', () => { + it( 'should start out empty', () => { + expect( dismissedTips( undefined, {} ) ).toEqual( {} ); + } ); + + it( 'should mark tips as dismissed', () => { + const state = dismissedTips( {}, { + type: 'DISMISS_TIP', + id: 'test/tip', + } ); + expect( state ).toEqual( { + 'test/tip': true, + } ); + } ); + } ); +} ); diff --git a/nux/store/test/selectors.js b/nux/store/test/selectors.js new file mode 100644 index 00000000000000..f9a54bfe9fb5e2 --- /dev/null +++ b/nux/store/test/selectors.js @@ -0,0 +1,104 @@ +/** + * Internal dependencies + */ +import { getAssociatedGuide, isTipVisible } from '../selectors'; + +describe( 'selectors', () => { + describe( 'getAssociatedGuide', () => { + const state = { + guides: [ + [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], + [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], + [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], + ], + preferences: { + dismissedTips: { + 'test/tip-1': true, + 'test/tip-a': true, + 'test/tip-b': true, + 'test/tip-α': true, + 'test/tip-β': true, + 'test/tip-γ': true, + }, + }, + }; + + it( 'should return null when there is no associated guide', () => { + expect( getAssociatedGuide( state, 'test/unknown' ) ).toBeNull(); + } ); + + it( 'should return the associated guide', () => { + expect( getAssociatedGuide( state, 'test/tip-2' ) ).toEqual( { + tipIDs: [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], + currentTipID: 'test/tip-2', + nextTipID: 'test/tip-3', + } ); + } ); + + it( 'should indicate when there is no next tip', () => { + expect( getAssociatedGuide( state, 'test/tip-b' ) ).toEqual( { + tipIDs: [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], + currentTipID: 'test/tip-c', + nextTipID: null, + } ); + } ); + + it( 'should indicate when there is no current or next tip', () => { + expect( getAssociatedGuide( state, 'test/tip-β' ) ).toEqual( { + tipIDs: [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], + currentTipID: null, + nextTipID: null, + } ); + } ); + } ); + + describe( 'isTipVisible', () => { + it( 'should return true by default', () => { + const state = { + guides: [], + preferences: { + areTipsDisabled: false, + dismissedTips: {}, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( true ); + } ); + + it( 'should return false if tips are disabled', () => { + const state = { + guides: [], + preferences: { + areTipsDisabled: true, + dismissedTips: {}, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); + } ); + + it( 'should return false if the tip is dismissed', () => { + const state = { + guides: [], + preferences: { + areTipsDisabled: false, + dismissedTips: { + 'test/tip': true, + }, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); + } ); + + it( 'should return false if the tip is in a guide and it is not the current tip', () => { + const state = { + guides: [ + [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], + ], + preferences: { + areTipsDisabled: false, + dismissedTips: {}, + }, + }; + expect( isTipVisible( state, 'test/tip-2' ) ).toBe( false ); + } ); + } ); +} ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index 1d360600ec2250..5f33dfcf6e8543 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -68,6 +68,11 @@ export async function visitAdmin( adminPath, query ) { export async function newPost( postType ) { await visitAdmin( 'post-new.php', postType ? 'post_type=' + postType : '' ); + + // Disable new user tips so that their UI doesn't get in the way + await page.evaluate( () => { + wp.data.dispatch( 'core/nux' ).disableTips(); + } ); } export async function newDesktopBrowserPage() { diff --git a/test/unit/jest.config.json b/test/unit/jest.config.json index 1c98a829e06693..d661ae82050172 100644 --- a/test/unit/jest.config.json +++ b/test/unit/jest.config.json @@ -1,11 +1,11 @@ { "rootDir": "../../", "collectCoverageFrom": [ - "(blocks|components|editor|utils|edit-post|viewport|plugins|core-data|core-blocks)/**/*.js", + "(blocks|components|editor|utils|edit-post|viewport|plugins|core-data|core-blocks|nux)/**/*.js", "packages/**/*.js" ], "moduleNameMapper": { - "@wordpress\\/(blocks|components|editor|utils|edit-post|viewport|plugins|core-data|core-blocks)$": "$1", + "@wordpress\\/(blocks|components|editor|utils|edit-post|viewport|plugins|core-data|core-blocks|nux)$": "$1", "@wordpress\\/(blob|data|date|dom|deprecated|element|postcss-themes)$": "packages/$1/src" }, "preset": "@wordpress/jest-preset-default", diff --git a/webpack.config.js b/webpack.config.js index 00b29959d41a39..6e488bc4edafeb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -146,6 +146,7 @@ const entryPointNames = [ 'plugins', 'edit-post', 'core-blocks', + 'nux', ]; const gutenbergPackages = [ From 6f51f9a5f53eaa0639533e132f935e51da9a7f2e Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 28 May 2018 17:14:48 +1000 Subject: [PATCH 02/21] Fix appearance of DotTip on mobile Force DotTip content to appear underneath the indicator dot when on mobile viewports. This requires overriding a lot of .component-popover styles which is quite gross. --- nux/components/dot-tip/style.scss | 47 ++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/nux/components/dot-tip/style.scss b/nux/components/dot-tip/style.scss index ee40c63f90b96a..34d59eb70115ca 100644 --- a/nux/components/dot-tip/style.scss +++ b/nux/components/dot-tip/style.scss @@ -50,25 +50,46 @@ $dot-scale: 3; // How much the pulse animation should scale up by in size } } + // Position the dot 25px away from the button &.is-left { margin-left: -25px; - - .components-popover__content { - margin-right: 20px; - } } - &.is-right { margin-left: 25px; + } - .components-popover__content { - margin-left: 20px; - } + // Position the tip content 20px away from the dot + &.is-top .components-popover__content { + margin-bottom: 20px; } -} + &.is-bottom .components-popover__content { + margin-top: 20px; + } + &.is-middle.is-left .components-popover__content { + margin-right: 20px; + } + &.is-middle.is-right .components-popover__content { + margin-left: 20px; + } + + // Extra specificity so that we can override the styles in .component-popover + &:not( .is-mobile ).is-left, + &:not( .is-mobile ).is-center, + &:not( .is-mobile ).is-right { -// Need extra specificity to override the z-index set by .component-popover -.nux-dot-tip, -.nux-dot-tip:not( .is-mobile ).is-bottom { - z-index: z-index( '.nux-dot-tip' ); + // Position tips above popovers + z-index: z-index( '.nux-dot-tip' ); + + // On mobile, always position the tip below the dot and fill the width of the viewport + @media ( max-width: $break-small ) { + .components-popover__content { + align-self: end; + left: 0; + margin: 20px 0 0 0; + max-width: none !important; // Override the inline style set by + position: fixed; + width: 100%; + } + } + } } From e29ca34918f4fe79adc2f133fce834e2df7ea7bd Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 28 May 2018 17:17:01 +1000 Subject: [PATCH 03/21] Remove unnecessary --- nux/components/dot-tip/index.js | 52 +++++++++--------- .../dot-tip/test/__snapshots__/index.js.snap | 54 +++++++++---------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index a0575edd31713e..61b309229eae43 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -6,7 +6,7 @@ import { defer, partial } from 'lodash'; /** * WordPress dependencies */ -import { Component, createRef, Fragment, compose } from '@wordpress/element'; +import { Component, createRef, compose } from '@wordpress/element'; import { Popover, Button, IconButton } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -43,32 +43,30 @@ export class DotTip extends Component { } return ( - - event.stopPropagation() } - > -

{ children }

-

- -

- -
-
+ event.stopPropagation() } + > +

{ children }

+

+ +

+ +
); } } diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap index 7e5747db6e87c5..dc98be81a0b1b2 100644 --- a/nux/components/dot-tip/test/__snapshots__/index.js.snap +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -1,32 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DotTip should render correctly 1`] = ` - - -

- It looks like you’re writing a letter. Would you like help? -

-

- -

- -
-
+ +

+ It looks like you’re writing a letter. Would you like help? +

+

+ +

+ +
`; From 712180eeaf3f7ff8d335a55182d3eda193f03c53 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 29 May 2018 11:01:49 +1000 Subject: [PATCH 04/21] Position NUX dots right next to the edge of the button Adjust popover's layout algorithm so that if yAxis === 'middle', it will set popoverLeft such that the popover points to the edge of the anchor node. This aligns with what e.g. position="bottom center" does. --- components/popover/utils.js | 29 ++++++++++++++++++++++------- nux/components/dot-tip/style.scss | 8 ++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/components/popover/utils.js b/components/popover/utils.js index 849f4202e68afa..bfaf441305fe23 100644 --- a/components/popover/utils.js +++ b/components/popover/utils.js @@ -11,26 +11,32 @@ const isMobileViewport = () => window.innerWidth < 782; * @param {Object} anchorRect Anchor Rect. * @param {Object} contentSize Content Size. * @param {string} xAxis Desired xAxis. + * @param {string} chosenYAxis yAxis to be used. * @param {boolean} expandOnMobile Whether to expand the popover on mobile or not. * * @return {Object} Popover xAxis position and constraints. */ -export function computePopoverXAxisPosition( anchorRect, contentSize, xAxis ) { +export function computePopoverXAxisPosition( anchorRect, contentSize, xAxis, chosenYAxis ) { const { width } = contentSize; - const popoverLeft = Math.round( anchorRect.left + ( anchorRect.width / 2 ) ); // x axis alignment choices + const anchorMidPoint = Math.round( anchorRect.left + ( anchorRect.width / 2 ) ); const centerAlignment = { + popoverLeft: anchorMidPoint, contentWidth: ( - ( popoverLeft - ( width / 2 ) > 0 ? ( width / 2 ) : popoverLeft ) + - ( popoverLeft + ( width / 2 ) > window.innerWidth ? window.innerWidth - popoverLeft : ( width / 2 ) ) + ( anchorMidPoint - ( width / 2 ) > 0 ? ( width / 2 ) : anchorMidPoint ) + + ( anchorMidPoint + ( width / 2 ) > window.innerWidth ? window.innerWidth - anchorMidPoint : ( width / 2 ) ) ), }; + const leftAlignmentX = chosenYAxis === 'middle' ? anchorRect.left : anchorMidPoint; const leftAlignment = { - contentWidth: popoverLeft - width > 0 ? width : popoverLeft, + popoverLeft: leftAlignmentX, + contentWidth: leftAlignmentX - width > 0 ? width : leftAlignmentX, }; + const rightAlignmentX = chosenYAxis === 'middle' ? anchorRect.right : anchorMidPoint; const rightAlignment = { - contentWidth: popoverLeft + width > window.innerWidth ? window.innerWidth - popoverLeft : width, + popoverLeft: rightAlignmentX, + contentWidth: rightAlignmentX + width > window.innerWidth ? window.innerWidth - rightAlignmentX : width, }; // Choosing the x axis @@ -48,6 +54,15 @@ export function computePopoverXAxisPosition( anchorRect, contentSize, xAxis ) { contentWidth = chosenWidth !== width ? chosenWidth : null; } + let popoverLeft; + if ( chosenXAxis === 'center' ) { + popoverLeft = centerAlignment.popoverLeft; + } else if ( chosenXAxis === 'left' ) { + popoverLeft = leftAlignment.popoverLeft; + } else { + popoverLeft = rightAlignment.popoverLeft; + } + return { xAxis: chosenXAxis, popoverLeft, @@ -131,8 +146,8 @@ export function computePopoverYAxisPosition( anchorRect, contentSize, yAxis ) { export function computePopoverPosition( anchorRect, contentSize, position = 'top', expandOnMobile = false ) { const [ yAxis, xAxis = 'center' ] = position.split( ' ' ); - const xAxisPosition = computePopoverXAxisPosition( anchorRect, contentSize, xAxis ); const yAxisPosition = computePopoverYAxisPosition( anchorRect, contentSize, yAxis ); + const xAxisPosition = computePopoverXAxisPosition( anchorRect, contentSize, xAxis, yAxisPosition.yAxis ); return { isMobile: isMobileViewport() && expandOnMobile, diff --git a/nux/components/dot-tip/style.scss b/nux/components/dot-tip/style.scss index 34d59eb70115ca..6bda2d2384be4e 100644 --- a/nux/components/dot-tip/style.scss +++ b/nux/components/dot-tip/style.scss @@ -50,15 +50,15 @@ $dot-scale: 3; // How much the pulse animation should scale up by in size } } - // Position the dot 25px away from the button + // Position the dot right next to the edge of the button &.is-left { - margin-left: -25px; + margin-left: -$dot-size / 2; } &.is-right { - margin-left: 25px; + margin-left: $dot-size / 2; } - // Position the tip content 20px away from the dot + // Position the tip content away from the dot &.is-top .components-popover__content { margin-bottom: 20px; } From 912019ae8bbea463138b2a8e396c946410863fb6 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 30 May 2018 10:16:06 +1000 Subject: [PATCH 05/21] Increase NUX tip padding --- nux/components/dot-tip/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nux/components/dot-tip/style.scss b/nux/components/dot-tip/style.scss index 6bda2d2384be4e..b053d3b5a148b6 100644 --- a/nux/components/dot-tip/style.scss +++ b/nux/components/dot-tip/style.scss @@ -36,7 +36,7 @@ $dot-scale: 3; // How much the pulse animation should scale up by in size } .components-popover__content { - padding: 5px ( 36px + 5px ) 5px 10px; + padding: 5px ( 36px + 5px ) 5px 20px; width: 350px; @include break-small { From 301bf26a5c8d13f7181167153474a03c8f5b3f73 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 30 May 2018 10:24:01 +1000 Subject: [PATCH 06/21] Add margin to sides of NUX tip on mobile --- nux/components/dot-tip/style.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nux/components/dot-tip/style.scss b/nux/components/dot-tip/style.scss index b053d3b5a148b6..7b84c5769a6d7e 100644 --- a/nux/components/dot-tip/style.scss +++ b/nux/components/dot-tip/style.scss @@ -84,11 +84,12 @@ $dot-scale: 3; // How much the pulse animation should scale up by in size @media ( max-width: $break-small ) { .components-popover__content { align-self: end; - left: 0; + left: 5px; margin: 20px 0 0 0; max-width: none !important; // Override the inline style set by position: fixed; - width: 100%; + right: 5px; + width: auto; } } } From deb6d3ceecde15e85c8596807a76ef7185da088e Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 09:51:37 +1000 Subject: [PATCH 07/21] Don't encourage using domReady in the NUX README --- nux/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nux/README.md b/nux/README.md index 22630698c1f9b9..b9a38d951fe57e 100644 --- a/nux/README.md +++ b/nux/README.md @@ -69,9 +69,7 @@ You can group a series of tips into a guide by calling the `triggerGuide` dispat 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' ] ); -} ); +dispatch( 'core/nux' ).triggerGuide( [ 'acme/product-info', 'acme/add-to-cart', 'acme/checkout' ] ); ``` ## Getting information about a guide From 69c82eefc73b4656229929924e9d3396d3174475 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 10:32:05 +1000 Subject: [PATCH 08/21] Add wp-nux as a dependency of wp-edit-post --- lib/client-assets.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index d4df0564ff4658..0cfa47fe9d7a43 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -424,7 +424,7 @@ function gutenberg_register_scripts_and_styles() { wp_register_style( 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), - array( 'wp-components', 'wp-editor', 'wp-edit-blocks', 'wp-core-blocks' ), + array( 'wp-components', 'wp-editor', 'wp-edit-blocks', 'wp-core-blocks', 'wp-nux' ), filemtime( gutenberg_dir_path() . 'build/edit-post/style.css' ) ); wp_style_add_data( 'wp-edit-post', 'rtl', 'replace' ); From 1f4521243f31a764b4c868248e984e81f4d05c54 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 10:33:53 +1000 Subject: [PATCH 09/21] Suggest that NUX tip identifiers are prefixed --- nux/components/dot-tip/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nux/components/dot-tip/README.md b/nux/components/dot-tip/README.md index 3b830a259d8086..caf164760ffeca 100644 --- a/nux/components/dot-tip/README.md +++ b/nux/components/dot-tip/README.md @@ -21,7 +21,7 @@ The component accepts the following props: ### id -An identifier that uniquely identifies the tip. +A string that uniquely identifies the tip. Identifiers should be prefixed with the name of the plugin, followed by a `/`. For example, `acme/add-to-cart`. - Type: `string` - Required: Yes From e956486751d5ad75c4c3451f43d146b2060b09b0 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 11:02:00 +1000 Subject: [PATCH 10/21] Don't use partial(), which is slated for removal --- nux/components/dot-tip/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index 61b309229eae43..15802c5a8cb766 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { defer, partial } from 'lodash'; +import { defer } from 'lodash'; /** * WordPress dependencies @@ -83,8 +83,12 @@ export default compose( withDispatch( ( dispatch, { id } ) => { const { dismissTip, disableTips } = dispatch( 'core/nux' ); return { - onDismiss: partial( dismissTip, id ), - onDisable: disableTips, + onDismiss() { + dismissTip( id ); + }, + onDisable() { + disableTips(); + }, }; } ), )( DotTip ); From a3070f6db3c95cf0a8f7eb96716b76d6988f05d2 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 11:12:25 +1000 Subject: [PATCH 11/21] =?UTF-8?q?tipID=20=E2=86=92=20tipId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nux/README.md | 6 +++--- nux/components/dot-tip/index.js | 2 +- nux/store/actions.js | 6 +++--- nux/store/reducer.js | 2 +- nux/store/selectors.js | 22 +++++++++++----------- nux/store/test/actions.js | 2 +- nux/store/test/reducer.js | 2 +- nux/store/test/selectors.js | 18 +++++++++--------- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/nux/README.md b/nux/README.md index b9a38d951fe57e..d3c9f069a9f886 100644 --- a/nux/README.md +++ b/nux/README.md @@ -78,7 +78,7 @@ dispatch( 'core/nux' ).triggerGuide( [ 'acme/product-info', 'acme/add-to-cart', ```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 ); +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/index.js b/nux/components/dot-tip/index.js index 15802c5a8cb766..5e1ee07fff7352 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -77,7 +77,7 @@ export default compose( const associatedGuide = getAssociatedGuide( id ); return { isVisible: isTipVisible( id ), - hasNextTip: !! ( associatedGuide && associatedGuide.nextTipID ), + hasNextTip: !! ( associatedGuide && associatedGuide.nextTipId ), }; } ), withDispatch( ( dispatch, { id } ) => { diff --git a/nux/store/actions.js b/nux/store/actions.js index a0918e86909af0..95b14731ab06e0 100644 --- a/nux/store/actions.js +++ b/nux/store/actions.js @@ -2,14 +2,14 @@ * Returns an action object that, when dispatched, presents a guide that takes * the user through a series of tips step by step. * - * @param {string[]} tipIDs Which tips to show in the guide. + * @param {string[]} tipIds Which tips to show in the guide. * * @return {Object} Action object. */ -export function triggerGuide( tipIDs ) { +export function triggerGuide( tipIds ) { return { type: 'TRIGGER_GUIDE', - tipIDs, + tipIds, }; } diff --git a/nux/store/reducer.js b/nux/store/reducer.js index c1dec6135b133e..8ed783edd7a3e6 100644 --- a/nux/store/reducer.js +++ b/nux/store/reducer.js @@ -8,7 +8,7 @@ export function guides( state = [], action ) { case 'TRIGGER_GUIDE': return [ ...state, - action.tipIDs, + action.tipIds, ]; } diff --git a/nux/store/selectors.js b/nux/store/selectors.js index 4271c55f38297a..0cf77a75783933 100644 --- a/nux/store/selectors.js +++ b/nux/store/selectors.js @@ -8,21 +8,21 @@ import { includes, difference, keys } from 'lodash'; * of. * * @param {Object} state Global application state. - * @param {string} tipID The tip to query. + * @param {string} tipId The tip to query. * * @typedef {Object} NUX.GuideInfo - * @property {string[]} tipIDs Which tips the guide contains. - * @property {?string} currentTipID The guide's currently showing tip. - * @property {?string} nextTipID The guide's next tip to show. + * @property {string[]} tipIds Which tips the guide contains. + * @property {?string} currentTipId The guide's currently showing tip. + * @property {?string} nextTipId The guide's next tip to show. * * @return {?NUX.GuideInfo} Information about the associated guide. */ -export function getAssociatedGuide( state, tipID ) { - for ( const tipIDs of state.guides ) { - if ( includes( tipIDs, tipID ) ) { - const nonDismissedTips = difference( tipIDs, keys( state.preferences.dismissedTips ) ); - const [ currentTipID = null, nextTipID = null ] = nonDismissedTips; - return { tipIDs, currentTipID, nextTipID }; +export function getAssociatedGuide( state, tipId ) { + for ( const tipIds of state.guides ) { + if ( includes( tipIds, tipId ) ) { + const nonDismissedTips = difference( tipIds, keys( state.preferences.dismissedTips ) ); + const [ currentTipId = null, nextTipId = null ] = nonDismissedTips; + return { tipIds, currentTipId, nextTipId }; } } @@ -49,7 +49,7 @@ export function isTipVisible( state, id ) { } const associatedGuide = getAssociatedGuide( state, id ); - if ( associatedGuide && associatedGuide.currentTipID !== id ) { + if ( associatedGuide && associatedGuide.currentTipId !== id ) { return false; } diff --git a/nux/store/test/actions.js b/nux/store/test/actions.js index 4fbd1142525cf8..1aa4d0cf6f2e34 100644 --- a/nux/store/test/actions.js +++ b/nux/store/test/actions.js @@ -8,7 +8,7 @@ describe( 'actions', () => { it( 'should return a TRIGGER_GUIDE action', () => { expect( triggerGuide( [ 'test/tip-1', 'test/tip-2' ] ) ).toEqual( { type: 'TRIGGER_GUIDE', - tipIDs: [ 'test/tip-1', 'test/tip-2' ], + tipIds: [ 'test/tip-1', 'test/tip-2' ], } ); } ); } ); diff --git a/nux/store/test/reducer.js b/nux/store/test/reducer.js index b2560c4c1e780e..a40fabdf49f84f 100644 --- a/nux/store/test/reducer.js +++ b/nux/store/test/reducer.js @@ -12,7 +12,7 @@ describe( 'reducer', () => { it( 'should add a guide when it is triggered', () => { const state = guides( [], { type: 'TRIGGER_GUIDE', - tipIDs: [ 'test/tip-1', 'test/tip-2' ], + tipIds: [ 'test/tip-1', 'test/tip-2' ], } ); expect( state ).toEqual( [ [ 'test/tip-1', 'test/tip-2' ], diff --git a/nux/store/test/selectors.js b/nux/store/test/selectors.js index f9a54bfe9fb5e2..08f6c7eea38f66 100644 --- a/nux/store/test/selectors.js +++ b/nux/store/test/selectors.js @@ -29,25 +29,25 @@ describe( 'selectors', () => { it( 'should return the associated guide', () => { expect( getAssociatedGuide( state, 'test/tip-2' ) ).toEqual( { - tipIDs: [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], - currentTipID: 'test/tip-2', - nextTipID: 'test/tip-3', + tipIds: [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], + currentTipId: 'test/tip-2', + nextTipId: 'test/tip-3', } ); } ); it( 'should indicate when there is no next tip', () => { expect( getAssociatedGuide( state, 'test/tip-b' ) ).toEqual( { - tipIDs: [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], - currentTipID: 'test/tip-c', - nextTipID: null, + tipIds: [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], + currentTipId: 'test/tip-c', + nextTipId: null, } ); } ); it( 'should indicate when there is no current or next tip', () => { expect( getAssociatedGuide( state, 'test/tip-β' ) ).toEqual( { - tipIDs: [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], - currentTipID: null, - nextTipID: null, + tipIds: [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], + currentTipId: null, + nextTipId: null, } ); } ); } ); From 15daa9c1a69c43ee954b2324e83727a20608fbd8 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 11:24:22 +1000 Subject: [PATCH 12/21] Improve NUX JSDoc comments --- nux/store/reducer.js | 26 ++++++++++++++++++++++++++ nux/store/selectors.js | 14 +++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/nux/store/reducer.js b/nux/store/reducer.js index 8ed783edd7a3e6..e8ba1abb7b0a5a 100644 --- a/nux/store/reducer.js +++ b/nux/store/reducer.js @@ -3,6 +3,15 @@ */ import { combineReducers } from '@wordpress/data'; +/** + * Reducer that tracks which tips are in a guide. Each guide is represented by + * an array which contains the tip identifiers contained within that guide. + * + * @param {Array} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Array} Updated state. + */ export function guides( state = [], action ) { switch ( action.type ) { case 'TRIGGER_GUIDE': @@ -15,6 +24,14 @@ export function guides( state = [], action ) { return state; } +/** + * Reducer that tracks whether or not tips are globally disabled. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ export function areTipsDisabled( state = false, action ) { switch ( action.type ) { case 'DISABLE_TIPS': @@ -24,6 +41,15 @@ export function areTipsDisabled( state = false, action ) { return state; } +/** + * Reducer that tracks which tips have been dismissed. If the state object + * contains a tip identifier, then that tip is dismissed. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ export function dismissedTips( state = {}, action ) { switch ( action.type ) { case 'DISMISS_TIP': diff --git a/nux/store/selectors.js b/nux/store/selectors.js index 0cf77a75783933..c821db0c23d8a9 100644 --- a/nux/store/selectors.js +++ b/nux/store/selectors.js @@ -4,16 +4,20 @@ import { includes, difference, keys } from 'lodash'; /** - * Returns an object describing the guide, if any, that the given tip is a part - * of. - * - * @param {Object} state Global application state. - * @param {string} tipId The tip to query. + * An object containing information about a guide. * * @typedef {Object} NUX.GuideInfo * @property {string[]} tipIds Which tips the guide contains. * @property {?string} currentTipId The guide's currently showing tip. * @property {?string} nextTipId The guide's next tip to show. + */ + +/** + * Returns an object describing the guide, if any, that the given tip is a part + * of. + * + * @param {Object} state Global application state. + * @param {string} tipId The tip to query. * * @return {?NUX.GuideInfo} Information about the associated guide. */ From 49a6804362c885455428e75390f80d42eee5a27b Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 11:33:29 +1000 Subject: [PATCH 13/21] Memoize the getAssociatedGuide() selector --- nux/store/selectors.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/nux/store/selectors.js b/nux/store/selectors.js index c821db0c23d8a9..bf7365df9f27f2 100644 --- a/nux/store/selectors.js +++ b/nux/store/selectors.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import createSelector from 'rememo'; import { includes, difference, keys } from 'lodash'; /** @@ -21,17 +22,23 @@ import { includes, difference, keys } from 'lodash'; * * @return {?NUX.GuideInfo} Information about the associated guide. */ -export function getAssociatedGuide( state, tipId ) { - for ( const tipIds of state.guides ) { - if ( includes( tipIds, tipId ) ) { - const nonDismissedTips = difference( tipIds, keys( state.preferences.dismissedTips ) ); - const [ currentTipId = null, nextTipId = null ] = nonDismissedTips; - return { tipIds, currentTipId, nextTipId }; +export const getAssociatedGuide = createSelector( + ( state, tipId ) => { + for ( const tipIds of state.guides ) { + if ( includes( tipIds, tipId ) ) { + const nonDismissedTips = difference( tipIds, keys( state.preferences.dismissedTips ) ); + const [ currentTipId = null, nextTipId = null ] = nonDismissedTips; + return { tipIds, currentTipId, nextTipId }; + } } - } - return null; -} + return null; + }, + ( state ) => [ + state.guides, + state.preferences.dismissedTips, + ], +); /** * Determines whether or not the given tip is showing. Tips are hidden if they From 7a1e8249ea418883db332fd767d7afef91bc7cf7 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 11:49:09 +1000 Subject: [PATCH 14/21] NUX: Dismiss the tip when one clicks away from it --- nux/components/dot-tip/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index 5e1ee07fff7352..8d062f60550b35 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -52,6 +52,7 @@ export class DotTip extends Component { role="dialog" aria-modal="true" aria-label={ __( 'New user tip' ) } + onClose={ onDismiss } onClick={ ( event ) => event.stopPropagation() } >

{ children }

From 6596c75b8f74487938b1682c9d1e903465a1e35b Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 14:10:59 +1000 Subject: [PATCH 15/21] Position tooltips above NUX tips --- components/tooltip/style.scss | 1 + edit-post/assets/stylesheets/_z-index.scss | 3 +++ 2 files changed, 4 insertions(+) diff --git a/components/tooltip/style.scss b/components/tooltip/style.scss index cef27ed6e96de3..9f792b7f2fdc9d 100644 --- a/components/tooltip/style.scss +++ b/components/tooltip/style.scss @@ -1,5 +1,6 @@ .components-tooltip.components-popover { pointer-events: none; + z-index: z-index( '.components-tooltip' ); &:before { border-color: transparent; diff --git a/edit-post/assets/stylesheets/_z-index.scss b/edit-post/assets/stylesheets/_z-index.scss index 26e526aa019b57..a4c704297de979 100644 --- a/edit-post/assets/stylesheets/_z-index.scss +++ b/edit-post/assets/stylesheets/_z-index.scss @@ -80,6 +80,9 @@ $z-layers: ( // Show NUX tips above popovers, wp-admin menus, submenus, and sidebar: '.nux-dot-tip': 1000001, + + // Show tooltips above NUX tips, wp-admin menus, submenus, and sidebar: + '.components-tooltip': 1000002 ); @function z-index( $key ) { From be79c137344fe4866615515ca0ea2b71e9c0b192 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 15:49:57 +1000 Subject: [PATCH 16/21] Remove aria-modal property from DotTip Safari 11 has a weird bug when this property is used. --- nux/components/dot-tip/index.js | 1 - nux/components/dot-tip/test/__snapshots__/index.js.snap | 1 - 2 files changed, 2 deletions(-) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index 8d062f60550b35..066b0f322a7388 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -50,7 +50,6 @@ export class DotTip extends Component { noArrow focusOnMount role="dialog" - aria-modal="true" aria-label={ __( 'New user tip' ) } onClose={ onDismiss } onClick={ ( event ) => event.stopPropagation() } diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap index dc98be81a0b1b2..04beecc50ccba6 100644 --- a/nux/components/dot-tip/test/__snapshots__/index.js.snap +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -3,7 +3,6 @@ exports[`DotTip should render correctly 1`] = ` Date: Thu, 31 May 2018 15:51:11 +1000 Subject: [PATCH 17/21] Make order of the markup in DotTip match how they appear visually --- nux/components/dot-tip/index.js | 12 ++++++------ .../dot-tip/test/__snapshots__/index.js.snap | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index 066b0f322a7388..411e8e76822cb5 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -54,18 +54,18 @@ export class DotTip extends Component { onClose={ onDismiss } onClick={ ( event ) => event.stopPropagation() } > -

{ children }

-

- -

+

{ children }

+

+ +

); } diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap index 04beecc50ccba6..13ad5e765cf080 100644 --- a/nux/components/dot-tip/test/__snapshots__/index.js.snap +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -10,6 +10,11 @@ exports[`DotTip should render correctly 1`] = ` position="middle right" role="dialog" > +

It looks like you’re writing a letter. Would you like help?

@@ -20,10 +25,5 @@ exports[`DotTip should render correctly 1`] = ` Got it

-
`; From edd6e91a282ef05db63f2152b4f0dd6f3d32adda Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 31 May 2018 15:53:07 +1000 Subject: [PATCH 18/21] Improve a11y of DotTip labels --- nux/components/dot-tip/index.js | 6 +++--- nux/components/dot-tip/test/__snapshots__/index.js.snap | 4 ++-- nux/components/dot-tip/test/index.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index 411e8e76822cb5..ce0980d45ee622 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -50,20 +50,20 @@ export class DotTip extends Component { noArrow focusOnMount role="dialog" - aria-label={ __( 'New user tip' ) } + aria-label={ __( 'Gutenberg tips' ) } onClose={ onDismiss } onClick={ ( event ) => event.stopPropagation() } >

{ children }

diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap index 13ad5e765cf080..04a9f060df43c5 100644 --- a/nux/components/dot-tip/test/__snapshots__/index.js.snap +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -2,7 +2,7 @@ exports[`DotTip should render correctly 1`] = `

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 20eb849ecfba68..31449168482362 100644 --- a/nux/components/dot-tip/test/index.js +++ b/nux/components/dot-tip/test/index.js @@ -45,7 +45,7 @@ describe( 'DotTip', () => { It looks like you’re writing a letter. Would you like help? ); - wrapper.find( 'IconButton[label="Disable guide"]' ).first().simulate( 'click' ); + wrapper.find( 'IconButton[label="Disable tips"]' ).first().simulate( 'click' ); expect( onDisable ).toHaveBeenCalled(); } ); } ); From ce7425394d485c4d392c356dad6f72ac7d85a97c Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 1 Jun 2018 13:02:50 +1000 Subject: [PATCH 19/21] Dismiss tips instead of disabling when the X is clicked --- nux/components/dot-tip/index.js | 11 ++++------- .../dot-tip/test/__snapshots__/index.js.snap | 2 +- nux/components/dot-tip/test/index.js | 10 +++++----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/nux/components/dot-tip/index.js b/nux/components/dot-tip/index.js index ce0980d45ee622..fcd9c2014511f1 100644 --- a/nux/components/dot-tip/index.js +++ b/nux/components/dot-tip/index.js @@ -36,7 +36,7 @@ export class DotTip extends Component { } render() { - const { children, isVisible, hasNextTip, onDismiss, onDisable } = this.props; + const { children, isVisible, hasNextTip, onDismiss } = this.props; if ( ! isVisible ) { return null; @@ -57,8 +57,8 @@ export class DotTip extends Component {

{ children }

@@ -81,14 +81,11 @@ export default compose( }; } ), withDispatch( ( dispatch, { id } ) => { - const { dismissTip, disableTips } = dispatch( 'core/nux' ); + const { dismissTip } = dispatch( 'core/nux' ); return { onDismiss() { dismissTip( id ); }, - onDisable() { - disableTips(); - }, }; } ), )( DotTip ); diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap index 04a9f060df43c5..42cccdf46e2b77 100644 --- a/nux/components/dot-tip/test/__snapshots__/index.js.snap +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -13,7 +13,7 @@ exports[`DotTip should render correctly 1`] = `

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() } > -

{ children }

+
); } diff --git a/nux/components/dot-tip/test/__snapshots__/index.js.snap b/nux/components/dot-tip/test/__snapshots__/index.js.snap index 42cccdf46e2b77..a3a6773f5261fe 100644 --- a/nux/components/dot-tip/test/__snapshots__/index.js.snap +++ b/nux/components/dot-tip/test/__snapshots__/index.js.snap @@ -10,11 +10,6 @@ exports[`DotTip should render correctly 1`] = ` position="middle right" role="dialog" > -

It looks like you’re writing a letter. Would you like help?

@@ -25,5 +20,10 @@ exports[`DotTip should render correctly 1`] = ` Got it

+ `; From c8c4d227f4f095b2e32bd236662245c654413059 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 4 Jun 2018 12:21:48 +1000 Subject: [PATCH 21/21] NUX: Only show inserter tip in DefaultBlockAppender Only display the first DotTip in the new user guide in the DefaultBlockAppender that appears in a new post. This prevents the guide from appearing in an existing post that contains empty blocks. --- editor/components/block-list/block.js | 14 ++------------ editor/components/default-block-appender/index.js | 9 ++++++++- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 5e9db9ccec6b7f..70eb8f82088c9e 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -28,7 +28,6 @@ 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 @@ -415,7 +414,6 @@ export class BlockListBlock extends Component { isEmptyDefaultBlock, isPreviousBlockADefaultEmptyBlock, hasSelectedInnerBlock, - hasTip, } = this.props; const isHovered = this.state.isHovered && ! isMultiSelecting; const { name: blockName, isValid } = block; @@ -428,7 +426,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 || hasTip ) && isEmptyDefaultBlock; + const showSideInserter = ( isSelected || isHovered ) && 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; @@ -597,11 +595,7 @@ 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.' ) } - - + /> ) } @@ -629,9 +623,6 @@ 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(); @@ -660,7 +651,6 @@ 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 ad5c4e14d1d7d4..5a844824ba62b4 100644 --- a/editor/components/default-block-appender/index.js +++ b/editor/components/default-block-appender/index.js @@ -88,9 +88,12 @@ export default compose( insertDefaultBlock, startTyping, } = dispatch( 'core/editor' ); + + const { dismissTip } = dispatch( 'core/nux' ); + return { onAppend() { - const { layout, rootUID } = ownProps; + const { layout, rootUID, hasTip } = ownProps; let attributes; if ( layout ) { @@ -99,6 +102,10 @@ export default compose( insertDefaultBlock( attributes, rootUID ); startTyping(); + + if ( hasTip ) { + dismissTip( 'core/editor.inserter' ); + } }, }; } ),