diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 7b62480dedc6a8..e0e9695a0036a7 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -976,6 +976,19 @@ _Returns_ - `boolean`: Is Valid. +### isBlockVisible + +Tells if the block is visible on the canvas or not. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _clientId_ `Object`: Client Id of the block. + +_Returns_ + +- `boolean`: True if the block is visible. + ### isBlockWithinSelection Returns true if the block corresponding to the specified client ID is @@ -1456,6 +1469,14 @@ _Parameters_ - _hasBlockMovingClientId_ `string|null`: Enable/Disable block moving mode. +### setBlockVisibility + +Action that sets whether a block has controlled inner blocks. + +_Parameters_ + +- _updates_ `Record`: The block's clientId. + ### setHasControlledInnerBlocks Action that sets whether a block has controlled inner blocks. diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 1f377a2b8ce25c..b97bb03c5c142f 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { AsyncModeProvider, useSelect } from '@wordpress/data'; +import { AsyncModeProvider, useSelect, useDispatch } from '@wordpress/data'; import { useViewportMatch, useMergeRefs } from '@wordpress/compose'; import { createContext, useState, useMemo } from '@wordpress/element'; @@ -48,6 +48,23 @@ function Root( { className, ...settings } ) { }, [] ); + const { setBlockVisibility } = useDispatch( blockEditorStore ); + const intersectionObserver = useMemo( () => { + const { IntersectionObserver: Observer } = window; + + if ( ! Observer ) { + return; + } + + return new Observer( ( entries ) => { + const updates = {}; + for ( const entry of entries ) { + const clientId = entry.target.getAttribute( 'data-block' ); + updates[ clientId ] = entry.isIntersecting; + } + setBlockVisibility( updates ); + } ); + }, [] ); const innerBlocksProps = useInnerBlocksProps( { ref: useMergeRefs( [ @@ -65,7 +82,9 @@ function Root( { className, ...settings } ) { ); return ( -
+ +
+ ); } @@ -90,34 +109,17 @@ function Items( { __experimentalAppenderTagName, __experimentalLayout: layout = defaultLayout, } ) { - const [ intersectingBlocks, setIntersectingBlocks ] = useState( new Set() ); - const intersectionObserver = useMemo( () => { - const { IntersectionObserver: Observer } = window; - - if ( ! Observer ) { - return; - } - - return new Observer( ( entries ) => { - setIntersectingBlocks( ( oldIntersectingBlocks ) => { - const newIntersectingBlocks = new Set( oldIntersectingBlocks ); - for ( const entry of entries ) { - const clientId = entry.target.getAttribute( 'data-block' ); - const action = entry.isIntersecting ? 'add' : 'delete'; - newIntersectingBlocks[ action ]( clientId ); - } - return newIntersectingBlocks; - } ); - } ); - }, [ setIntersectingBlocks ] ); - const { order, selectedBlocks } = useSelect( + const { order, selectedBlocks, visibleBlocks } = useSelect( ( select ) => { - const { getBlockOrder, getSelectedBlockClientIds } = select( - blockEditorStore - ); + const { + getBlockOrder, + getSelectedBlockClientIds, + __unstableGetVisibleBlocks, + } = select( blockEditorStore ); return { order: getBlockOrder( rootClientId ), selectedBlocks: getSelectedBlockClientIds(), + visibleBlocks: __unstableGetVisibleBlocks(), }; }, [ rootClientId ] @@ -125,24 +127,22 @@ function Items( { return ( - - { order.map( ( clientId ) => ( - - - - ) ) } - + { order.map( ( clientId ) => ( + + + + ) ) } { order.length < 1 && placeholder } { - const { getBlockListSettings, getBlockRootClientId } = select( - blockEditorStore - ); + const { + getBlockListSettings, + getBlockRootClientId, + isBlockVisible, + } = select( blockEditorStore ); const _rootClientId = getBlockRootClientId( previousClientId ); return { @@ -40,6 +42,9 @@ function BlockPopoverInbetween( { getBlockListSettings( _rootClientId )?.orientation || 'vertical', rootClientId: _rootClientId, + isVisible: + isBlockVisible( previousClientId ) && + isBlockVisible( nextClientId ), }; }, [ previousClientId ] @@ -48,7 +53,7 @@ function BlockPopoverInbetween( { const nextElement = useBlockElement( nextClientId ); const isVertical = orientation === 'vertical'; const style = useMemo( () => { - if ( ! previousElement && ! nextElement ) { + if ( ( ! previousElement && ! nextElement ) || ! isVisible ) { return {}; } @@ -87,7 +92,7 @@ function BlockPopoverInbetween( { }, [ previousElement, nextElement, isVertical ] ); const getAnchorRect = useCallback( () => { - if ( ! previousElement && ! nextElement ) { + if ( ( ! previousElement && ! nextElement ) || ! isVisible ) { return {}; } @@ -149,7 +154,7 @@ function BlockPopoverInbetween( { const popoverScrollRef = usePopoverScroll( __unstableContentRef ); - if ( ! previousElement || ! nextElement ) { + if ( ! previousElement || ! nextElement || ! isVisible ) { return null; } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index c9170d64801813..afb93c40c5d92c 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1603,3 +1603,15 @@ export function setHasControlledInnerBlocks( clientId, }; } + +/** + * Action that sets whether a block has controlled inner blocks. + * + * @param {Record} updates The block's clientId. + */ +export function setBlockVisibility( updates ) { + return { + type: 'SET_BLOCK_VISIBILITY', + updates, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 658918b8f3a09c..5df8ab6a148de8 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -606,6 +606,7 @@ const withBlockReset = ( reducer ) => ( state, action ) => { order: mapBlockOrder( action.blocks ), parents: mapBlockParents( action.blocks ), controlledInnerBlocks: {}, + visibility: {}, }; const subTree = buildBlockTree( newState, action.blocks ); @@ -1139,6 +1140,17 @@ export const blocks = flow( } return state; }, + + visibility( state = {}, action ) { + if ( action.type === 'SET_BLOCK_VISIBILITY' ) { + return { + ...state, + ...action.updates, + }; + } + + return state; + }, } ); /** @@ -1678,7 +1690,8 @@ export function automaticChangeStatus( state, action ) { return; // Undoing an automatic change should still be possible after mouse - // move. + // move or after visibility change. + case 'SET_BLOCK_VISIBILITY': case 'START_TYPING': case 'STOP_TYPING': return state; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index b8d72a3e113391..4a1c0ca1354489 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2651,3 +2651,31 @@ export function wasBlockJustInserted( state, clientId, source ) { lastBlockInserted.source === source ); } + +/** + * Tells if the block is visible on the canvas or not. + * + * @param {Object} state Global application state. + * @param {Object} clientId Client Id of the block. + * @return {boolean} True if the block is visible. + */ +export function isBlockVisible( state, clientId ) { + return state.blocks.visibility?.[ clientId ] ?? true; +} + +/** + * Returns the list of all hidden blocks. + * + * @param {Object} state Global application state. + * @return {[string]} List of hidden blocks. + */ +export const __unstableGetVisibleBlocks = createSelector( + ( state ) => { + return new Set( + Object.keys( state.blocks.visibility ).filter( + ( key ) => state.blocks.visibility[ key ] + ) + ); + }, + ( state ) => [ state.blocks.visibility ] +); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index faca5d4f86117f..ec8f69a5d7d48a 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -290,6 +290,7 @@ describe( 'state', () => { chicken: '', }, controlledInnerBlocks: {}, + visibility: {}, } ); expect( state.tree.chicken ).not.toBe( existingState.tree.chicken @@ -371,6 +372,7 @@ describe( 'state', () => { chicken: '', }, controlledInnerBlocks: {}, + visibility: {}, } ); expect( state.tree.chicken ).not.toBe( existingState.tree.chicken @@ -519,6 +521,7 @@ describe( 'state', () => { [ newChildBlockId3 ]: 'chicken', }, controlledInnerBlocks: {}, + visibility: {}, } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( @@ -627,6 +630,7 @@ describe( 'state', () => { [ newChildBlockId ]: 'chicken', }, controlledInnerBlocks: {}, + visibility: {}, } ); // The block object of the parent should be updated. @@ -648,6 +652,7 @@ describe( 'state', () => { isIgnoredChange: false, tree: {}, controlledInnerBlocks: {}, + visibility: {}, } ); } );