diff --git a/docs/designers-developers/developers/data/data-core-nux.md b/docs/designers-developers/developers/data/data-core-nux.md index e937601ec864b..66921755e5f07 100644 --- a/docs/designers-developers/developers/data/data-core-nux.md +++ b/docs/designers-developers/developers/data/data-core-nux.md @@ -32,6 +32,18 @@ _Returns_ - `?NUX.GuideInfo`: Information about the associated guide. +# **hasDismissedAnyTips** + +Returns whether or not any tips have been dismissed. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether any tips have been dismissed. + # **isTipVisible** Determines whether or not the given tip is showing. Tips are hidden if they diff --git a/package-lock.json b/package-lock.json index 04093ff7f7b4f..8773c09370fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3741,6 +3741,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/nux": "file:packages/nux", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", @@ -3997,7 +3998,6 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", "@wordpress/keycodes": "file:packages/keycodes", - "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", @@ -4039,7 +4039,6 @@ "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", - "@wordpress/nux": "file:packages/nux", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "@wordpress/wordcount": "file:packages/wordcount", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 63718286e9bb8..f9133889bf98b 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -370,7 +370,8 @@ The default editor settings bodyPlaceholder string Empty post placeholder titlePlaceholder string Empty title placeholder codeEditingEnabled string Whether or not the user can switch to the code editor - \_\_experimentalCanUserUseUnfilteredHTML string Whether the user should be able to use unfiltered HTML or the HTML should be filtered e.g., to remove elements considered insecure like iframes. + **experimentalCanUserUseUnfilteredHTML string Whether the user should be able to use unfiltered HTML or the HTML should be filtered e.g., to remove elements considered insecure like iframes. + **experimentalEnableTips boolean Whether or not tips aimed at new users should appear in the UI. # **SkipToSelectedBlock** diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 61c49f9d9a088..6b171bc3eb4da 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -37,6 +37,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/keycodes": "file:../keycodes", + "@wordpress/nux": "file:../nux", "@wordpress/rich-text": "file:../rich-text", "@wordpress/token-list": "file:../token-list", "@wordpress/url": "file:../url", diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index e04dbcbd932be..d31c422316e67 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -10,6 +10,7 @@ import { __ } from '@wordpress/i18n'; import { getBlockType, getUnregisteredTypeHandlerName } from '@wordpress/blocks'; import { PanelBody } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; +import { __experimentalInlineTip as InlineTip } from '@wordpress/nux'; /** * Internal dependencies @@ -27,6 +28,7 @@ const BlockInspector = ( { selectedBlockClientId, selectedBlockName, showNoBlockSelectedMessage = true, + enableTips, } ) => { if ( count > 1 ) { return ; @@ -51,6 +53,11 @@ const BlockInspector = ( { return ( <> + { enableTips && ( + + { __( 'The block tab contains additional settings for the selected block.' ) } + + ) }
@@ -91,18 +98,20 @@ const BlockInspector = ( { export default withSelect( ( select ) => { - const { getSelectedBlockClientId, getSelectedBlockCount, getBlockName } = select( 'core/block-editor' ); + const { getSelectedBlockClientId, getSelectedBlockCount, getBlockName, getSettings } = select( 'core/block-editor' ); const { getBlockStyles } = select( 'core/blocks' ); const selectedBlockClientId = getSelectedBlockClientId(); const selectedBlockName = selectedBlockClientId && getBlockName( selectedBlockClientId ); const blockType = selectedBlockClientId && getBlockType( selectedBlockName ); const blockStyles = selectedBlockClientId && getBlockStyles( selectedBlockName ); + const { __experimentalEnableTips: enableTips } = getSettings(); return { count: getSelectedBlockCount(), hasBlockStyles: blockStyles && blockStyles.length > 0, selectedBlockName, selectedBlockClientId, blockType, + enableTips, }; } )( BlockInspector ); diff --git a/packages/block-editor/src/components/block-inspector/style.scss b/packages/block-editor/src/components/block-inspector/style.scss index 2fdb6d04b44aa..1633e262da340 100644 --- a/packages/block-editor/src/components/block-inspector/style.scss +++ b/packages/block-editor/src/components/block-inspector/style.scss @@ -6,6 +6,9 @@ text-align: center; } +.block-editor-block-inspector__tip { + margin: 0 0 16px 0; +} .block-editor-block-inspector__card { display: flex; diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 12d119ce4ab51..ba96236f7f22b 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -22,7 +22,7 @@ import scrollIntoView from 'dom-scroll-into-view'; */ import { __, _n, _x, sprintf } from '@wordpress/i18n'; import { Component, createRef } from '@wordpress/element'; -import { withSpokenMessages, PanelBody } from '@wordpress/components'; +import { withSpokenMessages, PanelBody, ExternalLink } from '@wordpress/components'; import { getCategories, isReusableBlock, @@ -33,6 +33,7 @@ import { withDispatch, withSelect } from '@wordpress/data'; import { withInstanceId, compose, withSafeTimeout } from '@wordpress/compose'; import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; import { addQueryArgs } from '@wordpress/url'; +import { __experimentalInlineTip as InlineTip } from '@wordpress/nux'; /** * Internal dependencies @@ -247,7 +248,7 @@ export class InserterMenu extends Component { } render() { - const { instanceId, onSelect, rootClientId } = this.props; + const { instanceId, onSelect, rootClientId, enableTips } = this.props; const { childItems, hoveredItem, @@ -290,6 +291,16 @@ export class InserterMenu extends Component { aria-label={ __( 'Available block types' ) } > + { enableTips && ( + + { __( 'There are Blocks for most types of content: text, headings, images, lists, and lots more!' ) } + { ' ' } + + { __( 'Learn more' ) } + + + ) } + { diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 87b9a79f12fa4..b8deee07553be 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -105,6 +105,10 @@ $block-inserter-search-height: 38px; } } +.block-editor-inserter__tip { + margin: 0 0 $grid-size-large 0; +} + .block-editor-inserter__popover .block-editor-block-types-list { margin: 0 -8px; } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 1f91e29c7e2f5..f4f9a4815ab0d 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -28,9 +28,11 @@ export const PREFERENCES_DEFAULTS = { * titlePlaceholder string Empty title placeholder * codeEditingEnabled string Whether or not the user can switch to the code editor * __experimentalCanUserUseUnfilteredHTML string Whether the user should be able to use unfiltered HTML or the HTML should be filtered e.g., to remove elements considered insecure like iframes. + * __experimentalEnableTips boolean Whether or not tips aimed at new users should appear in the UI. */ export const SETTINGS_DEFAULTS = { alignWide: false, + colors: [ { name: __( 'Pale pink' ), @@ -142,7 +144,11 @@ export const SETTINGS_DEFAULTS = { allowedMimeTypes: null, availableLegacyWidgets: {}, + hasPermissionsToManageWidgets: false, + __experimentalCanUserUseUnfilteredHTML: false, + + __experimentalEnableTips: false, }; diff --git a/packages/components/src/notice/index.js b/packages/components/src/notice/index.js index 99d5f9968f111..6a945cd8c7e50 100644 --- a/packages/components/src/notice/index.js +++ b/packages/components/src/notice/index.js @@ -24,9 +24,14 @@ function Notice( { actions = [], __unstableHTML, } ) { - const classes = classnames( className, 'components-notice', 'is-' + status, { - 'is-dismissible': isDismissible, - } ); + const classes = classnames( + className, + 'components-notice', + status ? 'is-' + status : undefined, + { + 'is-dismissible': isDismissible, + } + ); if ( __unstableHTML ) { children = { children }; diff --git a/packages/e2e-test-utils/src/create-new-post.js b/packages/e2e-test-utils/src/create-new-post.js index 91a95bf5216a4..8616f55143a12 100644 --- a/packages/e2e-test-utils/src/create-new-post.js +++ b/packages/e2e-test-utils/src/create-new-post.js @@ -32,8 +32,4 @@ export async function createNewPost( { const action = _enableTips ? 'enableTips' : 'disableTips'; wp.data.dispatch( 'core/nux' )[ action ](); }, enableTips ); - - if ( enableTips ) { - await page.reload(); - } } diff --git a/packages/e2e-tests/specs/nux.test.js b/packages/e2e-tests/specs/nux.test.js index 40c980fac079c..f5f5c7e355aa2 100644 --- a/packages/e2e-tests/specs/nux.test.js +++ b/packages/e2e-tests/specs/nux.test.js @@ -2,179 +2,89 @@ * WordPress dependencies */ import { - clickBlockAppender, - clickOnMoreMenuItem, createNewPost, - saveDraft, + insertBlock, + openGlobalBlockInserter, toggleScreenOption, } from '@wordpress/e2e-test-utils'; -describe( 'New User Experience (NUX)', () => { - async function clickAllTips( page ) { - // Click through all available tips. - const tips = await getTips( page ); - const numberOfTips = tips.tipIds.length; - - for ( let i = 1; i < numberOfTips; i++ ) { - await page.click( '.nux-dot-tip .components-button.is-link' ); - } - - return { numberOfTips, tips }; - } - - async function getTips( page ) { - return await page.evaluate( () => { - return wp.data.select( 'core/nux' ).getAssociatedGuide( 'core/editor.inserter' ); - } ); - } - - async function getTipsEnabled( page ) { - return await page.evaluate( () => { - return wp.data.select( 'core/nux' ).areTipsEnabled(); - } ); - } +/** + * Queries the data store and returns whether or not NUX tips are enabled. + * + * @return {boolean} Whether or not NUX tips are enabled. + */ +async function areTipsEnabled() { + return await page.evaluate( () => wp.data.select( 'core/nux' ).areTipsEnabled() ); +} +describe( 'New User Experience (NUX)', () => { beforeEach( async () => { await createNewPost( { enableTips: true } ); } ); - it( 'should show tips to a first-time user', async () => { - const firstTipText = await page.$eval( '.nux-dot-tip', ( element ) => element.innerText ); - expect( firstTipText ).toContain( 'Welcome to the wonderful world of blocks!' ); - - const [ nextTipButton ] = await page.$x( "//button[contains(text(), 'See next tip')]" ); - await nextTipButton.click(); + it( 'should show a tip in the inserter', async () => { + // Open up the inserter. + await openGlobalBlockInserter(); - const secondTipText = await page.$eval( '.nux-dot-tip', ( element ) => element.innerText ); - expect( secondTipText ).toContain( 'You’ll find more settings for your page and blocks in the sidebar.' ); + // Check there's a tip in the inserter. + const inserterTip = await page.$( '.block-editor-inserter__tip' ); + expect( inserterTip ).not.toBeNull(); } ); - it( 'should show "Got it" once all tips have been displayed', async () => { - await clickAllTips( page ); + it( 'should show a tip in the block inspector', async () => { + // Insert any old block. + await insertBlock( 'Paragraph' ); - // Make sure "Got it" button appears on the last tip. - const gotItButton = await page.$x( "//button[contains(text(), 'Got it')]" ); - expect( gotItButton ).toHaveLength( 1 ); - - // Click the "Got it button". - await page.click( '.nux-dot-tip .components-button.is-link' ); - - // Verify no more tips are visible on the page. - const nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 0 ); - - // Tips should not be marked as disabled, but when the user has seen all - // of the available tips, they will not appear. - const areTipsEnabled = await getTipsEnabled( page ); - expect( areTipsEnabled ).toEqual( true ); + // Check there's a tip in the block inspector. + const blockInspectorTip = await page.$( '.block-editor-block-inspector__tip' ); + expect( blockInspectorTip ).not.toBeNull(); } ); - it( 'should hide and disable tips if "disable tips" button is clicked', async () => { - await page.click( '.nux-dot-tip__disable' ); + it( 'should dismiss a single tip if X button is clicked and confirmation is dismissed', async () => { + // Open up the inserter. + await openGlobalBlockInserter(); - // Verify no more tips are visible on the page. - let nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 0 ); + // Click the tip's X button. + await page.click( '.block-editor-inserter__tip button[aria-label="Dismiss this notice"]' ); - // We should be disabling the tips so they don't appear again. - const areTipsEnabled = await getTipsEnabled( page ); - expect( areTipsEnabled ).toEqual( false ); + // Dismiss the confirmation modal. + const [ noButton ] = await page.$x( '//*[contains(@class, "nux-hide-tips-confirmation")]//button[text()="No"]' ); + await noButton.click(); - // Refresh the page; tips should not show because they were disabled. - await page.reload(); + // The tip should be gone. + const inserterTip = await page.$( '.block-editor-inserter__tip' ); + expect( inserterTip ).toBeNull(); - nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 0 ); + // Tips should still be enabled. + expect( await areTipsEnabled() ).toBe( true ); } ); - it( 'should enable tips when the "Enable tips" option is toggled on', async () => { - // Start by disabling tips. - await page.click( '.nux-dot-tip__disable' ); - - // Verify no more tips are visible on the page. - let nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 0 ); - - // Tips should be disabled in localStorage as well. - let areTipsEnabled = await getTipsEnabled( page ); - expect( areTipsEnabled ).toEqual( false ); + it( 'should disable all tips if X button is clicked and confirmation is confirmed', async () => { + // Open up the inserter. + await openGlobalBlockInserter(); - // Toggle the 'Enable Tips' option to enable. - await toggleScreenOption( 'Enable Tips' ); + // Click the tip's X button. + await page.click( '.block-editor-inserter__tip button[aria-label="Dismiss this notice"]' ); - // Tips should once again appear. - nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 1 ); + // Accept the confirmation modal. + const [ disableTipsButton ] = await page.$x( '//*[contains(@class, "nux-hide-tips-confirmation")]//button[text()="Disable Tips"]' ); + await disableTipsButton.click(); - // Tips should be enabled in localStorage as well. - areTipsEnabled = await getTipsEnabled( page ); - expect( areTipsEnabled ).toEqual( true ); - } ); - - // TODO: This test should be enabled once - // https://github.com/WordPress/gutenberg/issues/7458 is fixed. - it.skip( 'should show tips as disabled if all tips have been shown', async () => { - await clickAllTips( page ); - - // Open the "More" menu to check the "Show Tips" element. - await page.click( '.edit-post-more-menu [aria-label="More tools & options"]' ); - const showTipsButton = await page.$x( '//button[contains(text(), "Show Tips")][@aria-pressed="false"]' ); + // The tip should be gone. + const inserterTip = await page.$( '.block-editor-inserter__tip' ); + expect( inserterTip ).toBeNull(); - expect( showTipsButton ).toHaveLength( 1 ); + // Tips should now be disabled. + expect( await areTipsEnabled() ).toBe( false ); } ); - // TODO: This test should be enabled once - // https://github.com/WordPress/gutenberg/issues/7458 is fixed. - it.skip( 'should reset tips if all tips have been shown and show tips was unchecked', async () => { - const { numberOfTips } = await clickAllTips( page ); - - // Click again to re-enable tips; they should appear. - await clickOnMoreMenuItem( 'Show Tips' ); - - // Open the "More" menu to check the "Show Tips" element. - await page.click( '.edit-post-more-menu [aria-label="More tools & options"]' ); - const showTipsButton = await page.$x( '//button[contains(text(), "Show Tips")][@aria-pressed="true"]' ); - - expect( showTipsButton ).toHaveLength( 1 ); - - // Tips should re-appear on the page. - const nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 1 ); - - // Tips should be enabled again. - const areTipsEnabled = await getTipsEnabled( page ); - expect( areTipsEnabled ).toEqual( true ); - - // Dismissed tips should be reset and ready to be shown again. - const resetTips = await getTips( page ); - const newNumberOfTips = resetTips.tipIds.length; - expect( newNumberOfTips ).toHaveLength( numberOfTips ); - } ); + it( 'should enable and disable tips when option is toggled', async () => { + // Toggling the option off should disable tips. + await toggleScreenOption( 'Enable Tips', false ); + expect( await areTipsEnabled() ).toBe( false ); - // TODO: This test should be enabled once - // https://github.com/WordPress/gutenberg/issues/7753 is fixed. - // See: https://github.com/WordPress/gutenberg/issues/7753#issuecomment-403952816 - it.skip( 'should show tips if "Show tips" was disabled on a draft and then enabled', async () => { - // Click the "Show tips" button (enabled by default) to disable tips. - await clickOnMoreMenuItem( 'Show Tips' ); - - // Let's type something so there's content in this post. - await page.click( '.editor-post-title__input' ); - await page.keyboard.type( 'Post title' ); - await clickBlockAppender(); - await page.keyboard.type( 'Post content goes here.' ); - await saveDraft(); - - // Refresh the page; tips should be disabled. - await page.reload(); - let nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 0 ); - - // Clicking should re-enable tips. - await clickOnMoreMenuItem( 'Show Tips' ); - - // Tips should re-appear on the page. - nuxTipElements = await page.$$( '.nux-dot-tip' ); - expect( nuxTipElements ).toHaveLength( 1 ); + // Toggling the option on should enable tips. + await toggleScreenOption( 'Enable Tips', true ); + expect( await areTipsEnabled() ).toBe( true ); } ); } ); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 09cebd91d061b..08f46e37086fa 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -36,7 +36,6 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/i18n": "file:../i18n", "@wordpress/keycodes": "file:../keycodes", - "@wordpress/nux": "file:../nux", "@wordpress/plugins": "file:../plugins", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", diff --git a/packages/edit-post/src/components/editor-initialization/index.js b/packages/edit-post/src/components/editor-initialization/index.js index 51cb90681f8ae..f7ab4d7f5b008 100644 --- a/packages/edit-post/src/components/editor-initialization/index.js +++ b/packages/edit-post/src/components/editor-initialization/index.js @@ -1,9 +1,3 @@ -/** - * WordPress dependencies - */ -import { useEffect } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; - /** * Internal dependencies */ @@ -24,14 +18,5 @@ export default function( { postId } ) { useAdjustSidebarListener( postId ); useBlockSelectionListener( postId ); useUpdatePostLinkListener( postId ); - const { triggerGuide } = useDispatch( 'core/nux' ); - useEffect( () => { - triggerGuide( [ - 'core/editor.inserter', - 'core/editor.settings', - 'core/editor.preview', - 'core/editor.publish', - ] ); - }, [ triggerGuide ] ); return null; } diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index 8ff5e2209594d..ca75a3fa3303b 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -4,7 +4,6 @@ import { compose } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; -import { DotTip } from '@wordpress/nux'; import { __ } from '@wordpress/i18n'; import { Inserter, @@ -30,12 +29,7 @@ function HeaderToolbar( { hasFixedToolbar, isLargeViewport, showInserter, isText className="edit-post-header-toolbar" aria-label={ toolbarAriaLabel } > -
- - - { __( 'Welcome to the wonderful world of blocks! Click the “+” (“Add block”) button to add a new block. There are blocks available for all kinds of content: you can insert text, headings, images, lists, and lots more!' ) } - -
+ diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index dff5622593603..b6817580b48d6 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -9,7 +9,6 @@ import { } from '@wordpress/editor'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { DotTip } from '@wordpress/nux'; /** * Internal dependencies @@ -63,19 +62,14 @@ function Header( { forceIsDirty={ hasActiveMetaboxes } forceIsSaving={ isSaving } /> -
- - - { __( 'You’ll find more settings for your page and blocks in the sidebar. Click the cog icon to toggle the sidebar open and closed.' ) } - -
+
diff --git a/packages/edit-post/src/components/options-modal/options/deferred.js b/packages/edit-post/src/components/options-modal/options/deferred.js deleted file mode 100644 index 2a0f84348d23b..0000000000000 --- a/packages/edit-post/src/components/options-modal/options/deferred.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import BaseOption from './base'; - -class DeferredOption extends Component { - constructor( { isChecked } ) { - super( ...arguments ); - this.state = { - isChecked, - }; - } - - componentWillUnmount() { - if ( this.state.isChecked !== this.props.isChecked ) { - this.props.onChange( this.state.isChecked ); - } - } - - render() { - return ( - this.setState( { isChecked } ) } - /> - ); - } -} - -export default DeferredOption; diff --git a/packages/edit-post/src/components/options-modal/options/enable-tips.js b/packages/edit-post/src/components/options-modal/options/enable-tips.js index 8771f8437ba53..928cfaaec9aff 100644 --- a/packages/edit-post/src/components/options-modal/options/enable-tips.js +++ b/packages/edit-post/src/components/options-modal/options/enable-tips.js @@ -7,7 +7,7 @@ import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import DeferredOption from './deferred'; +import BaseOption from './base'; export default compose( withSelect( ( select ) => ( { @@ -19,9 +19,4 @@ export default compose( onChange: ( isEnabled ) => ( isEnabled ? enableTips() : disableTips() ), }; } ) -)( - // Using DeferredOption here means enableTips() is called when the Options - // modal is dismissed. This stops the NUX guide from appearing above the - // Options modal, which looks totally weird. - DeferredOption -); +)( BaseOption ); diff --git a/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap index a81252b32b95f..906cc1d08da94 100644 --- a/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap @@ -12,7 +12,7 @@ exports[`OptionsModal should match snapshot when the modal is active 1`] = ` - diff --git a/packages/editor/package.json b/packages/editor/package.json index 88ddf540a926a..4b01678854583 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -40,7 +40,6 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", - "@wordpress/nux": "file:../nux", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", "@wordpress/wordcount": "file:../wordcount", diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index b09f50b431269..b77bc4211f654 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -10,7 +10,6 @@ import { Component, renderToString } from '@wordpress/element'; import { Button, Path, SVG } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; -import { DotTip } from '@wordpress/nux'; import { ifCondition, compose } from '@wordpress/compose'; import { applyFilters } from '@wordpress/hooks'; @@ -191,9 +190,6 @@ export class PostPreviewButton extends Component { __( '(opens in a new tab)' ) } - - { __( 'Click “Preview” to load a preview of this page, so you can make sure you’re happy with your blocks.' ) } - ); } diff --git a/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap index fc928b5e12cb9..587f30bf8a91b 100644 --- a/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/post-preview-button/test/__snapshots__/index.js.snap @@ -15,11 +15,6 @@ exports[`PostPreviewButton render() should render currentPostLink otherwise 1`] > (opens in a new tab) - - Click “Preview” to load a preview of this page, so you can make sure you’re happy with your blocks. - `; @@ -38,10 +33,5 @@ exports[`PostPreviewButton render() should render previewLink if provided 1`] = > (opens in a new tab) - - Click “Preview” to load a preview of this page, so you can make sure you’re happy with your blocks. - `; diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index ff3bedf5a79fb..604770d7157ad 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -11,7 +11,6 @@ import { Component, createRef } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; -import { DotTip } from '@wordpress/nux'; /** * Internal dependencies @@ -116,9 +115,6 @@ export class PostPublishButton extends Component { { ...componentProps } > { componentChildren } - - { __( 'Finished writing? That’s great, let’s get this published right now. Just click “Publish” and you’re good to go.' ) } - ); } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index cfa67ddbb615d..631fe2ead7e21 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -104,6 +104,7 @@ class EditorProvider extends Component { __experimentalMediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalFetchLinkSuggestions: fetchLinkSuggestions, __experimentalCanUserUseUnfilteredHTML: canUserUseUnfilteredHTML, + __experimentalEnableTips: true, }; } diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 546a22e7930b4..7ca31c33a7c6d 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,7 @@ +# 3.1.0 (Unreleased) + +- Adds the `InlineTip` component. + ## 3.0.6 (2019-01-03) ## 3.0.5 (2018-12-12) diff --git a/packages/nux/src/components/inline-tip/README.md b/packages/nux/src/components/inline-tip/README.md new file mode 100644 index 0000000000000..dea264ef68dc1 --- /dev/null +++ b/packages/nux/src/components/inline-tip/README.md @@ -0,0 +1,34 @@ +InlineTip +========= + +`InlineTip` is a React component that renders a single _tip_ on the screen. It has the same appearance as a [`Notice`](https://developer.wordpress.org/block-editor/components/notice/). Each tip is uniquely identified by a string passed to `tipId`. + +## Usage + +```jsx + + Add the product to your shopping cart by clicking ‘Add to Cart’. + +``` + +## Props + +The component accepts the following props: + +### tipId + +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 + +### className + +Class name added to the rendered tip. + +- Type: `string` +- Required: Yes + +### children + +Any React element or elements can be passed as children. They will be rendered within the tip. diff --git a/packages/nux/src/components/inline-tip/index.js b/packages/nux/src/components/inline-tip/index.js new file mode 100644 index 0000000000000..35554ab9b73ca --- /dev/null +++ b/packages/nux/src/components/inline-tip/index.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { Notice, Modal, Button } from '@wordpress/components'; +import { compose } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +export function InlineTip( { + children, + className, + hasDismissedAnyTips, + isTipVisible, + onDisableTips, + onDismissTip, +} ) { + const [ isConfirmationVisible, setIsConfirmationVisible ] = useState( false ); + + const dismissNotice = () => { + if ( hasDismissedAnyTips ) { + onDismissTip(); + } else { + setIsConfirmationVisible( true ); + } + }; + + const closeConfirmation = () => { + setIsConfirmationVisible( false ); + }; + + const dismissConfirmation = () => { + onDismissTip(); + closeConfirmation(); + }; + + const acceptConfirmation = () => { + onDisableTips(); + closeConfirmation(); + }; + + return ( + <> + { isTipVisible && ( + + { children } + + ) } + + { isConfirmationVisible && ( + + { __( 'Would you like to disable tips like these in the future?' ) } +
+ + +
+
+ ) } + + ); +} + +export default compose( + withSelect( ( select, { tipId } ) => { + const { isTipVisible, hasDismissedAnyTips } = select( 'core/nux' ); + return { + isTipVisible: isTipVisible( tipId ), + hasDismissedAnyTips: hasDismissedAnyTips(), + }; + } ), + withDispatch( ( dispatch, { tipId } ) => { + const { disableTips, dismissTip } = dispatch( 'core/nux' ); + return { + onDismissTip: () => dismissTip( tipId ), + onDisableTips: disableTips, + }; + } ) +)( InlineTip ); diff --git a/packages/nux/src/components/inline-tip/style.scss b/packages/nux/src/components/inline-tip/style.scss new file mode 100644 index 0000000000000..6911c53ce47fe --- /dev/null +++ b/packages/nux/src/components/inline-tip/style.scss @@ -0,0 +1,13 @@ +.nux-hide-tips-confirmation .components-modal__header { + border-bottom: none; + margin-bottom: 0; +} + +.nux-hide-tips-confirmation__buttons { + margin-top: 2rem; + text-align: right; + + .components-button { + margin-left: 10px; + } +} diff --git a/packages/nux/src/components/inline-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/inline-tip/test/__snapshots__/index.js.snap new file mode 100644 index 0000000000000..3472d2dcd8877 --- /dev/null +++ b/packages/nux/src/components/inline-tip/test/__snapshots__/index.js.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InlineTip should render correctly 1`] = ` + + + It looks like you’re writing a letter. Would you like help? + + +`; diff --git a/packages/nux/src/components/inline-tip/test/index.js b/packages/nux/src/components/inline-tip/test/index.js new file mode 100644 index 0000000000000..0f7c2eb538670 --- /dev/null +++ b/packages/nux/src/components/inline-tip/test/index.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { shallow, mount } from 'enzyme'; + +/** + * Internal dependencies + */ +import { InlineTip } from '../'; + +describe( 'InlineTip', () => { + it( 'should not render anything if invisible', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + expect( wrapper.children() ).toHaveLength( 0 ); + } ); + + it( 'should render correctly', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'calls `onDismissTip` when the tip is dismissed and a tip has already been dismissed', () => { + const onDismissTip = jest.fn(); + const wrapper = mount( + + It looks like you’re writing a letter. Would you like help? + + ); + wrapper.find( 'button[aria-label="Dismiss this notice"]' ).simulate( 'click' ); + expect( onDismissTip ).toHaveBeenCalled(); + } ); + + it( 'calls `onDismissTip` when the tip and confirmation are dismissed', () => { + const onDismissTip = jest.fn(); + const wrapper = mount( + + It looks like you’re writing a letter. Would you like help? + + ); + wrapper.find( 'button[aria-label="Dismiss this notice"]' ).simulate( 'click' ); + wrapper.find( 'button' ).find( { children: 'No' } ).simulate( 'click' ); + expect( onDismissTip ).toHaveBeenCalled(); + } ); + + it( 'calls `onDisableTips` when the tip is dismissed and the confirmation is accepted', () => { + const onDisableTips = jest.fn(); + const wrapper = mount( + + It looks like you’re writing a letter. Would you like help? + + ); + wrapper.find( 'button[aria-label="Dismiss this notice"]' ).simulate( 'click' ); + wrapper.find( 'button' ).find( { children: 'Disable Tips' } ).simulate( 'click' ); + expect( onDisableTips ).toHaveBeenCalled(); + } ); +} ); diff --git a/packages/nux/src/index.js b/packages/nux/src/index.js index a11d17bc96961..f57313de8e788 100644 --- a/packages/nux/src/index.js +++ b/packages/nux/src/index.js @@ -4,3 +4,4 @@ import './store'; export { default as DotTip } from './components/dot-tip'; +export { default as __experimentalInlineTip } from './components/inline-tip'; diff --git a/packages/nux/src/store/selectors.js b/packages/nux/src/store/selectors.js index 9225bd97077ee..74a793c74c382 100644 --- a/packages/nux/src/store/selectors.js +++ b/packages/nux/src/store/selectors.js @@ -2,7 +2,7 @@ * External dependencies */ import createSelector from 'rememo'; -import { includes, difference, keys, has } from 'lodash'; +import { includes, difference, keys, has, isEmpty } from 'lodash'; /** * An object containing information about a guide. @@ -77,3 +77,14 @@ export function isTipVisible( state, tipId ) { export function areTipsEnabled( state ) { return state.preferences.areTipsEnabled; } + +/** + * Returns whether or not any tips have been dismissed. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether any tips have been dismissed. + */ +export function hasDismissedAnyTips( state ) { + return ! isEmpty( state.preferences.dismissedTips ); +} diff --git a/packages/nux/src/store/test/selectors.js b/packages/nux/src/store/test/selectors.js index 546d052958dd5..6a382cd37cd01 100644 --- a/packages/nux/src/store/test/selectors.js +++ b/packages/nux/src/store/test/selectors.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { getAssociatedGuide, isTipVisible, areTipsEnabled } from '../selectors'; +import { getAssociatedGuide, isTipVisible, areTipsEnabled, hasDismissedAnyTips } from '../selectors'; describe( 'selectors', () => { describe( 'getAssociatedGuide', () => { @@ -145,4 +145,28 @@ describe( 'selectors', () => { expect( areTipsEnabled( state ) ).toBe( false ); } ); } ); + + describe( 'hasDismissedAnyTips', () => { + it( 'should return true if a tip has been dismissed', () => { + const state = { + guides: [], + preferences: { + dismissedTips: { + 'test/tip': true, + }, + }, + }; + expect( hasDismissedAnyTips( state ) ).toBe( true ); + } ); + + it( 'should return false if tips are disabled', () => { + const state = { + guides: [], + preferences: { + dismissedTips: {}, + }, + }; + expect( hasDismissedAnyTips( state ) ).toBe( false ); + } ); + } ); } ); diff --git a/packages/nux/src/style.scss b/packages/nux/src/style.scss index 0df73ff851e9f..57e39958ceb7a 100644 --- a/packages/nux/src/style.scss +++ b/packages/nux/src/style.scss @@ -1 +1,2 @@ @import "./components/dot-tip/style.scss"; +@import "./components/inline-tip/style.scss";