diff --git a/edit-post/components/modes/visual-editor/style.scss b/edit-post/components/modes/visual-editor/style.scss index 2c4fb279b97d7..906f213fc674e 100644 --- a/edit-post/components/modes/visual-editor/style.scss +++ b/edit-post/components/modes/visual-editor/style.scss @@ -37,17 +37,6 @@ } } -// This is a focus style shown for blocks that need an indicator even when in an isEditing state -// like for example an image block that receives arrowkey focus. -.edit-post-visual-editor .editor-block-list__block:not( .is-selected ) .editor-block-list__block-edit { - box-shadow: 0 0 0 0 $white, 0 0 0 0 $dark-gray-900; - transition: .1s box-shadow .05s; - - &:focus { - box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900; - } -} - .edit-post-visual-editor .editor-post-title { margin-left: auto; margin-right: auto; diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index c79b405b54088..ca7bb6a4c8775 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -3,7 +3,7 @@ */ import { connect } from 'react-redux'; import classnames from 'classnames'; -import { get, reduce, size, castArray, noop, first, last } from 'lodash'; +import { get, reduce, size, castArray, noop, first, last, some } from 'lodash'; import tinymce from 'tinymce'; /** @@ -16,6 +16,7 @@ import { getScrollContainer, placeCaretAtHorizontalEdge, placeCaretAtVerticalEdge, + isEditableNode, } from '@wordpress/utils'; import { BlockEdit, @@ -100,6 +101,7 @@ export class BlockListBlock extends Component { this.onClick = this.onClick.bind( this ); this.selectOnOpen = this.selectOnOpen.bind( this ); this.onSelectionChange = this.onSelectionChange.bind( this ); + this.updateHasEditableContent = this.updateHasEditableContent.bind( this ); this.previousOffset = null; this.hadTouchStart = false; @@ -108,6 +110,7 @@ export class BlockListBlock extends Component { error: null, isHovered: false, isSelectionCollapsed: true, + hasEditableContent: false, }; } @@ -137,6 +140,11 @@ export class BlockListBlock extends Component { if ( this.props.isSelected ) { this.focusTabbable(); } + + this.contentObserver = new window.MutationObserver( this.updateHasEditableContent ); + this.contentObserver.observe( this.node, { childList: true, subtree: true } ); + + this.updateHasEditableContent(); } componentWillReceiveProps( newProps ) { @@ -182,12 +190,21 @@ export class BlockListBlock extends Component { componentWillUnmount() { this.removeStopTypingListener(); document.removeEventListener( 'selectionchange', this.onSelectionChange ); + this.contentObserver.disconnect(); } removeStopTypingListener() { document.removeEventListener( 'mousemove', this.stopTypingOnMouseMove ); } + updateHasEditableContent() { + // Update the hasEditableContent property + const hasEditableContent = some( focus.tabbable.find( this.node ), isEditableNode ); + if ( this.state.hasEditableContent !== hasEditableContent ) { + this.setState( { hasEditableContent } ); + } + } + setBlockListRef( node ) { // Disable reason: The root return element uses a component to manage // event nesting, but the parent block list layout needs the raw DOM @@ -219,17 +236,12 @@ export class BlockListBlock extends Component { } // Find all tabbables within node. - const tabbables = focus.tabbable.find( this.node ) - .filter( ( node ) => node !== this.node ); + const tabbables = focus.tabbable.find( this.node ).filter( isEditableNode ); // If reversed (e.g. merge via backspace), use the last in the set of // tabbables. const isReverse = -1 === initialPosition; - const target = ( isReverse ? last : first )( tabbables ); - - if ( ! target ) { - return; - } + const target = ( isReverse ? last : first )( tabbables ) || this.node; target.focus(); @@ -522,6 +534,7 @@ export class BlockListBlock extends Component { isFirstMultiSelected, isLastInSelection, } = this.props; + const { error, hasEditableContent } = this.state; const isHovered = this.state.isHovered && ! this.props.isMultiSelecting; const { name: blockName, isValid } = block; const blockType = getBlockType( blockName ); @@ -540,8 +553,6 @@ export class BlockListBlock extends Component { const shouldShowSettingsMenu = shouldShowMovers; const shouldShowContextualToolbar = shouldAppearSelected && isValid && showContextualToolbar; const shouldShowMobileToolbar = shouldAppearSelected; - const { error } = this.state; - // 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. @@ -558,6 +569,7 @@ export class BlockListBlock extends Component { 'is-multi-selected': isMultiSelected, 'is-hovered': isHovered, 'is-reusable': isReusableBlock( blockType ), + 'has-editable-content': hasEditableContent, } ); const { onReplace } = this.props; diff --git a/editor/components/block-list/style.scss b/editor/components/block-list/style.scss index 22dd868d3c981..b3a8ba5d93d84 100644 --- a/editor/components/block-list/style.scss +++ b/editor/components/block-list/style.scss @@ -360,6 +360,12 @@ } } +// This is a focus style shown for blocks that need an indicator even when in an isEditing state +// like for example an image block that receives arrowkey focus. +.editor-block-list__block:not(.has-editable-content):not( .is-selected ) > .editor-block-list__block-edit:focus { + box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900; +} + .editor-block-list .editor-inserter { margin: $item-spacing; diff --git a/editor/components/writing-flow/index.js b/editor/components/writing-flow/index.js index d267b74503081..fa72fddc1314e 100644 --- a/editor/components/writing-flow/index.js +++ b/editor/components/writing-flow/index.js @@ -17,6 +17,7 @@ import { isVerticalEdge, placeCaretAtHorizontalEdge, placeCaretAtVerticalEdge, + isEditableNode, } from '@wordpress/utils'; /** @@ -76,12 +77,9 @@ class WritingFlow extends Component { getVisibleTabbables() { return focus.tabbable .find( this.container ) - .filter( ( node ) => ( - node.nodeName === 'INPUT' || - node.nodeName === 'TEXTAREA' || - node.contentEditable === 'true' || - node.classList.contains( 'editor-block-list__block-edit' ) - ) ); + .filter( ( node ) => + isEditableNode( node ) || node.classList.contains( 'editor-block-list__block-edit' ) + ); } getClosestTabbable( target, isReverse ) { diff --git a/utils/dom.js b/utils/dom.js index c90b4c7a01cf0..e58a6b86b8163 100644 --- a/utils/dom.js +++ b/utils/dom.js @@ -366,3 +366,18 @@ export function getScrollContainer( node ) { // Continue traversing return getScrollContainer( node.parentNode ); } + +/** + * Given a DOM node, returns whether the node is editable or not. + * + * @param {Element} node Node to check + * + * @return {boolean} Whether the node is editable or not + */ +export function isEditableNode( node ) { + return ( + node.nodeName === 'INPUT' || + node.nodeName === 'TEXTAREA' || + node.contentEditable === 'true' + ); +}