Skip to content

Commit

Permalink
Block: Outline when interacting with Toolbar Block Type/Movers (#20938)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Quach authored Mar 23, 2020
1 parent 17ba1a2 commit 3bbd057
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,19 @@ _Returns_

- `boolean`: Whether an ancestor of the block is in multi-selection set.

<a name="isBlockHighlighted" href="#isBlockHighlighted">#</a> **isBlockHighlighted**

Returns true if the current highlighted block matches the block clientId.

_Parameters_

- _state_ `Object`: Global application state.
- _clientId_ `string`: The block to check.

_Returns_

- `boolean`: Whether the block is currently highlighted.

<a name="isBlockInsertionPointVisible" href="#isBlockInsertionPointVisible">#</a> **isBlockInsertionPointVisible**

Returns true if we should show the block insertion point.
Expand Down Expand Up @@ -1296,6 +1309,15 @@ _Returns_

- `Object`: Action object.

<a name="toggleBlockHighlight" href="#toggleBlockHighlight">#</a> **toggleBlockHighlight**

Returns an action object that toggles the highlighted block state.

_Parameters_

- _clientId_ `string`: The block's clientId.
- _isHighlighted_ `boolean`: The highlight state.

<a name="toggleBlockMode" href="#toggleBlockMode">#</a> **toggleBlockMode**

Returns an action object used to toggle the block editing mode between
Expand Down
4 changes: 4 additions & 0 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function BlockListBlock( {
isLocked,
clientId,
rootClientId,
isHighlighted,
isSelected,
isMultiSelected,
isPartOfMultiSelection,
Expand Down Expand Up @@ -112,6 +113,7 @@ function BlockListBlock( {
'has-selected-ui': hasSelectedUI,
'has-warning': ! isValid || !! hasError || isUnregisteredBlock,
'is-selected': isSelected,
'is-highlighted': isHighlighted,
'is-multi-selected': isMultiSelected,
'is-reusable': isReusableBlock( blockType ),
'is-dragging': isDragging,
Expand Down Expand Up @@ -228,6 +230,7 @@ const applyWithSelect = withSelect(
getTemplateLock,
__unstableGetBlockWithoutInnerBlocks,
isNavigationMode,
isBlockHighlighted,
} = select( 'core/block-editor' );
const block = __unstableGetBlockWithoutInnerBlocks( clientId );
const isSelected = isBlockSelected( clientId );
Expand All @@ -248,6 +251,7 @@ const applyWithSelect = withSelect(
const { name, attributes, isValid } = block || {};

return {
isHighlighted: isBlockHighlighted( clientId ),
isMultiSelected: isBlockMultiSelected( clientId ),
isPartOfMultiSelection:
isBlockMultiSelected( clientId ) ||
Expand Down
17 changes: 17 additions & 0 deletions packages/block-editor/src/components/block-list/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
// The primary indicator of selection in text is the native selection marker.
// When selecting multiple blocks, we provide an additional selection indicator.
.is-navigate-mode & .block-editor-block-list__block.is-selected,
.block-editor-block-list__block.is-highlighted,
.block-editor-block-list__block.is-multi-selected {

// Show selection borders around every non-nested block's actual footprint.
Expand All @@ -168,6 +169,8 @@
// 2px outside.
box-shadow: 0 0 0 2px $blue-medium-focus;
border-radius: $radius-block-ui;
transition: box-shadow 0.2s ease-out;
@include reduce-motion("transition");

// Windows High Contrast mode will show this outline.
outline: 2px solid transparent;
Expand Down Expand Up @@ -203,6 +206,20 @@
min-height: ( $block-padding + $block-spacing ) * 2;
}

&::after {
content: "";
pointer-events: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: $radius-block-ui;
box-shadow: 0 0 0 2px transparent;
transition: box-shadow 0.1s ease-in;
@include reduce-motion("transition");
}

// Warnings
&.has-warning {
// When a block has a warning, you shouldn't be able to manipulate the contents.
Expand Down
20 changes: 12 additions & 8 deletions packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import BlockSwitcher from '../block-switcher';
import BlockControls from '../block-controls';
import BlockFormatControls from '../block-format-controls';
import BlockSettingsMenu from '../block-settings-menu';
import { useShowMoversGestures } from './utils';
import { useShowMoversGestures, useToggleBlockHighlight } from './utils';

export default function BlockToolbar( { hideDragHandle } ) {
const {
blockClientIds,
blockClientId,
hasFixedToolbar,
isValid,
mode,
Expand All @@ -36,15 +37,15 @@ export default function BlockToolbar( { hideDragHandle } ) {
getSettings,
} = select( 'core/block-editor' );
const selectedBlockClientIds = getSelectedBlockClientIds();
const blockRootClientId = getBlockRootClientId(
selectedBlockClientIds[ 0 ]
);
const selectedBlockClientId = selectedBlockClientIds[ 0 ];
const blockRootClientId = getBlockRootClientId( selectedBlockClientId );

const { __experimentalMoverDirection, __experimentalUIParts = {} } =
getBlockListSettings( blockRootClientId ) || {};

return {
blockClientIds: selectedBlockClientIds,
blockClientId: selectedBlockClientId,
hasFixedToolbar: getSettings().hasFixedToolbar,
rootClientId: blockRootClientId,
isValid:
Expand All @@ -60,12 +61,15 @@ export default function BlockToolbar( { hideDragHandle } ) {
};
}, [] );

const toggleBlockHighlight = useToggleBlockHighlight( blockClientId );
const nodeRef = useRef();

const {
showMovers,
gestures: showMoversGestures,
} = useShowMoversGestures( { ref: nodeRef } );
const { showMovers, gestures: showMoversGestures } = useShowMoversGestures(
{
ref: nodeRef,
onChange: toggleBlockHighlight,
}
);

const displayHeaderToolbar =
useViewportMatch( 'medium', '<' ) || hasFixedToolbar;
Expand Down
113 changes: 80 additions & 33 deletions packages/block-editor/src/components/block-toolbar/utils.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
/**
* External dependencies
*/
import { noop } from 'lodash';
/**
* WordPress dependencies
*/
import { useDispatch } from '@wordpress/data';
import { useState, useRef, useEffect, useCallback } from '@wordpress/element';

const { clearTimeout, setTimeout } = window;
const { clearTimeout, requestAnimationFrame, setTimeout } = window;
const DEBOUNCE_TIMEOUT = 250;

/**
* Hook that creates a showMover state, as well as debounced show/hide callbacks
*/
export function useDebouncedShowMovers( {
ref,
isFocused,
debounceTimeout = 500,
debounceTimeout = DEBOUNCE_TIMEOUT,
onChange = noop,
} ) {
const [ showMovers, setShowMovers ] = useState( false );
const timeoutRef = useRef();

const handleOnChange = ( nextIsFocused ) => {
setShowMovers( nextIsFocused );
onChange( nextIsFocused );
};

const getIsHovered = () => {
return ref?.current && ref.current.matches( ':hover' );
};
Expand All @@ -26,40 +38,41 @@ export function useDebouncedShowMovers( {
return ! isFocused && ! isHovered;
};

const debouncedShowMovers = useCallback(
( event ) => {
if ( event ) {
event.stopPropagation();
}
const clearTimeoutRef = () => {
const timeout = timeoutRef.current;

const timeout = timeoutRef.current;
if ( timeout && clearTimeout ) {
clearTimeout( timeout );
}
};

if ( timeout && clearTimeout ) {
clearTimeout( timeout );
}
if ( ! showMovers ) {
setShowMovers( true );
}
},
[ showMovers ]
);
const debouncedShowMovers = ( event ) => {
if ( event ) {
event.stopPropagation();
}

const debouncedHideMovers = useCallback(
( event ) => {
if ( event ) {
event.stopPropagation();
}
clearTimeoutRef();

timeoutRef.current = setTimeout( () => {
if ( shouldHideMovers() ) {
setShowMovers( false );
}
}, debounceTimeout );
},
[ isFocused ]
);
if ( ! showMovers ) {
handleOnChange( true );
}
};

const debouncedHideMovers = ( event ) => {
if ( event ) {
event.stopPropagation();
}

useEffect( () => () => clearTimeout( timeoutRef.current ), [] );
clearTimeoutRef();

timeoutRef.current = setTimeout( () => {
if ( shouldHideMovers() ) {
handleOnChange( false );
}
}, debounceTimeout );
};

useEffect( () => () => clearTimeoutRef(), [] );

return {
showMovers,
Expand All @@ -72,13 +85,17 @@ export function useDebouncedShowMovers( {
* Hook that provides a showMovers state and gesture events for DOM elements
* that interact with the showMovers state.
*/
export function useShowMoversGestures( { ref, debounceTimeout = 500 } ) {
export function useShowMoversGestures( {
ref,
debounceTimeout = DEBOUNCE_TIMEOUT,
onChange = noop,
} ) {
const [ isFocused, setIsFocused ] = useState( false );
const {
showMovers,
debouncedShowMovers,
debouncedHideMovers,
} = useDebouncedShowMovers( { ref, debounceTimeout, isFocused } );
} = useDebouncedShowMovers( { ref, debounceTimeout, isFocused, onChange } );

const registerRef = useRef( false );

Expand Down Expand Up @@ -135,3 +152,33 @@ export function useShowMoversGestures( { ref, debounceTimeout = 500 } ) {
},
};
}

/**
* Hook that toggles the highlight (outline) state of a block
*
* @param {string} clientId The block's clientId
*
* @return {Function} Callback function to toggle highlight state.
*/
export function useToggleBlockHighlight( clientId ) {
const { toggleBlockHighlight } = useDispatch( 'core/block-editor' );

const updateBlockHighlight = useCallback(
( isFocused ) => {
toggleBlockHighlight( clientId, isFocused );
},
[ clientId ]
);

useEffect( () => {
return () => {
// Sequences state change to enable editor updates (e.g. cursor
// position) to render correctly.
requestAnimationFrame( () => {
updateBlockHighlight( false );
} );
};
}, [] );

return updateBlockHighlight;
}
14 changes: 14 additions & 0 deletions packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -990,3 +990,17 @@ export function* insertAfterBlock( clientId ) {
);
yield insertDefaultBlock( {}, rootClientId, firstSelectedIndex + 1 );
}

/**
* Returns an action object that toggles the highlighted block state.
*
* @param {string} clientId The block's clientId.
* @param {boolean} isHighlighted The highlight state.
*/
export function toggleBlockHighlight( clientId, isHighlighted ) {
return {
type: 'TOGGLE_BLOCK_HIGHLIGHT',
clientId,
isHighlighted,
};
}
23 changes: 23 additions & 0 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,28 @@ export function automaticChangeStatus( state, action ) {
// Reset the state by default (for any action not handled).
}

/**
* Reducer returning current highlighted block.
*
* @param {boolean} state Current highlighted block.
* @param {Object} action Dispatched action.
*
* @return {string} Updated state.
*/
export function highlightedBlock( state, action ) {
const { clientId, isHighlighted } = action;

if ( action.type === 'TOGGLE_BLOCK_HIGHLIGHT' ) {
if ( isHighlighted ) {
return clientId;
} else if ( state === clientId ) {
return null;
}
}

return state;
}

export default combineReducers( {
blocks,
isTyping,
Expand All @@ -1467,4 +1489,5 @@ export default combineReducers( {
lastBlockAttributesChange,
isNavigationMode,
automaticChangeStatus,
highlightedBlock,
} );
12 changes: 12 additions & 0 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1621,3 +1621,15 @@ export function isNavigationMode( state ) {
export function didAutomaticChange( state ) {
return !! state.automaticChangeStatus;
}

/**
* Returns true if the current highlighted block matches the block clientId.
*
* @param {Object} state Global application state.
* @param {string} clientId The block to check.
*
* @return {boolean} Whether the block is currently highlighted.
*/
export function isBlockHighlighted( state, clientId ) {
return state.highlightedBlock === clientId;
}

0 comments on commit 3bbd057

Please sign in to comment.