diff --git a/blocks/editable/index.js b/blocks/editable/index.js index acea78c5812de1..eec313d7795a19 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -577,7 +577,7 @@ export default class Editable extends Component { this.editor.selection.collapse( false ); } } else if ( isActive ) { - this.editor.getBody().blur(); + // this.editor.getBody().blur(); } } diff --git a/blocks/library/image/block.js b/blocks/library/image/block.js index a11e692308d0e3..b72d830e2693b3 100644 --- a/blocks/library/image/block.js +++ b/blocks/library/image/block.js @@ -110,11 +110,13 @@ class ImageBlock extends Component { render() { const { attributes, setAttributes, focus, setFocus, className, settings } = this.props; const { url, alt, caption, align, id, href, width, height } = attributes; + const fallbackFocus = url ? 'caption' : 'upload'; + const focused = focus ? ( focus.editable || fallbackFocus ) : null; const availableSizes = this.getAvailableSizes(); const figureStyle = width ? { width } : {}; const isResizable = [ 'wide', 'full' ].indexOf( align ) === -1; - const uploadButtonProps = { isLarge: true }; + const uploadButtonProps = { isLarge: true, focus: focused === 'media' }; const uploadFromFiles = ( event ) => mediaUpload( event.target.files, setAttributes ); const dropFiles = ( files ) => mediaUpload( files, setAttributes ); @@ -161,6 +163,7 @@ class ImageBlock extends Component { className="wp-block-image__upload-button" onChange={ uploadFromFiles } accept="image/*" + focus={ focused === 'upload' ? true : null } > { __( 'Upload' ) } @@ -268,7 +271,7 @@ class ImageBlock extends Component { tagName="figcaption" placeholder={ __( 'Write caption…' ) } value={ caption } - focus={ focus && focus.editable === 'caption' ? focus : undefined } + focus={ focused === 'caption' ? focus : undefined } onFocus={ focusCaption } onChange={ ( value ) => setAttributes( { caption: value } ) } inlineToolbar diff --git a/components/button/index.js b/components/button/index.js index 079038a4297bbb..6b989dc3dfc4a0 100644 --- a/components/button/index.js +++ b/components/button/index.js @@ -12,15 +12,15 @@ import { Component, createElement } from '@wordpress/element'; * Internal dependencies */ import './style.scss'; - class Button extends Component { constructor( props ) { super( props ); this.setRef = this.setRef.bind( this ); } - componentDidMount() { - if ( this.props.focus ) { + componentWillReceiveProps( nextProps ) { + // consider blurring and improve checking here. + if ( this.props.focus !== nextProps.focus && nextProps.focus ) { this.ref.focus(); } } diff --git a/editor/actions.js b/editor/actions.js index 7c061149132a61..c4604665e48e63 100644 --- a/editor/actions.js +++ b/editor/actions.js @@ -93,11 +93,25 @@ export function updateBlock( uid, updates ) { }; } -export function focusBlock( uid, config ) { +export function focusBlock( uid ) { return { type: 'UPDATE_FOCUS', uid, - config, + config: { + target: 'block', + options: { }, + }, + }; +} + +export function focusBlockEdit( uid, config ) { + return { + type: 'UPDATE_FOCUS', + uid, + config: { + target: 'blockEdit', + options: config || {}, + }, }; } diff --git a/editor/block-toolbar/index.js b/editor/block-toolbar/index.js index 21371b493ddd58..a51f06ee86f423 100644 --- a/editor/block-toolbar/index.js +++ b/editor/block-toolbar/index.js @@ -89,11 +89,13 @@ class BlockToolbar extends Component { } } -export default connect( ( state ) => { - const block = getSelectedBlock( state ); +export default connect( + ( state ) => { + const block = getSelectedBlock( state ); - return ( { - block, - mode: block ? getBlockMode( state, block.uid ) : null, - } ); -} )( BlockToolbar ); + return ( { + block, + mode: block ? getBlockMode( state, block.uid ) : null, + } ); + } +)( BlockToolbar ); diff --git a/editor/header/header-toolbar/index.js b/editor/header/header-toolbar/index.js index cccbaf2039cb38..4c77481b9b34dc 100644 --- a/editor/header/header-toolbar/index.js +++ b/editor/header/header-toolbar/index.js @@ -63,5 +63,8 @@ export default connect( ( dispatch ) => ( { undo: () => dispatch( { type: 'UNDO' } ), redo: () => dispatch( { type: 'REDO' } ), + onFocusBlockEdit( uid, config ) { + dispatch( focusBlockEdit( uid, config ) ); + }, } ) )( HeaderToolbar ); diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 901a70189870a6..1e68d4131ad3c1 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -28,6 +28,7 @@ import { clearSelectedBlock, editPost, focusBlock, + focusBlockEdit, insertBlocks, mergeBlocks, removeBlock, @@ -54,7 +55,7 @@ import { getBlockMode, } from '../../selectors'; -const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes; +const { BACKSPACE, ESCAPE, DELETE, ENTER, TAB, UP, RIGHT, DOWN, LEFT } = keycodes; class VisualEditorBlock extends Component { constructor() { @@ -81,7 +82,7 @@ class VisualEditorBlock extends Component { } componentDidMount() { - if ( this.props.focus ) { + if ( this.props.focus && this.props.focus.target === 'block' ) { this.node.focus(); } @@ -113,7 +114,7 @@ class VisualEditorBlock extends Component { } // Focus node when focus state is programmatically transferred. - if ( this.props.focus && ! prevProps.focus && ! this.node.contains( document.activeElement ) ) { + if ( this.props.focus !== prevProps.focus && this.props.focus && this.props.focus.target === 'block' ) { this.node.focus(); } @@ -225,8 +226,9 @@ class VisualEditorBlock extends Component { } onFocus( event ) { + const { onFocusBlock } = this.props; if ( event.target === this.node ) { - this.props.onSelect(); + onFocusBlock( this.props.uid ); } } @@ -249,24 +251,49 @@ class VisualEditorBlock extends Component { } onKeyDown( event ) { - const { keyCode, target } = event; + const { uid, onRemove, previousBlock, nextBlock, onFocusBlockEdit, onFocusBlock, focus } = this.props; + + const { keyCode, target, shiftKey } = event; + + const focusOnContainer = target === this.node; switch ( keyCode ) { + case TAB: + if ( ! shiftKey && focus && focus.target === 'blockEdit' && nextBlock ) { + event.preventDefault(); + event.stopPropagation(); + onFocusBlockEdit( nextBlock.uid ); + } else if ( shiftKey && focus && focus.target === 'blockEdit' && previousBlock ) { + event.preventDefault(); + event.stopPropagation(); + onFocusBlockEdit( previousBlock.uid ); + } + break; case ENTER: // Insert default block after current block if enter and event // not already handled by descendant. - if ( target === this.node ) { + if ( focusOnContainer ) { event.preventDefault(); - - this.props.onInsertBlocks( [ - createBlock( 'core/paragraph' ), - ], this.props.order + 1 ); + event.stopPropagation(); + onFocusBlockEdit( this.props.uid ); } break; case UP: - case RIGHT: + if ( focusOnContainer && previousBlock ) { + event.preventDefault(); + event.stopPropagation(); + onFocusBlock( previousBlock.uid ); + } + break; case DOWN: + if ( focusOnContainer && nextBlock ) { + event.preventDefault(); + event.stopPropagation(); + onFocusBlock( nextBlock.uid ); + } + break; + case RIGHT: case LEFT: // Arrow keys do not fire keypress event, but should still // trigger typing mode. @@ -276,20 +303,20 @@ class VisualEditorBlock extends Component { case BACKSPACE: case DELETE: // Remove block on backspace. - if ( target === this.node ) { + if ( focusOnContainer ) { event.preventDefault(); - const { uid, onRemove, previousBlock, onFocus } = this.props; onRemove( uid ); if ( previousBlock ) { - onFocus( previousBlock.uid, { offset: -1 } ); + onFocusBlockEdit( previousBlock.uid, { offset: -1 } ); } } break; case ESCAPE: - // Deselect on escape. - this.props.onDeselect(); + if ( ! focusOnContainer ) { + onFocusBlock( this.props.uid ); + } break; } } @@ -322,17 +349,22 @@ class VisualEditorBlock extends Component { // Generate the wrapper class names handling the different states of the block. const { isHovered, isSelected, isMultiSelected, isFirstMultiSelected, focus } = this.props; - const showUI = isSelected && ( ! this.props.isTyping || ( focus && focus.collapsed === false ) ); + + const isNavigating = focus && focus.target === 'block'; + + // Ignoring focus collapsed ... probably want to add that. + const showUI = isSelected && ( ! this.props.isTyping && focus && focus.target === 'blockEdit' ); const isProperlyHovered = isHovered && ! this.props.isSelecting; const { error } = this.state; const wrapperClassName = classnames( 'editor-visual-editor__block', { 'has-warning': ! isValid || !! error, - 'is-selected': showUI, + 'is-selected': isSelected && ! isNavigating, 'is-multi-selected': isMultiSelected, 'is-hovered': isProperlyHovered, + 'is-navigating': isNavigating, } ); - const { onMouseLeave, onFocus, onReplace } = this.props; + const { onMouseLeave, onFocusBlockEdit, onReplace } = this.props; // Determine whether the block has props to apply to the wrapper. let wrapperProps; @@ -383,12 +415,12 @@ class VisualEditorBlock extends Component { { isValid && mode === 'visual' && ( { + before( () => { + // cy.login(); + cy.newPost(); + } ); + + it( 'Testing focus of paragraph', () => { + cy.auditBlockFocus( 'Paragraph', () => { + cy.focused().type( 'Paragraph' ); + } ); + } ); + + it( 'Testing focus of image', () => { + cy.auditBlockFocus( 'Image', () => { + // cy.focused().type( 'Paragraph' ); + } ); + } ); + + it( 'Testing focus of heading', () => { + cy.auditBlockFocus( 'Heading', () => { + cy.focused().type( 'Heading' ); + } ); + } ); +} ); diff --git a/test/e2e/support/focus-commands.js b/test/e2e/support/focus-commands.js new file mode 100644 index 00000000000000..93c205f825849a --- /dev/null +++ b/test/e2e/support/focus-commands.js @@ -0,0 +1,24 @@ +Cypress.Commands.add( 'auditBlockFocus', ( blockType, setupBlock = () => { } ) => { + // cy.get( '.editor-visual-editor__inserter [aria-label="' + blockType + '"]' ).click(); + cy.get( 'button.editor-inserter__toggle:first' ).click() + cy.get( '.editor-inserter__menu .editor-inserter__block:contains("' + blockType + '"):first' ).click(); + + cy.focused().then( ( preFocus ) => { + cy.wrap( preFocus ).closest( '.editor-visual-editor__block-edit' ).then( ( outer ) => { + assert.notEqual( outer.get( 0 ), preFocus.get( 0 ) ); + } ); + + setupBlock(); + + cy.focused().type( '{esc} '); + + cy.focused().then( ( outerFocus ) => { + expect( outerFocus ).to.have.class( 'editor-visual-editor__block-edit' ); + } ); + + cy.focused().type( '{enter}' ); + cy.focused().then( ( postFocus ) => { + assert.equal( postFocus.get( 0 ), preFocus.get( 0 ) ); + } ); + } ); +} ); diff --git a/test/e2e/support/gutenberg-commands.js b/test/e2e/support/gutenberg-commands.js index 5d13910102749b..5574b5f7be7ab5 100644 --- a/test/e2e/support/gutenberg-commands.js +++ b/test/e2e/support/gutenberg-commands.js @@ -1,4 +1,5 @@ Cypress.Commands.add( 'newPost', () => { + console.log('here'); cy.visitAdmin( '/post-new.php' ); } ); diff --git a/test/e2e/support/index.js b/test/e2e/support/index.js index 6ed6aa3c8e2aa6..1d7f6760491404 100644 --- a/test/e2e/support/index.js +++ b/test/e2e/support/index.js @@ -1,5 +1,6 @@ import './user-commands'; import './gutenberg-commands'; +import './focus-commands'; Cypress.Cookies.defaults( { whitelist: /^wordpress_/,