From 81be0d6e0e25f00a8f8ae0b68696c0b5f571ee62 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Feb 2026 17:17:20 +1100 Subject: [PATCH 1/7] Block Visibility: Centralize modal state in block-editor store Move the block visibility modal open/close state from local component state (useState) to the block-editor Redux store using private actions/selectors. This enables the modal to be triggered from the command palette, which was previously impossible because command callbacks don't render UI. Previously, 4 separate components each managed their own modal state: - BlockTools (keyboard shortcut) - ViewportVisibilityToolbar (toolbar button) - BlockVisibilityViewportMenuItem (settings menu) - ListViewBlock (list view keyboard shortcut) Now all entry points dispatch showBlockVisibilityModal(clientIds) and the modal is rendered from a single location (BlockTools). This also adds a "Hide" command to the command palette for block visibility. Co-Authored-By: Claude Opus 4.6 --- .../src/components/block-tools/index.js | 10 +++-- .../block-visibility/viewport-menu-item.js | 28 +++++-------- .../block-visibility/viewport-toolbar.js | 42 +++++++------------ .../src/components/list-view/block.js | 13 ++---- .../components/use-block-commands/index.js | 20 ++++++++- .../block-editor/src/store/private-actions.js | 25 +++++++++++ .../src/store/private-selectors.js | 12 ++++++ packages/block-editor/src/store/reducer.js | 20 +++++++++ .../src/store/test/private-actions.js | 20 +++++++++ .../src/store/test/private-selectors.js | 20 +++++++++ .../block-editor/src/store/test/reducer.js | 32 ++++++++++++++ 11 files changed, 183 insertions(+), 59 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index e28fdafb2f21ec..d4aac252b1fbdd 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -75,8 +75,6 @@ export default function BlockTools( { } ) { const { clientId, hasFixedToolbar, isTyping, isZoomOutMode, isDragging } = useSelect( selector, [] ); - const [ visibilityModalClientIds, setVisibilityModalClientIds ] = - useState( null ); const isMatch = useShortcutEventMatch(); const { getBlocksByClientId, @@ -86,7 +84,9 @@ export default function BlockTools( { getBlockName, isGroupable, getEditedContentOnlySection, + getBlockVisibilityModalClientIds, } = unlock( useSelect( blockEditorStore ) ); + const visibilityModalClientIds = getBlockVisibilityModalClientIds(); const { getGroupingBlockName } = useSelect( blocksStore ); const { showEmptyBlockSideInserter, showBlockToolbarPopover } = useShowBlockTools(); @@ -108,6 +108,8 @@ export default function BlockTools( { moveBlocksDown, expandBlock, stopEditingContentOnlySection, + showBlockVisibilityModal, + hideBlockVisibilityModal, } = unlock( useDispatch( blockEditorStore ) ); function onKeyDown( event ) { @@ -248,7 +250,7 @@ export default function BlockTools( { } // Open the visibility breakpoints modal. - setVisibilityModalClientIds( clientIds ); + showBlockVisibilityModal( clientIds ); } } @@ -324,7 +326,7 @@ export default function BlockTools( { { visibilityModalClientIds && ( setVisibilityModalClientIds( null ) } + onClose={ hideBlockVisibilityModal } /> ) } diff --git a/packages/block-editor/src/components/block-visibility/viewport-menu-item.js b/packages/block-editor/src/components/block-visibility/viewport-menu-item.js index 0d37b4363e880c..edd68b9c994c2e 100644 --- a/packages/block-editor/src/components/block-visibility/viewport-menu-item.js +++ b/packages/block-editor/src/components/block-visibility/viewport-menu-item.js @@ -3,19 +3,16 @@ */ import { __ } from '@wordpress/i18n'; import { MenuItem } from '@wordpress/components'; -import { useState } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies */ -import { BlockVisibilityModal } from './'; import { store as blockEditorStore } from '../../store'; import { unlock } from '../../lock-unlock'; export default function BlockVisibilityViewportMenuItem( { clientIds } ) { - const [ isModalOpen, setIsModalOpen ] = useState( false ); const { areBlocksHiddenAnywhere, shortcut } = useSelect( ( select ) => { const { isBlockHiddenAnywhere } = unlock( @@ -34,20 +31,15 @@ export default function BlockVisibilityViewportMenuItem( { clientIds } ) { }, [ clientIds ] ); + const { showBlockVisibilityModal } = unlock( + useDispatch( blockEditorStore ) + ); return ( - <> - setIsModalOpen( true ) } - shortcut={ shortcut } - > - { areBlocksHiddenAnywhere ? __( 'Show' ) : __( 'Hide' ) } - - { isModalOpen && ( - setIsModalOpen( false ) } - /> - ) } - + showBlockVisibilityModal( clientIds ) } + shortcut={ shortcut } + > + { areBlocksHiddenAnywhere ? __( 'Show' ) : __( 'Hide' ) } + ); } diff --git a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js index 520e219c3ad540..125a2c7178dcbe 100644 --- a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js +++ b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js @@ -3,21 +3,19 @@ */ import { __ } from '@wordpress/i18n'; import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; -import { useRef, useEffect, useState } from '@wordpress/element'; +import { useRef, useEffect } from '@wordpress/element'; import { seen, unseen } from '@wordpress/icons'; import { hasBlockSupport } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { BlockVisibilityModal } from './'; import { unlock } from '../../lock-unlock'; export default function BlockVisibilityViewportToolbar( { clientIds } ) { const hasBlockVisibilityButtonShownRef = useRef( false ); - const [ isModalOpen, setIsModalOpen ] = useState( false ); const { canToggleBlockVisibility, areBlocksHiddenAnywhere } = useSelect( ( select ) => { const { getBlocksByClientId, getBlockName, isBlockHiddenAnywhere } = @@ -39,6 +37,7 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { [ clientIds ] ); + const blockEditorDispatch = useDispatch( blockEditorStore ); /* * If the block visibility button has been shown, we don't want to @@ -60,28 +59,19 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { return null; } + const { showBlockVisibilityModal } = unlock( blockEditorDispatch ); + return ( - <> - - setIsModalOpen( true ) } - aria-expanded={ isModalOpen } - aria-haspopup={ ! isModalOpen ? 'dialog' : undefined } - /> - - { isModalOpen && ( - setIsModalOpen( false ) } - /> - ) } - + + showBlockVisibilityModal( clientIds ) } + aria-haspopup="dialog" + /> + ); } diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index d9cb0518bac123..08307f86c3d703 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -54,7 +54,7 @@ import { useBlockRename, BlockRenameModal } from '../block-rename'; import AriaReferencedText from './aria-referenced-text'; import { unlock } from '../../lock-unlock'; import usePasteStyles from '../use-paste-styles'; -import { useBlockVisibility, BlockVisibilityModal } from '../block-visibility'; +import { useBlockVisibility } from '../block-visibility'; import { deviceTypeKey } from '../../store/private-keys'; import { BLOCK_VISIBILITY_VIEWPORTS } from '../block-visibility/constants'; @@ -83,8 +83,6 @@ function ListViewBlock( { const [ isHovered, setIsHovered ] = useState( false ); const [ settingsAnchorRect, setSettingsAnchorRect ] = useState(); const [ isRenameModalOpen, setIsRenameModalOpen ] = useState( false ); - const [ visibilityModalClientIds, setVisibilityModalClientIds ] = - useState( null ); const { isLocked } = useBlockLock( clientId ); const isFirstSelectedBlock = @@ -101,6 +99,7 @@ function ListViewBlock( { removeBlocks, insertAfterBlock, insertBeforeBlock, + showBlockVisibilityModal, } = unlock( useDispatch( blockEditorStore ) ); const debouncedToggleBlockHighlight = useDebounce( @@ -414,7 +413,7 @@ function ListViewBlock( { } // Open the visibility breakpoints modal. - setVisibilityModalClientIds( blocksToUpdate ); + showBlockVisibilityModal( blocksToUpdate ); } else if ( isMatch( 'core/block-editor/rename', event ) ) { const { blocksToUpdate } = getBlocksToUpdate(); const isContentOnly = @@ -721,12 +720,6 @@ function ListViewBlock( { ) } ) } - { visibilityModalClientIds && ( - setVisibilityModalClientIds( null ) } - /> - ) } { isRenameModalOpen && ( function useTransformCommands() { @@ -163,13 +165,14 @@ const getQuickActionsCommands = () => const blocks = getBlocksByClientId( clientIds ); + const blockEditorDispatch = useDispatch( blockEditorStore ); const { removeBlocks, replaceBlocks, duplicateBlocks, insertAfterBlock, insertBeforeBlock, - } = useDispatch( blockEditorStore ); + } = blockEditorDispatch; const onGroup = () => { if ( ! blocks.length ) { @@ -204,6 +207,7 @@ const getQuickActionsCommands = () => return { isLoading: false, commands: [] }; } + const { showBlockVisibilityModal } = unlock( blockEditorDispatch ); const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); const canInsertDefaultBlock = canInsertBlockType( getDefaultBlockName(), @@ -283,6 +287,20 @@ const getQuickActionsCommands = () => } ); } + const supportsVisibility = blocks.every( + ( block ) => + !! block && hasBlockSupport( block.name, 'visibility', true ) + ); + + if ( supportsVisibility ) { + commands.push( { + name: 'toggle-visibility', + label: __( 'Hide' ), + callback: () => showBlockVisibilityModal( clientIds ), + icon: unseen, + } ); + } + return { isLoading: false, commands: commands.map( ( command ) => ( { diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 82eefc3e941576..276ca096fd503f 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -475,3 +475,28 @@ export function closeListViewContentPanel() { type: 'CLOSE_LIST_VIEW_CONTENT_PANEL', }; } + +/** + * Returns an action object used to open the block visibility modal + * for the given client IDs. + * + * @param {string[]} clientIds Client IDs of blocks to configure visibility for. + * @return {Object} Action object. + */ +export function showBlockVisibilityModal( clientIds ) { + return { + type: 'SHOW_BLOCK_VISIBILITY_MODAL', + clientIds, + }; +} + +/** + * Returns an action object used to close the block visibility modal. + * + * @return {Object} Action object. + */ +export function hideBlockVisibilityModal() { + return { + type: 'HIDE_BLOCK_VISIBILITY_MODAL', + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 3c46ea9d1989bc..cd5c3d633a4d70 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -988,3 +988,15 @@ export function isListViewPanelOpened( state, clientId ) { export function getListViewExpandRevision( state ) { return state.listViewExpandRevision || 0; } + +/** + * Returns the client IDs for the block visibility modal, or null if + * the modal is not open. + * + * @param {Object} state Global application state. + * + * @return {Array|null} Client IDs for the visibility modal, or null. + */ +export function getBlockVisibilityModalClientIds( state ) { + return state.blockVisibilityModalClientIds; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index d7ae3e2ea2e2f4..ae4f57ce0dac7b 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1511,6 +1511,25 @@ export function isSelectionEnabled( state = true, action ) { * * @return {Object|false} Data for removal prompt display, if any. */ +/** + * Reducer returning the client IDs for the block visibility modal, + * or null if the modal is not open. + * + * @param {Array|null} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Array|null} Client IDs for the visibility modal. + */ +export function blockVisibilityModalClientIds( state = null, action ) { + switch ( action.type ) { + case 'SHOW_BLOCK_VISIBILITY_MODAL': + return action.clientIds; + case 'HIDE_BLOCK_VISIBILITY_MODAL': + return null; + } + return state; +} + function removalPromptData( state = false, action ) { switch ( action.type ) { case 'DISPLAY_BLOCK_REMOVAL_PROMPT': @@ -2220,6 +2239,7 @@ const combinedReducers = combineReducers( { lastBlockInserted, editedContentOnlySection, blockVisibility, + blockVisibilityModalClientIds, blockEditingModes, styleOverrides, removalPromptData, diff --git a/packages/block-editor/src/store/test/private-actions.js b/packages/block-editor/src/store/test/private-actions.js index a390bdd3939e5c..2fc09d5da40f9b 100644 --- a/packages/block-editor/src/store/test/private-actions.js +++ b/packages/block-editor/src/store/test/private-actions.js @@ -9,6 +9,8 @@ import { setInsertionPoint, startDragging, stopDragging, + showBlockVisibilityModal, + hideBlockVisibilityModal, } from '../private-actions'; describe( 'private actions', () => { @@ -121,4 +123,22 @@ describe( 'private actions', () => { } ); } ); } ); + + describe( 'showBlockVisibilityModal', () => { + it( 'should return the SHOW_BLOCK_VISIBILITY_MODAL action with clientIds', () => { + const clientIds = [ 'client-1', 'client-2' ]; + expect( showBlockVisibilityModal( clientIds ) ).toEqual( { + type: 'SHOW_BLOCK_VISIBILITY_MODAL', + clientIds, + } ); + } ); + } ); + + describe( 'hideBlockVisibilityModal', () => { + it( 'should return the HIDE_BLOCK_VISIBILITY_MODAL action', () => { + expect( hideBlockVisibilityModal() ).toEqual( { + type: 'HIDE_BLOCK_VISIBILITY_MODAL', + } ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 76e1a1a4bccfb5..eeb4f4b43c2822 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -21,6 +21,7 @@ import { isLockedBlock, isBlockHiddenAnywhere, isBlockHiddenAtViewport, + getBlockVisibilityModalClientIds, } from '../private-selectors'; import { getBlockEditingMode } from '../selectors'; import { deviceTypeKey } from '../private-keys'; @@ -1321,4 +1322,23 @@ describe( 'private selectors', () => { expect( result ).toBe( false ); } ); } ); + + describe( 'getBlockVisibilityModalClientIds', () => { + it( 'should return null when modal is not open', () => { + const state = { + blockVisibilityModalClientIds: null, + }; + expect( getBlockVisibilityModalClientIds( state ) ).toBeNull(); + } ); + + it( 'should return client IDs when modal is open', () => { + const clientIds = [ 'client-1', 'client-2' ]; + const state = { + blockVisibilityModalClientIds: clientIds, + }; + expect( getBlockVisibilityModalClientIds( state ) ).toEqual( + clientIds + ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 469868078fe2fa..130db3df5f29e2 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -42,6 +42,7 @@ import { zoomLevel, editedContentOnlySection, withDerivedBlockEditingModes, + blockVisibilityModalClientIds, } from '../reducer'; import { unlock } from '../../lock-unlock'; @@ -4570,4 +4571,35 @@ describe( 'state', () => { } ); } ); } ); + + describe( 'blockVisibilityModalClientIds', () => { + it( 'should default to null', () => { + const state = blockVisibilityModalClientIds( undefined, {} ); + expect( state ).toBeNull(); + } ); + + it( 'should return clientIds on SHOW_BLOCK_VISIBILITY_MODAL', () => { + const clientIds = [ 'client-1', 'client-2' ]; + const state = blockVisibilityModalClientIds( null, { + type: 'SHOW_BLOCK_VISIBILITY_MODAL', + clientIds, + } ); + expect( state ).toEqual( clientIds ); + } ); + + it( 'should return null on HIDE_BLOCK_VISIBILITY_MODAL', () => { + const state = blockVisibilityModalClientIds( [ 'client-1' ], { + type: 'HIDE_BLOCK_VISIBILITY_MODAL', + } ); + expect( state ).toBeNull(); + } ); + + it( 'should return current state for unknown actions', () => { + const currentState = [ 'client-1' ]; + const state = blockVisibilityModalClientIds( currentState, { + type: 'UNKNOWN_ACTION', + } ); + expect( state ).toBe( currentState ); + } ); + } ); } ); From 775177992c166454cacc1efae40023155d3a308b Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Feb 2026 18:25:59 +1100 Subject: [PATCH 2/7] Fix: Subscribe to visibility modal state changes in BlockTools Move getBlockVisibilityModalClientIds() into the useSelect callback so that store changes properly trigger re-renders. Previously it was called outside the callback, reading the value once without setting up a subscription. Co-Authored-By: Claude Opus 4.6 --- .../src/components/block-tools/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index d4aac252b1fbdd..70c304b73f449a 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -45,6 +45,7 @@ function selector( select ) { isTyping, isDragging, isZoomOut, + getBlockVisibilityModalClientIds, } = unlock( select( blockEditorStore ) ); const clientId = @@ -56,6 +57,7 @@ function selector( select ) { isTyping: isTyping(), isZoomOutMode: isZoomOut(), isDragging: isDragging(), + visibilityModalClientIds: getBlockVisibilityModalClientIds(), }; } @@ -73,8 +75,14 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const { clientId, hasFixedToolbar, isTyping, isZoomOutMode, isDragging } = - useSelect( selector, [] ); + const { + clientId, + hasFixedToolbar, + isTyping, + isZoomOutMode, + isDragging, + visibilityModalClientIds, + } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getBlocksByClientId, @@ -84,9 +92,7 @@ export default function BlockTools( { getBlockName, isGroupable, getEditedContentOnlySection, - getBlockVisibilityModalClientIds, } = unlock( useSelect( blockEditorStore ) ); - const visibilityModalClientIds = getBlockVisibilityModalClientIds(); const { getGroupingBlockName } = useSelect( blocksStore ); const { showEmptyBlockSideInserter, showBlockToolbarPopover } = useShowBlockTools(); From 9368eb97664e82ce4fb32231e8a7b8d7ae833a99 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Feb 2026 18:50:23 +1100 Subject: [PATCH 3/7] Toggle command label between Hide/Show based on block visibility state Use isBlockHiddenAnywhere selector to check if any selected block has visibility rules set, and toggle the command label ("Hide"/"Show") and icon (unseen/seen) accordingly. Co-Authored-By: Claude Opus 4.6 --- .../src/components/use-block-commands/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index 7fe83027e8df31..1e155fea54352c 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -16,6 +16,7 @@ import { plus as add, group, ungroup, + seen, unseen, } from '@wordpress/icons'; @@ -159,7 +160,8 @@ const getQuickActionsCommands = () => getBlockRootClientId, getBlocksByClientId, canRemoveBlocks, - } = useSelect( blockEditorStore ); + isBlockHiddenAnywhere, + } = unlock( useSelect( blockEditorStore ) ); const { getDefaultBlockName, getGroupingBlockName } = useSelect( blocksStore ); @@ -293,11 +295,14 @@ const getQuickActionsCommands = () => ); if ( supportsVisibility ) { + const hasHiddenBlock = clientIds.some( ( id ) => + isBlockHiddenAnywhere( id ) + ); commands.push( { name: 'toggle-visibility', - label: __( 'Hide' ), + label: hasHiddenBlock ? __( 'Show' ) : __( 'Hide' ), callback: () => showBlockVisibilityModal( clientIds ), - icon: unseen, + icon: hasHiddenBlock ? seen : unseen, } ); } From 91e46989196b8574cf4405e00039dc58252aa29f Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Feb 2026 14:13:06 +1100 Subject: [PATCH 4/7] Update JSDoc comments to specify the expected types for the block visibility modal state and client IDs, changing from Array to string[]. --- .../src/store/private-selectors.js | 2 +- packages/block-editor/src/store/reducer.js | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index cd5c3d633a4d70..a44cf73639571a 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -995,7 +995,7 @@ export function getListViewExpandRevision( state ) { * * @param {Object} state Global application state. * - * @return {Array|null} Client IDs for the visibility modal, or null. + * @return {string[]|null} Client IDs for the visibility modal, or null. */ export function getBlockVisibilityModalClientIds( state ) { return state.blockVisibilityModalClientIds; diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index ae4f57ce0dac7b..d44f3a6c093f02 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1502,23 +1502,14 @@ export function isSelectionEnabled( state = true, action ) { return state; } -/** - * Reducer returning the data needed to display a prompt when certain blocks - * are removed, or `false` if no such prompt is requested. - * - * @param {boolean} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object|false} Data for removal prompt display, if any. - */ /** * Reducer returning the client IDs for the block visibility modal, * or null if the modal is not open. * - * @param {Array|null} state Current state. - * @param {Object} action Dispatched action. + * @param {string[]|null} state Current state. + * @param {Object} action Dispatched action. * - * @return {Array|null} Client IDs for the visibility modal. + * @return {string[]|null} Client IDs for the visibility modal. */ export function blockVisibilityModalClientIds( state = null, action ) { switch ( action.type ) { @@ -1530,6 +1521,15 @@ export function blockVisibilityModalClientIds( state = null, action ) { return state; } +/** + * Reducer returning the data needed to display a prompt when certain blocks + * are removed, or `false` if no such prompt is requested. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object|false} Data for removal prompt display, if any. + */ function removalPromptData( state = false, action ) { switch ( action.type ) { case 'DISPLAY_BLOCK_REMOVAL_PROMPT': From bc05d53ca461ae97a4cca73b94a4a73894bae6c2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Feb 2026 14:18:05 +1100 Subject: [PATCH 5/7] Restore aria-expanded on visibility toolbar button Read the modal open state from the store via getBlockVisibilityModalClientIds() inside the useSelect callback and wire it to aria-expanded so assistive tech reflects the open state. Co-Authored-By: Claude Opus 4.6 --- .../block-visibility/viewport-toolbar.js | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js index 125a2c7178dcbe..be328794c31cbd 100644 --- a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js +++ b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js @@ -16,27 +16,33 @@ import { unlock } from '../../lock-unlock'; export default function BlockVisibilityViewportToolbar( { clientIds } ) { const hasBlockVisibilityButtonShownRef = useRef( false ); - const { canToggleBlockVisibility, areBlocksHiddenAnywhere } = useSelect( - ( select ) => { - const { getBlocksByClientId, getBlockName, isBlockHiddenAnywhere } = - unlock( select( blockEditorStore ) ); - const _blocks = getBlocksByClientId( clientIds ); - return { - canToggleBlockVisibility: _blocks.every( ( { clientId } ) => - hasBlockSupport( - getBlockName( clientId ), - 'visibility', - true - ) - ), - areBlocksHiddenAnywhere: clientIds?.every( ( clientId ) => - isBlockHiddenAnywhere( clientId ) - ), - }; - }, + const { canToggleBlockVisibility, areBlocksHiddenAnywhere, isModalOpen } = + useSelect( + ( select ) => { + const { + getBlocksByClientId, + getBlockName, + isBlockHiddenAnywhere, + getBlockVisibilityModalClientIds, + } = unlock( select( blockEditorStore ) ); + const _blocks = getBlocksByClientId( clientIds ); + return { + canToggleBlockVisibility: _blocks.every( ( { clientId } ) => + hasBlockSupport( + getBlockName( clientId ), + 'visibility', + true + ) + ), + areBlocksHiddenAnywhere: clientIds?.every( ( clientId ) => + isBlockHiddenAnywhere( clientId ) + ), + isModalOpen: getBlockVisibilityModalClientIds() !== null, + }; + }, - [ clientIds ] - ); + [ clientIds ] + ); const blockEditorDispatch = useDispatch( blockEditorStore ); /* @@ -70,6 +76,7 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { areBlocksHiddenAnywhere ? __( 'Hidden' ) : __( 'Visible' ) } onClick={ () => showBlockVisibilityModal( clientIds ) } + aria-expanded={ isModalOpen } aria-haspopup="dialog" /> From 6a4994533cfd5ecbe056d0aecb5e7622b35b5f1d Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Feb 2026 14:58:41 +1100 Subject: [PATCH 6/7] Rename blockVisibilityModal* to viewportModal* for future extensibility The viewport modal will eventually support editing any block property per breakpoint, not just visibility. Renaming the private store APIs now avoids a breaking change later. Co-Authored-By: Claude Opus 4.6 --- .../src/components/block-tools/index.js | 18 +++++----- .../block-visibility/viewport-menu-item.js | 6 ++-- .../block-visibility/viewport-toolbar.js | 8 ++--- .../src/components/list-view/block.js | 4 +-- .../components/use-block-commands/index.js | 4 +-- .../block-editor/src/store/private-actions.js | 14 ++++---- .../src/store/private-selectors.js | 33 +++++++++++++++++-- packages/block-editor/src/store/reducer.js | 12 +++---- .../src/store/test/private-actions.js | 20 +++++------ .../src/store/test/private-selectors.js | 14 ++++---- .../block-editor/src/store/test/reducer.js | 20 +++++------ 11 files changed, 88 insertions(+), 65 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 70c304b73f449a..4e167df21a85c3 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -45,7 +45,7 @@ function selector( select ) { isTyping, isDragging, isZoomOut, - getBlockVisibilityModalClientIds, + getViewportModalClientIds, } = unlock( select( blockEditorStore ) ); const clientId = @@ -57,7 +57,7 @@ function selector( select ) { isTyping: isTyping(), isZoomOutMode: isZoomOut(), isDragging: isDragging(), - visibilityModalClientIds: getBlockVisibilityModalClientIds(), + viewportModalClientIds: getViewportModalClientIds(), }; } @@ -81,7 +81,7 @@ export default function BlockTools( { isTyping, isZoomOutMode, isDragging, - visibilityModalClientIds, + viewportModalClientIds, } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { @@ -114,8 +114,8 @@ export default function BlockTools( { moveBlocksDown, expandBlock, stopEditingContentOnlySection, - showBlockVisibilityModal, - hideBlockVisibilityModal, + showViewportModal, + hideViewportModal, } = unlock( useDispatch( blockEditorStore ) ); function onKeyDown( event ) { @@ -256,7 +256,7 @@ export default function BlockTools( { } // Open the visibility breakpoints modal. - showBlockVisibilityModal( clientIds ); + showViewportModal( clientIds ); } } @@ -329,10 +329,10 @@ export default function BlockTools( { onClose={ () => setRenamingBlockClientId( null ) } /> ) } - { visibilityModalClientIds && ( + { viewportModalClientIds && ( ) } diff --git a/packages/block-editor/src/components/block-visibility/viewport-menu-item.js b/packages/block-editor/src/components/block-visibility/viewport-menu-item.js index edd68b9c994c2e..349d09dddcb846 100644 --- a/packages/block-editor/src/components/block-visibility/viewport-menu-item.js +++ b/packages/block-editor/src/components/block-visibility/viewport-menu-item.js @@ -31,12 +31,10 @@ export default function BlockVisibilityViewportMenuItem( { clientIds } ) { }, [ clientIds ] ); - const { showBlockVisibilityModal } = unlock( - useDispatch( blockEditorStore ) - ); + const { showViewportModal } = unlock( useDispatch( blockEditorStore ) ); return ( showBlockVisibilityModal( clientIds ) } + onClick={ () => showViewportModal( clientIds ) } shortcut={ shortcut } > { areBlocksHiddenAnywhere ? __( 'Show' ) : __( 'Hide' ) } diff --git a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js index be328794c31cbd..2b154b48c2a16b 100644 --- a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js +++ b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js @@ -23,7 +23,7 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { getBlocksByClientId, getBlockName, isBlockHiddenAnywhere, - getBlockVisibilityModalClientIds, + getViewportModalClientIds, } = unlock( select( blockEditorStore ) ); const _blocks = getBlocksByClientId( clientIds ); return { @@ -37,7 +37,7 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { areBlocksHiddenAnywhere: clientIds?.every( ( clientId ) => isBlockHiddenAnywhere( clientId ) ), - isModalOpen: getBlockVisibilityModalClientIds() !== null, + isModalOpen: getViewportModalClientIds() !== null, }; }, @@ -65,7 +65,7 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { return null; } - const { showBlockVisibilityModal } = unlock( blockEditorDispatch ); + const { showViewportModal } = unlock( blockEditorDispatch ); return ( @@ -75,7 +75,7 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { label={ areBlocksHiddenAnywhere ? __( 'Hidden' ) : __( 'Visible' ) } - onClick={ () => showBlockVisibilityModal( clientIds ) } + onClick={ () => showViewportModal( clientIds ) } aria-expanded={ isModalOpen } aria-haspopup="dialog" /> diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 08307f86c3d703..6b7c7520a92cef 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -99,7 +99,7 @@ function ListViewBlock( { removeBlocks, insertAfterBlock, insertBeforeBlock, - showBlockVisibilityModal, + showViewportModal, } = unlock( useDispatch( blockEditorStore ) ); const debouncedToggleBlockHighlight = useDebounce( @@ -413,7 +413,7 @@ function ListViewBlock( { } // Open the visibility breakpoints modal. - showBlockVisibilityModal( blocksToUpdate ); + showViewportModal( blocksToUpdate ); } else if ( isMatch( 'core/block-editor/rename', event ) ) { const { blocksToUpdate } = getBlocksToUpdate(); const isContentOnly = diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index 1e155fea54352c..5358407bce5164 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -209,7 +209,7 @@ const getQuickActionsCommands = () => return { isLoading: false, commands: [] }; } - const { showBlockVisibilityModal } = unlock( blockEditorDispatch ); + const { showViewportModal } = unlock( blockEditorDispatch ); const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); const canInsertDefaultBlock = canInsertBlockType( getDefaultBlockName(), @@ -301,7 +301,7 @@ const getQuickActionsCommands = () => commands.push( { name: 'toggle-visibility', label: hasHiddenBlock ? __( 'Show' ) : __( 'Hide' ), - callback: () => showBlockVisibilityModal( clientIds ), + callback: () => showViewportModal( clientIds ), icon: hasHiddenBlock ? seen : unseen, } ); } diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 276ca096fd503f..daf9880034dff8 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -477,26 +477,26 @@ export function closeListViewContentPanel() { } /** - * Returns an action object used to open the block visibility modal + * Returns an action object used to open the viewport modal * for the given client IDs. * - * @param {string[]} clientIds Client IDs of blocks to configure visibility for. + * @param {string[]} clientIds Client IDs of blocks to configure viewport settings for. * @return {Object} Action object. */ -export function showBlockVisibilityModal( clientIds ) { +export function showViewportModal( clientIds ) { return { - type: 'SHOW_BLOCK_VISIBILITY_MODAL', + type: 'SHOW_VIEWPORT_MODAL', clientIds, }; } /** - * Returns an action object used to close the block visibility modal. + * Returns an action object used to close the viewport modal. * * @return {Object} Action object. */ -export function hideBlockVisibilityModal() { +export function hideViewportModal() { return { - type: 'HIDE_BLOCK_VISIBILITY_MODAL', + type: 'HIDE_VIEWPORT_MODAL', }; } diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index a44cf73639571a..d2e96dd008da65 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -990,13 +990,40 @@ export function getListViewExpandRevision( state ) { } /** - * Returns the client IDs for the block visibility modal, or null if + * Returns whether a List View panel is opened. + * + * @param {Object} state Global application state. + * @param {string} clientId Client ID of the block. + * + * @return {boolean} Whether the panel is opened. + */ +export function isListViewPanelOpened( state, clientId ) { + // If allOpen flag is set, all panels are open + if ( state.openedListViewPanels?.allOpen ) { + return true; + } + return state.openedListViewPanels?.panels?.[ clientId ] === true; +} + +/** + * Returns the List View expand revision number. + * + * @param {Object} state Global application state. + * + * @return {number} The expand revision number. + */ +export function getListViewExpandRevision( state ) { + return state.listViewExpandRevision || 0; +} + +/** + * Returns the client IDs for the viewport modal, or null if * the modal is not open. * * @param {Object} state Global application state. * * @return {string[]|null} Client IDs for the visibility modal, or null. */ -export function getBlockVisibilityModalClientIds( state ) { - return state.blockVisibilityModalClientIds; +export function getViewportModalClientIds( state ) { + return state.viewportModalClientIds; } diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index d44f3a6c093f02..34672d709eef1f 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1503,19 +1503,19 @@ export function isSelectionEnabled( state = true, action ) { } /** - * Reducer returning the client IDs for the block visibility modal, + * Reducer returning the client IDs for the viewport modal, * or null if the modal is not open. * * @param {string[]|null} state Current state. * @param {Object} action Dispatched action. * - * @return {string[]|null} Client IDs for the visibility modal. + * @return {string[]|null} Client IDs for the viewport modal. */ -export function blockVisibilityModalClientIds( state = null, action ) { +export function viewportModalClientIds( state = null, action ) { switch ( action.type ) { - case 'SHOW_BLOCK_VISIBILITY_MODAL': + case 'SHOW_VIEWPORT_MODAL': return action.clientIds; - case 'HIDE_BLOCK_VISIBILITY_MODAL': + case 'HIDE_VIEWPORT_MODAL': return null; } return state; @@ -2239,7 +2239,7 @@ const combinedReducers = combineReducers( { lastBlockInserted, editedContentOnlySection, blockVisibility, - blockVisibilityModalClientIds, + viewportModalClientIds, blockEditingModes, styleOverrides, removalPromptData, diff --git a/packages/block-editor/src/store/test/private-actions.js b/packages/block-editor/src/store/test/private-actions.js index 2fc09d5da40f9b..c97e0218070349 100644 --- a/packages/block-editor/src/store/test/private-actions.js +++ b/packages/block-editor/src/store/test/private-actions.js @@ -9,8 +9,8 @@ import { setInsertionPoint, startDragging, stopDragging, - showBlockVisibilityModal, - hideBlockVisibilityModal, + showViewportModal, + hideViewportModal, } from '../private-actions'; describe( 'private actions', () => { @@ -124,20 +124,20 @@ describe( 'private actions', () => { } ); } ); - describe( 'showBlockVisibilityModal', () => { - it( 'should return the SHOW_BLOCK_VISIBILITY_MODAL action with clientIds', () => { + describe( 'showViewportModal', () => { + it( 'should return the SHOW_VIEWPORT_MODAL action with clientIds', () => { const clientIds = [ 'client-1', 'client-2' ]; - expect( showBlockVisibilityModal( clientIds ) ).toEqual( { - type: 'SHOW_BLOCK_VISIBILITY_MODAL', + expect( showViewportModal( clientIds ) ).toEqual( { + type: 'SHOW_VIEWPORT_MODAL', clientIds, } ); } ); } ); - describe( 'hideBlockVisibilityModal', () => { - it( 'should return the HIDE_BLOCK_VISIBILITY_MODAL action', () => { - expect( hideBlockVisibilityModal() ).toEqual( { - type: 'HIDE_BLOCK_VISIBILITY_MODAL', + describe( 'hideViewportModal', () => { + it( 'should return the HIDE_VIEWPORT_MODAL action', () => { + expect( hideViewportModal() ).toEqual( { + type: 'HIDE_VIEWPORT_MODAL', } ); } ); } ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index eeb4f4b43c2822..5fc60b0d728168 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -21,7 +21,7 @@ import { isLockedBlock, isBlockHiddenAnywhere, isBlockHiddenAtViewport, - getBlockVisibilityModalClientIds, + getViewportModalClientIds, } from '../private-selectors'; import { getBlockEditingMode } from '../selectors'; import { deviceTypeKey } from '../private-keys'; @@ -1323,22 +1323,20 @@ describe( 'private selectors', () => { } ); } ); - describe( 'getBlockVisibilityModalClientIds', () => { + describe( 'getViewportModalClientIds', () => { it( 'should return null when modal is not open', () => { const state = { - blockVisibilityModalClientIds: null, + viewportModalClientIds: null, }; - expect( getBlockVisibilityModalClientIds( state ) ).toBeNull(); + expect( getViewportModalClientIds( state ) ).toBeNull(); } ); it( 'should return client IDs when modal is open', () => { const clientIds = [ 'client-1', 'client-2' ]; const state = { - blockVisibilityModalClientIds: clientIds, + viewportModalClientIds: clientIds, }; - expect( getBlockVisibilityModalClientIds( state ) ).toEqual( - clientIds - ); + expect( getViewportModalClientIds( state ) ).toEqual( clientIds ); } ); } ); } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 130db3df5f29e2..f42ee848594fea 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -42,7 +42,7 @@ import { zoomLevel, editedContentOnlySection, withDerivedBlockEditingModes, - blockVisibilityModalClientIds, + viewportModalClientIds, } from '../reducer'; import { unlock } from '../../lock-unlock'; @@ -4572,31 +4572,31 @@ describe( 'state', () => { } ); } ); - describe( 'blockVisibilityModalClientIds', () => { + describe( 'viewportModalClientIds', () => { it( 'should default to null', () => { - const state = blockVisibilityModalClientIds( undefined, {} ); + const state = viewportModalClientIds( undefined, {} ); expect( state ).toBeNull(); } ); - it( 'should return clientIds on SHOW_BLOCK_VISIBILITY_MODAL', () => { + it( 'should return clientIds on SHOW_VIEWPORT_MODAL', () => { const clientIds = [ 'client-1', 'client-2' ]; - const state = blockVisibilityModalClientIds( null, { - type: 'SHOW_BLOCK_VISIBILITY_MODAL', + const state = viewportModalClientIds( null, { + type: 'SHOW_VIEWPORT_MODAL', clientIds, } ); expect( state ).toEqual( clientIds ); } ); - it( 'should return null on HIDE_BLOCK_VISIBILITY_MODAL', () => { - const state = blockVisibilityModalClientIds( [ 'client-1' ], { - type: 'HIDE_BLOCK_VISIBILITY_MODAL', + it( 'should return null on HIDE_VIEWPORT_MODAL', () => { + const state = viewportModalClientIds( [ 'client-1' ], { + type: 'HIDE_VIEWPORT_MODAL', } ); expect( state ).toBeNull(); } ); it( 'should return current state for unknown actions', () => { const currentState = [ 'client-1' ]; - const state = blockVisibilityModalClientIds( currentState, { + const state = viewportModalClientIds( currentState, { type: 'UNKNOWN_ACTION', } ); expect( state ).toBe( currentState ); From bbd067cb255cd3cddc485aebfa4dc6137ef81bee Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Feb 2026 11:53:24 +1100 Subject: [PATCH 7/7] Revert aria-expanded change in cdd92a696b56c9e89a48927bb080873da6b7c5c2 --- .../block-visibility/viewport-toolbar.js | 47 ++++++++----------- .../src/store/private-selectors.js | 27 ----------- 2 files changed, 20 insertions(+), 54 deletions(-) diff --git a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js index 2b154b48c2a16b..b95a6c90036ac7 100644 --- a/packages/block-editor/src/components/block-visibility/viewport-toolbar.js +++ b/packages/block-editor/src/components/block-visibility/viewport-toolbar.js @@ -16,33 +16,27 @@ import { unlock } from '../../lock-unlock'; export default function BlockVisibilityViewportToolbar( { clientIds } ) { const hasBlockVisibilityButtonShownRef = useRef( false ); - const { canToggleBlockVisibility, areBlocksHiddenAnywhere, isModalOpen } = - useSelect( - ( select ) => { - const { - getBlocksByClientId, - getBlockName, - isBlockHiddenAnywhere, - getViewportModalClientIds, - } = unlock( select( blockEditorStore ) ); - const _blocks = getBlocksByClientId( clientIds ); - return { - canToggleBlockVisibility: _blocks.every( ( { clientId } ) => - hasBlockSupport( - getBlockName( clientId ), - 'visibility', - true - ) - ), - areBlocksHiddenAnywhere: clientIds?.every( ( clientId ) => - isBlockHiddenAnywhere( clientId ) - ), - isModalOpen: getViewportModalClientIds() !== null, - }; - }, + const { canToggleBlockVisibility, areBlocksHiddenAnywhere } = useSelect( + ( select ) => { + const { getBlocksByClientId, getBlockName, isBlockHiddenAnywhere } = + unlock( select( blockEditorStore ) ); + const _blocks = getBlocksByClientId( clientIds ); + return { + canToggleBlockVisibility: _blocks.every( ( { clientId } ) => + hasBlockSupport( + getBlockName( clientId ), + 'visibility', + true + ) + ), + areBlocksHiddenAnywhere: clientIds?.every( ( clientId ) => + isBlockHiddenAnywhere( clientId ) + ), + }; + }, - [ clientIds ] - ); + [ clientIds ] + ); const blockEditorDispatch = useDispatch( blockEditorStore ); /* @@ -76,7 +70,6 @@ export default function BlockVisibilityViewportToolbar( { clientIds } ) { areBlocksHiddenAnywhere ? __( 'Hidden' ) : __( 'Visible' ) } onClick={ () => showViewportModal( clientIds ) } - aria-expanded={ isModalOpen } aria-haspopup="dialog" /> diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index d2e96dd008da65..cefc88d074f7ca 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -989,33 +989,6 @@ export function getListViewExpandRevision( state ) { return state.listViewExpandRevision || 0; } -/** - * Returns whether a List View panel is opened. - * - * @param {Object} state Global application state. - * @param {string} clientId Client ID of the block. - * - * @return {boolean} Whether the panel is opened. - */ -export function isListViewPanelOpened( state, clientId ) { - // If allOpen flag is set, all panels are open - if ( state.openedListViewPanels?.allOpen ) { - return true; - } - return state.openedListViewPanels?.panels?.[ clientId ] === true; -} - -/** - * Returns the List View expand revision number. - * - * @param {Object} state Global application state. - * - * @return {number} The expand revision number. - */ -export function getListViewExpandRevision( state ) { - return state.listViewExpandRevision || 0; -} - /** * Returns the client IDs for the viewport modal, or null if * the modal is not open.