diff --git a/blocks/library/columns/editor.scss b/blocks/library/columns/editor.scss new file mode 100644 index 0000000000000..f23c87cb7dc13 --- /dev/null +++ b/blocks/library/columns/editor.scss @@ -0,0 +1,54 @@ +// These margins make sure that nested blocks stack/overlay with the parent block chrome +// This is sort of an experiment at making sure the editor looks as much like the end result as possible +// Potentially the rules here can apply to all nested blocks and enable stacking, in which case it should be moved elsewhere +.wp-block-columns .editor-block-list__layout { + &:first-child { + margin-left: -$block-padding; + } + &:last-child { + margin-right: -$block-padding; + } + + // This max-width is used to constrain the main editor column, it should not cascade into columns + .editor-block-list__block { + max-width: none; + } +} + +// Wide: show no left/right margin on wide, so they stack with the column side UI +.editor-block-list__block[data-align="wide"] .wp-block-columns .editor-block-list__layout { + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } +} + +// Fullwide: show margin left/right to ensure there's room for the side UI +// This is not a 1:1 preview with the front-end where these margins would presumably be zero +// @todo this could be revisited, by for example showing this margin only when the parent block was selected first +// Then at least an unselected columns block would be an accurate preview +.editor-block-list__block[data-align="full"] .wp-block-columns .editor-block-list__layout { + &:first-child { + margin-left: $block-side-ui-padding; + } + &:last-child { + margin-right: $block-side-ui-padding; + } +} + +// Hide appender shortcuts in columns +// @todo This essentially duplicates the mobile styles for the appender component +// It would be nice to be able to use element queries in that component instead https://github.com/tomhodgins/element-queries-spec +.wp-block-columns { + .editor-inserter-with-shortcuts { + display: none; + } + + .editor-block-list__empty-block-inserter, + .editor-default-block-appender .editor-inserter { + left: auto; + right: $item-spacing; + } +} diff --git a/blocks/library/columns/index.js b/blocks/library/columns/index.js index 896fe5d845233..9079f31fad13f 100644 --- a/blocks/library/columns/index.js +++ b/blocks/library/columns/index.js @@ -15,6 +15,7 @@ import { RangeControl } from '@wordpress/components'; * Internal dependencies */ import './style.scss'; +import './editor.scss'; import InspectorControls from '../../inspector-controls'; import BlockControls from '../../block-controls'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; diff --git a/blocks/library/paragraph/editor.scss b/blocks/library/paragraph/editor.scss index e5fe10296b7f1..36c22e7acfc45 100644 --- a/blocks/library/paragraph/editor.scss +++ b/blocks/library/paragraph/editor.scss @@ -2,6 +2,11 @@ background: white; } +// Don't show white background when a nesting parent is selected +.editor-block-list__layout .editor-block-list__layout .editor-block-list__block .wp-block-paragraph { + background: inherit; +} + .blocks-font-size__main { display: flex; justify-content: space-between; diff --git a/edit-post/assets/stylesheets/_variables.scss b/edit-post/assets/stylesheets/_variables.scss index 77b3c859f27a8..58d372a3cec67 100644 --- a/edit-post/assets/stylesheets/_variables.scss +++ b/edit-post/assets/stylesheets/_variables.scss @@ -46,6 +46,7 @@ $block-padding: 14px; $block-mover-margin: 18px; $block-spacing: 4px; $block-side-ui-padding: 36px; +$block-side-ui-width: 28px; // The side UI max height matches a single line of text, 56px. 28px is half, allowing 2 mover arrows // Buttons & UI Widgets $button-style__radius-roundrect: 4px; diff --git a/edit-post/assets/stylesheets/_z-index.scss b/edit-post/assets/stylesheets/_z-index.scss index 65bd9cfa18456..b701ad4f8257e 100644 --- a/edit-post/assets/stylesheets/_z-index.scss +++ b/edit-post/assets/stylesheets/_z-index.scss @@ -33,6 +33,10 @@ $z-layers: ( '.components-drop-zone': 100, '.components-drop-zone__content': 110, + // Block controls, particularly in nested contexts, floats aside block and + // should overlap most block content. + '.editor-block-list__block.is-{selected,hovered} .editor-block-{settings-menu,mover}': 100, + // Show sidebar above wp-admin navigation bar for mobile viewports: // #wpadminbar { z-index: 99999 } '.edit-post-sidebar': 100000, diff --git a/edit-post/components/visual-editor/style.scss b/edit-post/components/visual-editor/style.scss index d3b58ae4e2d90..7c8a15dd8e9b8 100644 --- a/edit-post/components/visual-editor/style.scss +++ b/edit-post/components/visual-editor/style.scss @@ -47,8 +47,8 @@ margin-right: -$block-side-ui-padding; } - &[data-align="full"] .editor-block-contextual-toolbar, - &[data-align="wide"] .editor-block-contextual-toolbar { + &[data-align="full"] > .editor-block-contextual-toolbar, + &[data-align="wide"] > .editor-block-contextual-toolbar { // don't affect nested block toolbars max-width: $content-width + 2; // 1px border left and right margin-left: auto; margin-right: auto; diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index ec4059d9e1924..170c14e5abc8f 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -39,6 +39,7 @@ import InvalidBlockWarning from './invalid-block-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; +import BlockBreadcrumb from './breadcrumb'; import BlockContextualToolbar from './block-contextual-toolbar'; import BlockMultiControls from './multi-controls'; import BlockMobileToolbar from './block-mobile-toolbar'; @@ -438,11 +439,11 @@ export class BlockListBlock extends Component { // Insertion point can only be made visible when the side inserter is // not present, and either the block is at the extent of a selection or - // is the last block in the top-level list rendering. + // is the first block in the top-level list rendering. const shouldShowInsertionPoint = ( - ( ! isMultiSelected && ! isLast ) || + ( ! isMultiSelected && ! isFirst ) || ( isMultiSelected && isLastInSelection ) || - ( isLast && ! rootUID && ! isEmptyDefaultBlock ) + ( isFirst && ! rootUID && ! isEmptyDefaultBlock ) ); // Generate the wrapper class names handling the different states of the block. @@ -479,6 +480,7 @@ export class BlockListBlock extends Component { + { shouldShowInsertionPoint && ( + + ) } ) } + { isHovered && } { shouldShowContextualToolbar && } { isFirstMultiSelected && } { !! error && } - { shouldShowInsertionPoint && ( - - ) } { showSideInserter && (
diff --git a/editor/components/block-list/breadcrumb.js b/editor/components/block-list/breadcrumb.js new file mode 100644 index 0000000000000..e2faca50cd606 --- /dev/null +++ b/editor/components/block-list/breadcrumb.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/element'; +import { Dashicon, Tooltip, Toolbar, Button } from '@wordpress/components'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import NavigableToolbar from '../navigable-toolbar'; +import BlockTitle from '../block-title'; + +/** + * Stops propagation of the given event argument. Assumes that the event has + * been completely handled and no other listeners should be informed. + * + * For the breadcrumb component, this is used for improved interoperability + * with the block's `onFocus` handler which selects the block, thus conflicting + * with the intention to select the root block. + * + * @param {Event} event Event for which propagation should be stopped. + */ +function stopPropagation( event ) { + event.stopPropagation(); +} + +/** + * Block breadcrumb component, displaying the label of the block. If the block + * descends from a root block, a button is displayed enabling the user to select + * the root block. + * + * @param {string} props.uid UID of block. + * @param {string} props.rootUID UID of block's root. + * @param {Function} props.selectRootBlock Callback to select root block. + * + * @return {WPElement} Breadcrumb element. + */ +function BlockBreadcrumb( { uid, rootUID, selectRootBlock } ) { + return ( + + + { rootUID && ( + + + + ) } + + + + ); +} + +export default compose( [ + withSelect( ( select, ownProps ) => { + const { getBlockRootUID } = select( 'core/editor' ); + const { uid } = ownProps; + + return { + rootUID: getBlockRootUID( uid ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { rootUID } = ownProps; + const { selectBlock } = dispatch( 'core/editor' ); + + return { + selectRootBlock: () => selectBlock( rootUID ), + }; + } ), +] )( BlockBreadcrumb ); diff --git a/editor/components/block-list/ignore-nested-events.js b/editor/components/block-list/ignore-nested-events.js index fe13f37df13b8..7cdc45063d4a9 100644 --- a/editor/components/block-list/ignore-nested-events.js +++ b/editor/components/block-list/ignore-nested-events.js @@ -43,10 +43,7 @@ class IgnoreNestedEvents extends Component { * @return {void} */ proxyEvent( event ) { - // Skip if already handled (i.e. assume nested block) - if ( event.nativeEvent._blockHandled ) { - return; - } + const isHandled = !! event.nativeEvent._blockHandled; // Assign into the native event, since React will reuse their synthetic // event objects and this property assignment could otherwise leak. @@ -55,7 +52,14 @@ class IgnoreNestedEvents extends Component { event.nativeEvent._blockHandled = true; // Invoke original prop handler - const propKey = this.eventMap[ event.type ]; + let propKey = this.eventMap[ event.type ]; + + // If already handled (i.e. assume nested block), only invoke a + // corresponding "Handled"-suffixed prop callback. + if ( isHandled ) { + propKey += 'Handled'; + } + if ( this.props[ propKey ] ) { this.props[ propKey ]( event ); } @@ -69,16 +73,24 @@ class IgnoreNestedEvents extends Component { ...Object.keys( props ), ], ( result, key ) => { // Try to match prop key as event handler - const match = key.match( /^on([A-Z][a-zA-Z]+)$/ ); + const match = key.match( /^on([A-Z][a-zA-Z]+?)(Handled)?$/ ); if ( match ) { + const isHandledProp = !! match[ 2 ]; + if ( isHandledProp ) { + // Avoid assigning through the invalid prop key. This + // assumes mutation of shallow clone by above spread. + delete props[ key ]; + } + // Re-map the prop to the local proxy handler to check whether // the event has already been handled. - result[ key ] = this.proxyEvent; + const proxiedPropName = 'on' + match[ 1 ]; + result[ proxiedPropName ] = this.proxyEvent; // Assign event -> propName into an instance variable, so as to // avoid re-renders which could be incurred either by setState // or in mapping values to a newly created function. - this.eventMap[ match[ 1 ].toLowerCase() ] = key; + this.eventMap[ match[ 1 ].toLowerCase() ] = proxiedPropName; } return result; diff --git a/editor/components/block-list/insertion-point.js b/editor/components/block-list/insertion-point.js index f296f88cd5a5d..2f94cda3f6e61 100644 --- a/editor/components/block-list/insertion-point.js +++ b/editor/components/block-list/insertion-point.js @@ -60,7 +60,7 @@ export default compose( connect( ( state, { uid, rootUID } ) => { const blockIndex = uid ? getBlockIndex( state, uid, rootUID ) : -1; - const insertIndex = blockIndex + 1; + const insertIndex = blockIndex; const insertionPoint = getBlockInsertionPoint( state ); const block = uid ? getBlock( state, uid ) : null; diff --git a/editor/components/block-list/layout.js b/editor/components/block-list/layout.js index f688787c95658..d1e33331dc5c1 100644 --- a/editor/components/block-list/layout.js +++ b/editor/components/block-list/layout.js @@ -24,7 +24,6 @@ import { Component } from '@wordpress/element'; */ import './style.scss'; import BlockListBlock from './block'; -import BlockInsertionPoint from './insertion-point'; import IgnoreNestedEvents from './ignore-nested-events'; import DefaultBlockAppender from '../default-block-appender'; import { @@ -216,7 +215,6 @@ class BlockListLayout extends Component { return (
- { !! blockUIDs.length && } { map( blockUIDs, ( uid, blockIndex ) => ( .editor-block-mover:before, &.is-hovered > .editor-block-mover:before { border-right: 1px solid $light-gray-500; - right: 6px; + right: 0; + } + + &.is-selected > .editor-block-settings-menu:before, + &.is-hovered > .editor-block-settings-menu:before { + border-left: 1px solid $light-gray-500; + left: 0; } &.is-typing .editor-block-list__empty-block-inserter, @@ -92,12 +108,6 @@ transition: opacity 0.2s; } - &.is-selected > .editor-block-settings-menu:before, - &.is-hovered > .editor-block-settings-menu:before { - border-left: 1px solid $light-gray-500; - left: 6px; - } - /** * Selected Block style */ @@ -160,26 +170,6 @@ outline: 1px dashed $light-gray-500; } - // @todo, this appears to be unused - .iframe-overlay { - position: relative; - } - - .iframe-overlay:before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - } - - &.is-selected .iframe-overlay:before { - display: none; - } - - /** * Alignments */ @@ -252,7 +242,7 @@ margin-right: -$block-side-ui-padding; } - .editor-block-list__block-edit { + > .editor-block-list__block-edit { margin-left: -$block-padding; margin-right: -$block-padding; @@ -262,41 +252,39 @@ } } - .editor-block-list__block-edit:before { + > .editor-block-list__block-edit:before { left: 0; right: 0; border-left-width: 0; border-right-width: 0; } - .editor-block-mover { + // Adjust how movers behave on the full-wide block, and don't affect children + > .editor-block-mover { display: none; } @include break-wide() { - .editor-block-mover { + > .editor-block-mover { display: block; top: -29px; left: 10px; height: auto; + width: auto; + z-index: inherit; &:before { content: none; } } - .editor-block-mover__control { + > .editor-block-mover .editor-block-mover__control { float: left; - margin-right: 8px; } } - .editor-block-settings-menu__control { - float: left; - margin-right: 8px; - } - - .editor-block-settings-menu { + // Also adjust block settings menu + > .editor-block-settings-menu { top: -41px; right: 10px; height: auto; @@ -305,6 +293,10 @@ content: none; } } + + > .editor-block-settings-menu .editor-block-settings-menu__control { + float: left; + } } // Clear floats @@ -334,23 +326,37 @@ border-bottom: 3px solid $blue-medium-500; } } +} - /** - * Left and right side UI - */ +/** + * Left and right side UI + */ + +.editor-block-list__block { + // Left and right > .editor-block-settings-menu, > .editor-block-mover { position: absolute; - top: 0; + top: -.9px; // .9px because of the collapsing margins hack, see line 27, @todo revisit when we allow margins to collapse + bottom: -.9px; // utilize full vertical space to increase hoverable area padding: 0; + width: $block-side-ui-width; + } + + // Elevate when selected or hovered + &.is-selected, + &.is-hovered { + .editor-block-settings-menu, + .editor-block-mover { + z-index: z-index( '.editor-block-list__block.is-{selected,hovered} .editor-block-{settings-menu,mover}' ); + } } // Right side UI > .editor-block-settings-menu { - right: #{ -1 * $block-mover-margin - $block-padding + 2px }; - padding-top: 2px; + right: -$block-side-ui-width; // Mobile display: none; @@ -362,8 +368,7 @@ // Left side UI > .editor-block-mover { - left: -$block-mover-margin - $block-padding + 4px; - padding-top: 6px; + left: -$block-side-ui-width; z-index: z-index( '.editor-block-mover' ); // Mobile @@ -373,7 +378,7 @@ } } - // Mobile tools + // Show side UI inline below the block on mobile .editor-block-list__block-mobile-toolbar { display: flex; flex-direction: row; @@ -420,6 +425,11 @@ } } + +/** + * In-Canvas Inserter + */ + .editor-block-list .editor-inserter { margin: $item-spacing; @@ -489,22 +499,11 @@ } } -// In between blocks .editor-block-list__block > .editor-block-list__insertion-point { position: absolute; - bottom: -$block-padding; - height: $block-padding * 2; // Matches the whole empty space between two blocks - top: auto; - left: 0; - right: 0; -} - -// Before the first block -.editor-block-list__layout > .editor-block-list__insertion-point { - position: relative; top: -$block-padding; - margin-left: auto; - margin-right: auto; + height: $block-padding * 2; // Matches the whole empty space between two blocks + bottom: auto; left: 0; right: 0; } @@ -533,7 +532,8 @@ * Block Toolbar */ -.editor-block-contextual-toolbar { +.editor-block-contextual-toolbar, +.editor-block-breadcrumb { position: sticky; z-index: z-index( '.editor-block-contextual-toolbar' ); white-space: nowrap; @@ -557,8 +557,15 @@ margin-right: -$block-padding - 1px; @include break-small() { - margin-left: -$block-padding - $block-side-ui-padding - 1px; // stack borders + // stack borders + margin-left: -$block-padding - $block-side-ui-padding - 1px; margin-right: -$block-padding - $block-side-ui-padding - 1px; + + // except for wide elements, this causes a horizontal scrollbar + [data-align="full"] & { + margin-left: -$block-padding - $block-side-ui-padding; + margin-right: -$block-padding - $block-side-ui-padding; + } } // on mobile, toolbars fix differently @@ -567,35 +574,54 @@ top: -1px; // stack borders } - .editor-block-toolbar { - border: 1px solid $light-gray-500; - width: 100%; + // Reset pointer-events on children. + & > * { + pointer-events: auto; + } - // this prevents floats from messing up the position - position: absolute; - left: 0; +} - .editor-block-list__block[data-align="right"] & { - left: auto; - right: 0; - } +.editor-block-contextual-toolbar .editor-block-toolbar, +.editor-block-breadcrumb .components-toolbar { + border: 1px solid $light-gray-500; + width: 100%; - // remove stacked borders in inline toolbar - > div:first-child { - margin-left: -1px; - } + // this prevents floats from messing up the position + position: absolute; + left: 0; - > .editor-block-switcher:first-child { - margin-left: -2px; - } + .editor-block-list__block[data-align="right"] & { + left: auto; + right: 0; + } - @include break-small() { - width: auto; - } + // remove stacked borders in inline toolbar + > div:first-child { + margin-left: -1px; } - // Reset pointer-events on children. - & > * { - pointer-events: auto; + > .editor-block-switcher:first-child { + margin-left: -2px; + } + + @include break-small() { + width: auto; + } +} + +.editor-block-breadcrumb .components-toolbar { + padding: 0px 12px; + line-height: $block-toolbar-height - 1px; + font-family: $default-font; + font-size: $default-font-size; + color: $dark-gray-500; + cursor: default; + + .components-button { + margin-left: -12px; + margin-right: 12px; + border-right: 1px solid $light-gray-500; + color: $dark-gray-500; + padding-top: 6px; } } diff --git a/editor/components/block-list/test/ignore-nested-events.js b/editor/components/block-list/test/ignore-nested-events.js index 73f9c63660706..48559baf29909 100644 --- a/editor/components/block-list/test/ignore-nested-events.js +++ b/editor/components/block-list/test/ignore-nested-events.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; /** * Internal dependencies @@ -10,24 +10,24 @@ import IgnoreNestedEvents from '../ignore-nested-events'; describe( 'IgnoreNestedEvents', () => { it( 'passes props to its rendered div', () => { - const wrapper = shallow( + const wrapper = mount( ); - expect( wrapper.type() ).toBe( 'div' ); + expect( wrapper.find( 'div' ) ).toHaveLength( 1 ); expect( wrapper.prop( 'className' ) ).toBe( 'foo' ); } ); it( 'stops propagation of events to ancestor IgnoreNestedEvents', () => { const spyOuter = jest.fn(); const spyInner = jest.fn(); - const wrapper = shallow( + const wrapper = mount( ); - wrapper.childAt( 0 ).simulate( 'click' ); + wrapper.find( 'div' ).last().simulate( 'click' ); expect( spyInner ).toHaveBeenCalled(); expect( spyOuter ).not.toHaveBeenCalled(); @@ -36,7 +36,7 @@ describe( 'IgnoreNestedEvents', () => { it( 'stops propagation of child handled events', () => { const spyOuter = jest.fn(); const spyInner = jest.fn(); - const wrapper = shallow( + const wrapper = mount(
@@ -51,4 +51,23 @@ describe( 'IgnoreNestedEvents', () => { expect( spyInner ).not.toHaveBeenCalled(); expect( spyOuter ).not.toHaveBeenCalled(); } ); + + it( 'invokes callback of Handled-suffixed prop if handled', () => { + const spyOuter = jest.fn(); + const spyInner = jest.fn(); + const wrapper = mount( + + +
+ + + + ); + + const div = wrapper.childAt( 0 ).childAt( 0 ); + div.simulate( 'click' ); + + expect( spyInner ).not.toHaveBeenCalled(); + expect( spyOuter ).toHaveBeenCalled(); + } ); } ); diff --git a/editor/components/block-mover/style.scss b/editor/components/block-mover/style.scss index 985f0239e370f..314ec7fa168cb 100644 --- a/editor/components/block-mover/style.scss +++ b/editor/components/block-mover/style.scss @@ -1,15 +1,14 @@ // Mover icon buttons .editor-block-mover__control { display: block; - padding: 2px; - margin: 0 6px 0 4px; border: none; outline: none; background: none; color: $dark-gray-300; cursor: pointer; - border-radius: 50%; - width: $icon-button-size-small; + padding: 0; + width: $block-side-ui-width; + height: $block-side-ui-width; // the side UI can be no taller than 2 * $block-side-ui-width, which matches the height of a line of text &[aria-disabled="true"] { cursor: default; @@ -17,6 +16,23 @@ pointer-events: none; } + // Try a background, only for nested situations @todo + @include break-small() { + .editor-block-list__layout .editor-block-list__layout & { + background: $white; + border-color: $light-gray-500; + border-style: solid; + border-width: 1px; + + &:first-child { + border-width: 1px 1px 0 1px; + } + &:last-child { + border-width: 0 1px 1px 1px; + } + } + } + // apply styles to SVG for movers on the desktop breakpoint @include break-small { // unstyle inherited icon button styles diff --git a/editor/components/block-settings-menu/style.scss b/editor/components/block-settings-menu/style.scss index 901c14999acd7..278e485aaf873 100644 --- a/editor/components/block-settings-menu/style.scss +++ b/editor/components/block-settings-menu/style.scss @@ -1,3 +1,26 @@ +// The Blocks "More" Menu ellipsis icon button +.editor-block-settings-menu__toggle { + justify-content: center; + padding: 0; + width: $block-side-ui-width; + height: $block-side-ui-width * 2; // same height as a single line of text, our smallest block + + // Try a background, only for nested situations @todo + @include break-small() { + .editor-block-list__layout .editor-block-list__layout & { + background: $white; + border-color: $light-gray-500; + border-style: solid; + border-width: 1px; + } + } + + .dashicon { + transform: rotate( 90deg ); + } +} + +// Popout menu .editor-block-settings-menu__popover { z-index: z-index( '.editor-block-settings-menu__popover' ); @@ -9,57 +32,44 @@ .components-popover__content { width: 182px; } -} - -.editor-block-settings-menu__content { - width: 100%; -} -// The ellipsis icon button -.editor-block-settings-menu__toggle { - border-radius: 50%; - width: auto; - padding: 2px; - margin: 14px 4px 14px 8px; - width: $icon-button-size-small; - - .dashicon { - transform: rotate( 90deg ); + .editor-block-settings-menu__content { + width: 100%; } -} -.editor-block-settings-menu__separator { - margin-top: $item-spacing; - margin-bottom: $item-spacing; - border-top: 1px solid $light-gray-500; -} + .editor-block-settings-menu__separator { + margin-top: $item-spacing; + margin-bottom: $item-spacing; + border-top: 1px solid $light-gray-500; + } -.editor-block-settings-menu__title { - display: block; - padding: 6px; - color: $dark-gray-300; -} + .editor-block-settings-menu__title { + display: block; + padding: 6px; + color: $dark-gray-300; + } -// Popout menu -.editor-block-settings-menu__control { - width: 100%; - justify-content: flex-start; - padding: 8px; - background: none; - outline: none; - border-radius: 0; - color: $dark-gray-500; - text-align: left; - cursor: pointer; - @include menu-style__neutral; + // Menu items + .editor-block-settings-menu__control { + width: 100%; + justify-content: flex-start; + padding: 8px; + background: none; + outline: none; + border-radius: 0; + color: $dark-gray-500; + text-align: left; + cursor: pointer; + @include menu-style__neutral; - &:hover, - &:focus, - &:not(:disabled):hover { - @include menu-style__focus; - } + &:hover, + &:focus, + &:not(:disabled):hover { + @include menu-style__focus; + } - .dashicon { - margin-right: 5px; + .dashicon { + margin-right: 5px; + } } } diff --git a/editor/components/block-title/README.md b/editor/components/block-title/README.md new file mode 100644 index 0000000000000..9bf6964f44d11 --- /dev/null +++ b/editor/components/block-title/README.md @@ -0,0 +1,10 @@ +Block Title +=========== + +Renders the block's configured title as a string, or empty if the title cannot be determined. + +## Usage + +```jsx + +``` diff --git a/editor/components/block-title/index.js b/editor/components/block-title/index.js new file mode 100644 index 0000000000000..0dd1c1232f768 --- /dev/null +++ b/editor/components/block-title/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { getBlockType } from '@wordpress/blocks'; + +/** + * Renders the block's configured title as a string, or empty if the title + * cannot be determined. + * + * @example + * + * ```jsx + * + * ``` + * + * @param {?string} props.name Block name. + * + * @return {?string} Block title. + */ +export function BlockTitle( { name } ) { + if ( ! name ) { + return null; + } + + const blockType = getBlockType( name ); + if ( ! blockType ) { + return null; + } + + return blockType.title; +} + +export default withSelect( ( select, ownProps ) => { + const { getBlockName } = select( 'core/editor' ); + const { uid } = ownProps; + + return { + name: getBlockName( uid ), + }; +} )( BlockTitle ); diff --git a/editor/components/block-title/test/index.js b/editor/components/block-title/test/index.js new file mode 100644 index 0000000000000..4306ae6247b93 --- /dev/null +++ b/editor/components/block-title/test/index.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { BlockTitle } from '../'; + +jest.mock( '@wordpress/blocks', () => { + return { + getBlockType( name ) { + switch ( name ) { + case 'name-not-exists': + return null; + + case 'name-exists': + return { title: 'Block Title' }; + } + }, + }; +} ); + +describe( 'BlockTitle', () => { + it( 'renders nothing if name is falsey', () => { + const wrapper = shallow( ); + + expect( wrapper.type() ).toBe( null ); + } ); + + it( 'renders nothing if block type does not exist', () => { + const wrapper = shallow( ); + + expect( wrapper.type() ).toBe( null ); + } ); + + it( 'renders title if block type exists', () => { + const wrapper = shallow( ); + + expect( wrapper.text() ).toBe( 'Block Title' ); + } ); +} ); diff --git a/editor/components/default-block-appender/style.scss b/editor/components/default-block-appender/style.scss index b462662158fbc..1a4181ae3300c 100644 --- a/editor/components/default-block-appender/style.scss +++ b/editor/components/default-block-appender/style.scss @@ -11,6 +11,7 @@ $empty-paragraph-height: $text-editor-font-size * 4; padding: $block-padding; height: $empty-paragraph-height; font-size: $editor-font-size; + font-family: $editor-font; cursor: text; width: 100%; height: $empty-paragraph-height; diff --git a/editor/components/index.js b/editor/components/index.js index e67d431fb1b44..3280e8fa2d65c 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -57,6 +57,7 @@ export { default as BlockList } from './block-list'; export { default as BlockMover } from './block-mover'; export { default as BlockSelectionClearer } from './block-selection-clearer'; export { default as BlockSettingsMenu } from './block-settings-menu'; +export { default as BlockTitle } from './block-title'; export { default as BlockToolbar } from './block-toolbar'; export { default as CopyHandler } from './copy-handler'; export { default as DefaultBlockAppender } from './default-block-appender'; diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 6e2ccbf6a3473..11a092a86f082 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -379,6 +379,20 @@ export const getBlockDependantsCacheBust = createSelector( ), ); +/** + * Returns a block's name given its UID, or null if no block exists with the + * UID. + * + * @param {Object} state Editor state. + * @param {string} uid Block unique ID. + * + * @return {string} Block name. + */ +export function getBlockName( state, uid ) { + const block = state.editor.present.blocksByUid[ uid ]; + return block ? block.name : null; +} + /** * Returns a block given its unique ID. This is a parsed copy of the block, * containing its `blockName`, identifier (`uid`), and current `attributes` diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index ccc3fc33cfc47..6aaae2447266e 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -39,6 +39,7 @@ const { isEditedPostBeingScheduled, getEditedPostPreviewLink, getBlockDependantsCacheBust, + getBlockName, getBlock, getBlocks, getBlockCount, @@ -1222,6 +1223,51 @@ describe( 'selectors', () => { } ); } ); + describe( 'getBlockName', () => { + it( 'returns null if no block by uid', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUid: {}, + blockOrder: {}, + edits: {}, + }, + }, + }; + + const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + + expect( name ).toBe( null ); + } ); + + it( 'returns block name', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUid: { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + uid: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + name: 'core/paragraph', + attributes: {}, + }, + }, + blockOrder: { + '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], + }, + edits: {}, + }, + }, + }; + + const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + + expect( name ).toBe( 'core/paragraph' ); + } ); + } ); + describe( 'getBlock', () => { it( 'should return the block', () => { const state = { diff --git a/test/e2e/specs/adding-blocks.test.js b/test/e2e/specs/adding-blocks.test.js index 5ab3af5b4742d..ec1809272637b 100644 --- a/test/e2e/specs/adding-blocks.test.js +++ b/test/e2e/specs/adding-blocks.test.js @@ -10,6 +10,29 @@ describe( 'adding blocks', () => { await newPost(); } ); + /** + * Given a Puppeteer ElementHandle, clicks around the center-right point. + * + * TEMPORARY: This is a mild hack to work around a bug in the application + * which prevents clicking at center of the inserter, due to conflicting + * overlap of focused block contextual toolbar. + * + * @see Puppeteer.ElementHandle#click + * + * @link https://github.com/WordPress/gutenberg/pull/5658#issuecomment-376943568 + * + * @param {Puppeteer.ElementHandle} elementHandle Element handle. + * + * @return {Promise} Promise resolving when element clicked. + */ + async function clickAtRightish( elementHandle ) { + await elementHandle._scrollIntoViewIfNeeded(); + const box = await elementHandle._assertBoundingBox(); + const x = box.x + ( box.width * 0.75 ); + const y = box.y + ( box.height / 2 ); + return page.mouse.click( x, y ); + } + it( 'Should insert content using the placeholder and the regular inserter', async () => { // Default block appender is provisional await page.click( '.editor-default-block-appender' ); @@ -39,7 +62,8 @@ describe( 'adding blocks', () => { // Using the between inserter await page.mouse.move( 200, 300 ); await page.mouse.move( 250, 350 ); - await page.click( '[data-type="core/paragraph"] .editor-block-list__insertion-point-inserter' ); + const inserter = await page.$( '[data-type="core/quote"] .editor-block-list__insertion-point-inserter' ); + await clickAtRightish( inserter ); await page.keyboard.type( 'Second paragraph' ); // Switch to Text Mode to check HTML Output