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 ef56ca64b3ad77..0af9da47fe4f56 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -15,6 +15,7 @@ import { NavigableToolbar, BlockNavigationDropdown, } from '@wordpress/editor'; +import { rawShortcut, displayShortcut, shortcutAriaLabel } from '@wordpress/keycodes'; /** * Internal dependencies @@ -34,9 +35,19 @@ function HeaderToolbar( { hasFixedToolbar, isLargeViewport, showInserter } ) { 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/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss index 02410b24407a54..791cc9a61429f3 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.scss @@ -22,6 +22,17 @@ } } +.edit-post-header-toolbar__inserter-button-wrapper { + align-items: center; + display: flex; + position: relative; +} + +.edit-post-header-toolbar__inserter-button-tip { + position: absolute; + right: -17px; +} + // Block toolbar when fixed to the top of the screen. .edit-post-header-toolbar__block-toolbar { // Stack toolbar below Editor Bar. diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 0663fcd5f98b36..1ca35c04d1eb98 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -59,7 +59,7 @@ function Header( { forceIsDirty={ hasActiveMetaboxes } forceIsSaving={ isSaving } /> -
+
- + { __( 'You’ll find more settings for your page and blocks in the sidebar. Click “Settings” to open it.' ) }
diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index 987dcbfd5e14cd..36f52ea85d5dce 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -63,11 +63,21 @@ @include editor-left(".edit-post-header"); -.edit-post-header__settings { +.edit-post-header__settings, +.edit-post-header__settings-button-wrapper { display: inline-flex; align-items: center; } +.edit-post-header__settings-button-wrapper { + position: relative; +} + +.edit-post-header__settings-button-tip { + left: -17px; + position: absolute; +} + .edit-post-header .components-button { // Header toggle buttons. &.is-toggled { diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index 5c31e9e271de8f..2295c957b8da46 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -182,7 +182,7 @@ 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/style.scss b/packages/editor/src/components/post-preview-button/style.scss new file mode 100644 index 00000000000000..809cbe235b8ac8 --- /dev/null +++ b/packages/editor/src/components/post-preview-button/style.scss @@ -0,0 +1,10 @@ +.editor-post-preview { + align-items: center; + display: flex; + position: relative; +} + +.editor-post-preview__tip { + left: -17px; + position: absolute; +} 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 cfbde2342e780d..6766ebb3f51268 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,12 @@ 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 +39,11 @@ 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 cfed6dbd112f53..888ece7d7b7dcf 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -107,7 +107,7 @@ 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/post-publish-panel/style.scss b/packages/editor/src/components/post-publish-panel/style.scss index 0faa264e871ad7..7b8e0ef79d7113 100644 --- a/packages/editor/src/components/post-publish-panel/style.scss +++ b/packages/editor/src/components/post-publish-panel/style.scss @@ -40,6 +40,15 @@ padding: 16px; } +.editor-post-publish-panel__toggle { + position: relative; +} + +.editor-post-publish-panel__toggle-tip { + left: -17px; + position: absolute; +} + .components-button.editor-post-publish-panel__toggle.is-primary { display: inline-flex; align-items: center; diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 5b79e1b52407ac..93ad9ec18990f1 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -33,6 +33,7 @@ @import "./components/post-last-revision/style.scss"; @import "./components/post-locked-modal/style.scss"; @import "./components/post-permalink/style.scss"; +@import "./components/post-preview-button/style.scss"; @import "./components/post-publish-panel/style.scss"; @import "./components/post-saved-state/style.scss"; @import "./components/post-taxonomies/style.scss"; diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index ffe9ba7ca937ec..5cd8a88c3cb7c4 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,13 @@ +## 4.0.0 (Unreleased) + +### New Features + +- `DotTip`s can now be made collapsible using the `isCollapsible` prop. + +### Breaking Changes + +- `DotTip` will no longer automatically position its indicator next to the parent component. Instead, users of `DotTip` should manually position the indicator by applying a `className` or styling `.nux-dot-tip`. + ## 3.0.4 (2018-11-30) ## 3.0.3 (2018-11-22) @@ -28,7 +38,7 @@ ### Deprecations -- The id prop of DotTip has been deprecated. Please use the tipId prop instead. +- The `id` prop of `DotTip` has been deprecated. Please use the `tipId` prop instead. ## 2.0.6 (2018-10-22) @@ -38,6 +48,6 @@ ## 2.0.0 (2018-09-05) -### Breaking Change +### Breaking Changes - Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. diff --git a/packages/nux/src/components/dot-tip/README.md b/packages/nux/src/components/dot-tip/README.md index 5cde47c52293d8..ee44a000f85398 100644 --- a/packages/nux/src/components/dot-tip/README.md +++ b/packages/nux/src/components/dot-tip/README.md @@ -29,3 +29,44 @@ A string that uniquely identifies the tip. Identifiers should be prefixed with t ### children Any React element or elements can be passed as children. They will be rendered within the tip bubble. + +### isCollapsible + +Marks a tip as being collapsible. Collapsible tips show a pulsating indicator and nothing else. Clicking on the indicator will expand or collapse the tip. + +Defaults to `false`. + +- Type: `boolean` +- Required: No + +### title + +A short string which describes the tip. This is used to label the button which expands or collapses the tip if it is collapsible. + +```jsx + + Click here to add the product to your shopping cart. + +``` + +- Type: `string` +- Required: No + +### shortcut + +An object which, if specified, configures a keyboard shortcut which will expand or collapse the tip if it is collapsible. + +The object must contain a `raw` property which is the keyboard shortcut to bind to. + +Optionally, the object can contain a `display` property which is a textual description of the shortcut used for the button tooltip. + +Optionally, the object can contain an `ariaLabel` property which is a textual description of the shortcut used for screen readers. + +```jsx + + Click here to add the product to your shopping cart. + +``` + +- Type: `Object` +- Required: No diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js index cd2356f8026690..7e2501b8cd916d 100644 --- a/packages/nux/src/components/dot-tip/index.js +++ b/packages/nux/src/components/dot-tip/index.js @@ -1,60 +1,131 @@ /** * WordPress dependencies */ +import { Component } from '@wordpress/element'; import { compose } from '@wordpress/compose'; -import { Popover, Button, IconButton } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { + Button, + IconButton, + KeyboardShortcuts, + Popover, + Tooltip, + withSpokenMessages, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; -function getAnchorRect( anchor ) { - // The default getAnchorRect() excludes an element's top and bottom padding - // from its calculation. We want tips to point to the outer margin of an - // element, so we override getAnchorRect() to include all padding. - return anchor.parentNode.getBoundingClientRect(); -} - -function onClick( event ) { +function stopEventPropagation( event ) { // Tips are often nested within buttons. We stop propagation so that clicking // on a tip doesn't result in the button being clicked. event.stopPropagation(); } -export function DotTip( { - children, - isVisible, - hasNextTip, - onDismiss, - onDisable, -} ) { - if ( ! isVisible ) { - return null; +export class DotTip extends Component { + constructor( { isCollapsible } ) { + super( ...arguments ); + + this.toggleIsOpen = this.toggleIsOpen.bind( this ); + + this.state = { + isOpen: ! isCollapsible, + }; } - return ( - -

{ children }

-

- -

- -
- ); + componentDidMount() { + const { isCollapsible, shortcut, title, debouncedSpeak } = this.props; + + if ( isCollapsible && shortcut && shortcut.raw && shortcut.ariaLabel && title ) { + debouncedSpeak( + sprintf( __( 'Press “%s” to open the tip for “%s”.' ), shortcut.ariaLabel, title ) + ); + } + } + + toggleIsOpen( event ) { + stopEventPropagation( event ); + + if ( this.props.isCollapsible ) { + this.setState( { isOpen: ! this.state.isOpen } ); + } + } + + render() { + const { + children, + className, + hasNextTip, + isCollapsible, + isVisible, + title, + onDisable, + onDismiss, + shortcut, + } = this.props; + const { isOpen } = this.state; + + if ( ! isVisible ) { + return null; + } + + let classes = 'nux-dot-tip'; + if ( className ) { + classes += ` ${ className }`; + } + + let label; + if ( title ) { + label = isOpen ? + sprintf( __( 'Close tip for “%s”' ), title ) : + sprintf( __( 'Open tip for “%s”' ), title ); + } else { + label = isOpen ? __( 'Close tip' ) : __( 'Open tip' ); + } + + let popover = null; + if ( isOpen ) { + popover = ( + +

{ children }

+

+ +

+ +
+ ); + } + + return isCollapsible ? ( + + + + ) : ( +
{ popover }
+ ); + } } export default compose( @@ -77,4 +148,5 @@ export default compose( }, }; } ), + withSpokenMessages )( DotTip ); diff --git a/packages/nux/src/components/dot-tip/style.scss b/packages/nux/src/components/dot-tip/style.scss index e8e25b423830ca..693d3d346ce293 100644 --- a/packages/nux/src/components/dot-tip/style.scss +++ b/packages/nux/src/components/dot-tip/style.scss @@ -1,40 +1,53 @@ -$dot-size: 8px; // Size of the indicator dot +$dot-size: 12px; // 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; + background: $blue-medium-800; + border-radius: 100%; + border: none; + height: $dot-size; + outline: none; + padding: 0; + position: relative; + width: $dot-size; + z-index: z-index(".nux-dot-tip"); + + &:active { + outline: none; + } + + &:focus { + // TODO: Would look nicer if this focus outline were a circle. + outline-offset: ($dot-size * $dot-scale - $dot-size) / 2; + outline: 1px solid $dark-gray-300; } &::before { animation: nux-pulse 1.6s infinite cubic-bezier(0.17, 0.67, 0.92, 0.62); background: rgba($blue-medium-800, 0.9); + border-radius: 100%; + content: " "; height: $dot-size * $dot-scale; - left: -($dot-size * $dot-scale) / 2; - top: -($dot-size * $dot-scale) / 2; - transform: scale((1 / $dot-scale)); + left: -$dot-size; + position: absolute; + top: -$dot-size; + 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; - } +button.nux-dot-tip { + cursor: pointer; +} - @keyframes nux-pulse { - 100% { - background: rgba($blue-medium-800, 0); - transform: scale(1); - } +@keyframes nux-pulse { + 100% { + background: rgba($blue-medium-800, 0); + transform: scale(1); } +} +.nux-dot-tip__popover { .components-popover__content { padding: 5px (36px + 5px) 5px 20px; width: 350px; @@ -50,20 +63,6 @@ $dot-scale: 3; // How much the pulse animation should scale up by in size } } - // Position the dot right next to the edge of the button - &.is-top { - margin-top: -$dot-size / 2; - } - &.is-bottom { - margin-top: $dot-size / 2; - } - &.is-middle.is-left { - margin-left: -$dot-size / 2; - } - &.is-middle.is-right { - margin-left: $dot-size / 2; - } - // Position the tip content away from the dot &.is-top .components-popover__content { margin-bottom: 20px; diff --git a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap index d9639628174e7d..639e413c6ed472 100644 --- a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap +++ b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap @@ -1,30 +1,95 @@ // 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? -

-

- -

- -
+ +`; + +exports[`DotTip should render a custom label when collapsible 1`] = ` + + +

+ + + +
+`; + +exports[`DotTip should render the tip when non-collapsible 1`] = ` +
+ +

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

+

+ +

+ +
+
`; diff --git a/packages/nux/src/components/dot-tip/test/index.js b/packages/nux/src/components/dot-tip/test/index.js index aa69872d5e6e80..f9070aecf01148 100644 --- a/packages/nux/src/components/dot-tip/test/index.js +++ b/packages/nux/src/components/dot-tip/test/index.js @@ -2,7 +2,6 @@ * External dependencies */ import { shallow } from 'enzyme'; -import { noop } from 'lodash'; /** * Internal dependencies @@ -21,19 +20,52 @@ describe( 'DotTip', () => { expect( wrapper.isEmptyRender() ).toBe( true ); } ); - it( 'should render correctly', () => { + it( 'should render the tip when non-collapsible', () => { const wrapper = shallow( - + It looks like you’re writing a letter. Would you like help? ); expect( wrapper ).toMatchSnapshot(); } ); + it( 'should render a button when collapsible', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'should render a custom label when collapsible', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'should render the tip after being clicked when collapsible', () => { + const wrapper = shallow( + + It looks like you’re writing a letter. Would you like help? + + ); + + const stopPropagation = jest.fn(); + wrapper.find( 'button' ).simulate( 'click', { stopPropagation } ); + wrapper.update(); + + expect( wrapper ).toMatchSnapshot(); + expect( stopPropagation ).toHaveBeenCalled(); + } ); + 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? ); @@ -44,7 +76,7 @@ describe( 'DotTip', () => { it( 'should call onDisable when the X button is clicked', () => { const onDisable = jest.fn(); const wrapper = shallow( - + It looks like you’re writing a letter. Would you like help? ); diff --git a/test/e2e/specs/nux.test.js b/test/e2e/specs/nux.test.js index 8702587f6b55b7..225c1bd7baee2b 100644 --- a/test/e2e/specs/nux.test.js +++ b/test/e2e/specs/nux.test.js @@ -11,12 +11,15 @@ import { describe( 'New User Experience (NUX)', () => { async function clickAllTips( page ) { - // Click through all available tips. const tips = await getTips( page ); const numberOfTips = tips.tipIds.length; + // Open up the first tip. + await page.click( '.nux-dot-tip' ); + + // Click through all of the tips. for ( let i = 1; i < numberOfTips; i++ ) { - await page.click( '.nux-dot-tip .components-button.is-link' ); + await page.click( '.nux-dot-tip__popover .components-button.is-link' ); } return { numberOfTips, tips }; @@ -39,13 +42,19 @@ describe( 'New User Experience (NUX)', () => { } ); it( 'should show tips to a first-time user', async () => { - const firstTipText = await page.$eval( '.nux-dot-tip', ( element ) => element.innerText ); + // Click on the tip indicator to open the first tip. + await page.click( '.nux-dot-tip' ); + + // Check that we see the first tip. + const firstTipText = await page.$eval( '.nux-dot-tip__popover', ( element ) => element.innerText ); expect( firstTipText ).toContain( 'Welcome to the wonderful world of blocks!' ); + // Go to the next tip. const [ nextTipButton ] = await page.$x( "//button[contains(text(), 'See next tip')]" ); await nextTipButton.click(); - const secondTipText = await page.$eval( '.nux-dot-tip', ( element ) => element.innerText ); + // Check that we see the second tip. + const secondTipText = await page.$eval( '.nux-dot-tip__popover', ( element ) => element.innerText ); expect( secondTipText ).toContain( 'You’ll find more settings for your page and blocks in the sidebar.' ); } ); @@ -57,7 +66,7 @@ describe( 'New User Experience (NUX)', () => { expect( gotItButton ).toHaveLength( 1 ); // Click the "Got it button". - await page.click( '.nux-dot-tip .components-button.is-link' ); + await page.click( '.nux-dot-tip__popover .components-button.is-link' ); // Verify no more tips are visible on the page. const nuxTipElements = await page.$$( '.nux-dot-tip' ); @@ -70,6 +79,10 @@ describe( 'New User Experience (NUX)', () => { } ); it( 'should hide and disable tips if "disable tips" button is clicked', async () => { + // Click on the tip indicator to open the first tip. + await page.click( '.nux-dot-tip' ); + + // Click the X. await page.click( '.nux-dot-tip__disable' ); // Verify no more tips are visible on the page. @@ -88,7 +101,10 @@ describe( 'New User Experience (NUX)', () => { } ); it( 'should enable tips when the "Enable tips" option is toggled on', async () => { - // Start by disabling tips. + // Click on the tip indicator to open the first tip. + await page.click( '.nux-dot-tip' ); + + // Click the X to disable tips. await page.click( '.nux-dot-tip__disable' ); // Verify no more tips are visible on the page.