diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js
index fa1c1f3f6baa4d..a6babfbdacfa65 100644
--- a/editor/modes/visual-editor/block.js
+++ b/editor/modes/visual-editor/block.js
@@ -2,8 +2,10 @@
* External dependencies
*/
import { connect } from 'react-redux';
+import classnames from 'classnames';
-function VisualEditorBlock( { block, onChange } ) {
+function VisualEditorBlock( props ) {
+ const { block } = props;
const settings = wp.blocks.getBlockSettings( block.blockType );
let BlockEdit;
@@ -16,7 +18,7 @@ function VisualEditorBlock( { block, onChange } ) {
}
function onAttributesChange( attributes ) {
- onChange( {
+ props.onChange( {
attributes: {
...block.attributes,
...attributes
@@ -24,16 +26,41 @@ function VisualEditorBlock( { block, onChange } ) {
} );
}
+ const { isSelected, isHovered } = props;
+ const className = classnames( 'editor-visual-editor__block', {
+ 'is-selected': isSelected,
+ 'is-hovered': isHovered
+ } );
+
+ const { onSelect, onDeselect, onMouseEnter, onMouseLeave } = props;
+
+ // Disable reason: Each block can receive focus but must be able to contain
+ // block children. Tab keyboard navigation enabled by tabIndex assignment.
+
+ /* eslint-disable jsx-a11y/no-static-element-interactions */
return (
-
+
+
+
);
+ /* eslint-enable jsx-a11y/no-static-element-interactions */
}
export default connect(
( state, ownProps ) => ( {
- block: state.blocks.byUid[ ownProps.uid ]
+ block: state.blocks.byUid[ ownProps.uid ],
+ isSelected: !! state.blocks.selected[ ownProps.uid ],
+ isHovered: !! state.blocks.hovered[ ownProps.uid ]
} ),
( dispatch, ownProps ) => ( {
onChange( updates ) {
@@ -42,6 +69,34 @@ export default connect(
uid: ownProps.uid,
updates
} );
+ },
+ onSelect() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_SELECTED',
+ selected: true,
+ uid: ownProps.uid
+ } );
+ },
+ onDeselect() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_SELECTED',
+ selected: false,
+ uid: ownProps.uid
+ } );
+ },
+ onMouseEnter() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_HOVERED',
+ hovered: true,
+ uid: ownProps.uid
+ } );
+ },
+ onMouseLeave() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_HOVERED',
+ hovered: false,
+ uid: ownProps.uid
+ } );
}
} )
)( VisualEditorBlock );
diff --git a/editor/modes/visual-editor/style.scss b/editor/modes/visual-editor/style.scss
index 4946a69686ae71..5d5d1ee39c39be 100644
--- a/editor/modes/visual-editor/style.scss
+++ b/editor/modes/visual-editor/style.scss
@@ -8,4 +8,23 @@
font-size: $editor-font-size;
line-height: $editor-line-height;
}
+
+ p {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+}
+
+.editor-visual-editor__block {
+ padding: 15px;
+ border: 2px solid transparent;
+ transition: 0.2s border-color;
+
+ &.is-hovered {
+ border-left: 2px solid $light-gray-500;
+ }
+
+ &.is-selected {
+ border: 2px solid $light-gray-500;
+ }
}
diff --git a/editor/state.js b/editor/state.js
index 975e2dc1ad0a2c..b5cc8b7e7bf917 100644
--- a/editor/state.js
+++ b/editor/state.js
@@ -22,44 +22,84 @@ export function html( state = null, action ) {
}
/**
- * Reducer returning editor blocks state, an object with keys byUid and order,
- * where blocks are parsed from current HTML markup.
+ * Reducer returning editor blocks state, an combined reducer of keys byUid,
+ * order, selected, hovered, where blocks are parsed from current HTML markup.
*
* @param {Object} state Current state
* @param {Object} action Dispatched action
* @return {Object} Updated state
*/
-export function blocks( state, action ) {
- if ( undefined === state ) {
- state = {
- byUid: {},
- order: []
- };
- }
+export const blocks = ( () => {
+ const reducer = combineReducers( {
+ byUid( state = {}, action ) {
+ switch ( action.type ) {
+ case 'SET_HTML':
+ return keyBy( action.blockNodes, 'uid' );
- switch ( action.type ) {
- case 'SET_HTML':
- const blockNodes = wp.blocks.parse( action.html );
- return {
- byUid: keyBy( blockNodes, 'uid' ),
- order: blockNodes.map( ( { uid } ) => uid )
- };
+ case 'UPDATE_BLOCK':
+ return {
+ ...state,
+ [ action.uid ]: {
+ ...state[ action.uid ],
+ ...action.updates
+ }
+ };
+ }
+
+ return state;
+ },
+ order( state = [], action ) {
+ switch ( action.type ) {
+ case 'SET_HTML':
+ return action.blockNodes.map( ( { uid } ) => uid );
+ }
+
+ return state;
+ },
+ selected( state = {}, action ) {
+ switch ( action.type ) {
+ case 'TOGGLE_BLOCK_SELECTED':
+ return {
+ ...state,
+ [ action.uid ]: action.selected
+ };
+ }
- case 'UPDATE_BLOCK':
- return {
- ...state,
- byUid: {
- ...state.byUid,
- [ action.uid ]: {
- ...state.byUid[ action.uid ],
- ...action.updates
+ return state;
+ },
+ hovered( state = {}, action ) {
+ switch ( action.type ) {
+ case 'TOGGLE_BLOCK_HOVERED':
+ return {
+ ...state,
+ [ action.uid ]: action.hovered
+ };
+
+ case 'TOGGLE_BLOCK_SELECTED':
+ if ( state[ action.uid ] ) {
+ return {
+ ...state,
+ [ action.uid ]: false
+ };
}
- }
+ break;
+ }
+
+ return state;
+ }
+ } );
+
+ return ( state, action ) => {
+ if ( 'SET_HTML' === action.type ) {
+ action = {
+ ...action,
+ blockNodes: wp.blocks.parse( action.html )
};
- }
+ }
- return state;
-}
+ return reducer( state, action );
+ };
+} )();
/**
* Reducer returning current editor mode, either "visual" or "text".
diff --git a/editor/test/state.js b/editor/test/state.js
index 477dcf0d4d9969..379397a654d90e 100644
--- a/editor/test/state.js
+++ b/editor/test/state.js
@@ -43,19 +43,23 @@ describe( 'state', () => {
wp.blocks.unregisterBlock( 'core/test-block' );
} );
- it( 'should return empty byUid, order by default', () => {
+ it( 'should return empty byUid, order, selected, hovered by default', () => {
const state = blocks( undefined, {} );
expect( state ).to.eql( {
byUid: {},
- order: []
+ order: [],
+ selected: {},
+ hovered: {}
} );
} );
it( 'should key set html blocks', () => {
const original = deepFreeze( {
byUid: {},
- order: []
+ order: [],
+ selected: {},
+ hovered: {}
} );
const state = blocks( original, {
type: 'SET_HTML',
@@ -64,8 +68,6 @@ describe( 'state', () => {
expect( Object.keys( state.byUid ) ).to.have.lengthOf( 1 );
expect( values( state.byUid )[ 0 ].blockType ).to.equal( 'core/test-block' );
- expect( state.order ).to.have.lengthOf( 1 );
- expect( state.order[ 0 ] ).to.be.a( 'string' );
} );
it( 'should return with block updates', () => {
@@ -77,7 +79,9 @@ describe( 'state', () => {
attributes: {}
}
},
- order: [ 'kumquat' ]
+ order: [ 'kumquat' ],
+ selected: {},
+ hovered: {}
} );
const state = blocks( original, {
type: 'UPDATE_BLOCK',
@@ -90,7 +94,53 @@ describe( 'state', () => {
} );
expect( state.byUid.kumquat.attributes.updated ).to.be.true();
- expect( state.order[ 0 ] ).to.equal( 'kumquat' );
+ } );
+
+ it( 'should return with block uid as hovered', () => {
+ const original = deepFreeze( {
+ byUid: {
+ kumquat: {
+ uid: 'kumquat',
+ blockType: 'core/test-block',
+ attributes: {}
+ }
+ },
+ order: [ 'kumquat' ],
+ selected: {},
+ hovered: {}
+ } );
+ const state = blocks( original, {
+ type: 'TOGGLE_BLOCK_HOVERED',
+ uid: 'kumquat',
+ hovered: true
+ } );
+
+ expect( state.hovered.kumquat ).to.be.true();
+ } );
+
+ it( 'should return with block uid as selected, clearing hover', () => {
+ const original = deepFreeze( {
+ byUid: {
+ kumquat: {
+ uid: 'kumquat',
+ blockType: 'core/test-block',
+ attributes: {}
+ }
+ },
+ order: [ 'kumquat' ],
+ selected: {},
+ hovered: {
+ kumquat: true
+ }
+ } );
+ const state = blocks( original, {
+ type: 'TOGGLE_BLOCK_SELECTED',
+ uid: 'kumquat',
+ selected: true
+ } );
+
+ expect( state.hovered.kumquat ).to.be.false();
+ expect( state.selected.kumquat ).to.be.true();
} );
} );
diff --git a/package.json b/package.json
index fa56c0cfd5ee0b..4cb2246f253999 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"babel-core": "^6.24.0",
"babel-eslint": "^7.2.0",
"babel-loader": "^6.4.1",
+ "babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-plugin-transform-runtime": "^6.23.0",
@@ -54,7 +55,7 @@
"webpack-node-externals": "^1.5.4"
},
"dependencies": {
- "babel-plugin-lodash": "^3.2.11",
+ "classnames": "^2.2.5",
"hpq": "^1.1.1",
"lodash": "^4.17.4",
"react-redux": "^5.0.3",