Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/reference-guides/data/data-core-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,18 @@ _Returns_

- `boolean`: Whether the publish sidebar is open.

### isResponsiveEditing

Returns whether device-specific editing mode is enabled. When enabled, changes to styles and block visibility apply only to the current device.

_Parameters_

- _state_ `Object`: Global application state.

_Returns_

- `boolean`: Whether device-specific editing is enabled.

### isSavingNonPostEntityChanges

Returns true if non-post entities are currently being saved, or false otherwise.
Expand Down Expand Up @@ -1536,6 +1548,18 @@ _Parameters_

- _mode_ `string`: Mode (one of 'post-only' or 'template-locked').

### setResponsiveEditing

Action that enables or disables device-specific editing mode. When enabled, changes to styles and block visibility apply only to the current device.

_Parameters_

- _enabled_ `boolean`: Whether device-specific editing is enabled.

_Returns_

- `Object`: Action object.

### setTemplateValidity

_Related_
Expand Down
99 changes: 97 additions & 2 deletions lib/block-supports/block-visibility.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,32 @@
*/

/**
* Render nothing if the block is hidden.
* Add 'display' to allowed CSS properties for the style engine.
*
* @param array $styles Array of allowed CSS properties.
* @return array Modified array with 'display' added.
Comment on lines +11 to +12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should type be consistent with the filter type (string[])?

*/
function gutenberg_add_display_to_safe_style_css( $styles ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should $styles be called $attr instead (in sync with the filter naming and docs)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not need this at all since the style engine generates proper CSS rules, and enqueues them. In this case we're not using inline styles unless I'm missing something.

That said, I'm putting together a "backend" PR behind the experiment to kick this feature off. It'll take the best bits from both PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not need this at all since the style engine generates proper CSS rules, and enqueues them. In this case we're not using inline styles unless I'm missing something.

Ignore me.

My brain just reminded me that I wrote WP_Style_Engine_CSS_Declarations::filter_declaration and I forgot that it runs values through the filter. Such a long time ago...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we need to add display to the list in safecss_filter_attr. I'm not sure why it's not in there yet, we output display styles for layout already. It might be those aren't going through the style engine at all.

$styles[] = 'display';
return $styles;
}
add_filter( 'safe_style_css', 'gutenberg_add_display_to_safe_style_css' );

/**
* Get the breakpoint media queries for responsive visibility.
*
* @return array Associative array of viewport => media query.
*/
function gutenberg_get_responsive_visibility_breakpoints() {
return array(
'desktop' => '@media screen and (min-width: 782px)',
'tablet' => '@media screen and (min-width: 600px) and (max-width: 781px)',
'mobile' => '@media screen and (max-width: 599px)',
);
}

/**
* Render nothing if the block is hidden, or add responsive visibility classes.
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
Expand All @@ -19,10 +44,80 @@ function gutenberg_render_block_visibility_support( $block_content, $block ) {
return $block_content;
}

if ( isset( $block['attrs']['metadata']['blockVisibility'] ) && false === $block['attrs']['metadata']['blockVisibility'] ) {
if ( ! isset( $block['attrs']['metadata']['blockVisibility'] ) ) {
return $block_content;
}

$block_visibility = $block['attrs']['metadata']['blockVisibility'];

// If blockVisibility is false, hide the block completely (original behavior).
if ( false === $block_visibility ) {
return '';
}

// If blockVisibility is an object, handle responsive visibility.
if ( is_array( $block_visibility ) ) {
$allowed_devices = array( 'desktop', 'tablet', 'mobile' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow filtering this list, together with the list of breakpoints?

$hidden_devices = array();

foreach ( $block_visibility as $device => $is_hidden ) {
// Only allow whitelisted device names.
if ( false === $is_hidden && in_array( $device, $allowed_devices, true ) ) {
$hidden_devices[] = $device;
}
}

// If all devices are hidden, return empty.
if ( count( $hidden_devices ) === 3 &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we don't hardcode this. Maybe at some point we allow adding additional (or intentionally removing) allowed devices.

Suggested change
if ( count( $hidden_devices ) === 3 &&
if ( count( $hidden_devices ) === count( $allowed_devices ) &&

Copy link
Member Author

@mtias mtias Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we'd make that filterable and adapt. For now it's a good way to ensure no extraneous info sneaks in.

in_array( 'desktop', $hidden_devices, true ) &&
in_array( 'tablet', $hidden_devices, true ) &&
in_array( 'mobile', $hidden_devices, true )
Comment on lines +72 to +74
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this separate check needed? Isn't the count check enough?

) {
return '';
}

// If there are responsive hiding rules, add a class and generate CSS.
if ( ! empty( $hidden_devices ) ) {
// Generate a unique class name based on the hidden devices.
sort( $hidden_devices );
$visibility_class = 'wp-block-visibility-' . implode( '-', $hidden_devices );

// Generate CSS rules using the style engine.
$breakpoints = gutenberg_get_responsive_visibility_breakpoints();
$css_rules = array();

foreach ( $hidden_devices as $device ) {
if ( isset( $breakpoints[ $device ] ) ) {
$css_rules[] = array(
'selector' => '.' . $visibility_class,
'declarations' => array(
'display' => 'none',
),
'rules_group' => $breakpoints[ $device ],
);
}
}

if ( ! empty( $css_rules ) ) {
gutenberg_style_engine_get_stylesheet_from_css_rules(
$css_rules,
array(
'context' => 'block-supports',
)
);
}

// Add the visibility class to the block.
$processor = new WP_HTML_Tag_Processor( $block_content );
if ( $processor->next_tag() ) {
$existing_class = $processor->get_attribute( 'class' );
$new_class = $existing_class ? $existing_class . ' ' . $visibility_class : $visibility_class;
$processor->set_attribute( 'class', $new_class );
$block_content = $processor->get_updated_html();
}
}
}

return $block_content;
}

Expand Down
66 changes: 64 additions & 2 deletions packages/block-editor/src/components/block-inspector/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import {
getBlockType,
getUnregisteredTypeHandlerName,
hasBlockSupport,
store as blocksStore,
} from '@wordpress/blocks';
import { PanelBody, __unstableMotion as motion } from '@wordpress/components';
import {
Notice,
PanelBody,
__unstableMotion as motion,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';

/**
Expand Down Expand Up @@ -332,6 +337,54 @@ const BlockInspectorSingleBlock = ( {
const isBlockSynced = blockInformation.isSynced;
const shouldShowTabs = ! isBlockSynced && hasMultipleTabs;

// Get block visibility information and current device type
const { canToggleBlockVisibility, blockVisibility, deviceType } = useSelect(
( select ) => {
const { getBlockName, getBlockAttributes, getSettings } =
select( blockEditorStore );
const blockAttributes = getBlockAttributes( clientId );
const settings = getSettings();
return {
canToggleBlockVisibility: hasBlockSupport(
getBlockName( clientId ),
'visibility',
true
),
blockVisibility: blockAttributes?.metadata?.blockVisibility,
deviceType: settings.__experimentalDeviceType || 'Desktop',
};
},
[ clientId ]
);

// Check if the block is hidden on the current device
const getVisibilityNotice = () => {
if ( ! canToggleBlockVisibility || blockVisibility === undefined ) {
return null;
}

// If hidden on all devices, show a general notice
if ( blockVisibility === false ) {
return __( 'This block is hidden.' );
}

// If hidden on a specific viewport, only show the notice when previewing that view
if ( typeof blockVisibility === 'object' ) {
const deviceKey = deviceType.toLowerCase();
if ( blockVisibility[ deviceKey ] === false ) {
return sprintf(
/* translators: %s: device type (Desktop, Tablet, or Mobile) */
__( 'Block is hidden on %s' ),
deviceKey
);
}
}

return null;
};

const visibilityNotice = getVisibilityNotice();

return (
<div className="block-editor-block-inspector">
{ hasParentChildBlockCards && (
Expand All @@ -348,6 +401,15 @@ const BlockInspectorSingleBlock = ( {
isChild={ hasParentChildBlockCards }
clientId={ clientId }
/>
{ visibilityNotice && (
<Notice
status="warning"
isDismissible={ false }
className="block-editor-block-inspector__visibility-notice"
>
{ visibilityNotice }
</Notice>
) }
{ window?.__experimentalContentOnlyPatternInsertion && (
<EditContents clientId={ clientId } />
) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@
justify-content: center;
}
}

.block-editor-block-inspector__visibility-notice {
margin: 0 $grid-unit-20 $grid-unit-20;
}
Comment on lines +66 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I wish this were not necessary. Any other way to apply this spacing? Maybe an extra CardBody wrapper?

19 changes: 18 additions & 1 deletion packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,23 @@ function BlockListBlockProvider( props ) {
__experimentalBlockBindingsSupportedAttributes?.[ blockName ];

const hasLightBlockWrapper = blockType?.apiVersion > 1;

// Compute isBlockHidden based on current device preview
const blockVisibility = attributes?.metadata?.blockVisibility;
const settings = getSettings();
let computedIsBlockHidden = blockVisibility === false;

if (
! computedIsBlockHidden &&
typeof blockVisibility === 'object'
) {
const viewportType =
settings.__experimentalDeviceType ?? 'Desktop';
const viewportKey = viewportType.toLowerCase();
computedIsBlockHidden =
blockVisibility[ viewportKey ] === false;
}

const previewContext = {
isPreviewMode,
blockWithoutAttributes,
Expand All @@ -633,7 +650,7 @@ function BlockListBlockProvider( props ) {
? getBlockDefaultClassName( blockName )
: undefined,
blockTitle: blockType?.title,
isBlockHidden: attributes?.metadata?.blockVisibility === false,
isBlockHidden: computedIsBlockHidden,
bindableAttributes,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const BlockSettingsMenuControlsSlot = ( { fillProps, clientIds = null } ) => {
{ showVisibilityButton && (
<BlockVisibilityMenuItem
clientIds={ selectedClientIds }
onClose={ fillProps?.onClose }
/>
) }
{ fills }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function BlockSettingsDropdown( {
openedBlockSettingsMenu,
isContentOnly,
isZoomOut,
responsiveEditing,
} = useSelect(
( select ) => {
const {
Expand All @@ -100,6 +101,7 @@ export function BlockSettingsDropdown( {
getOpenedBlockSettingsMenu,
getBlockEditingMode,
isZoomOut: _isZoomOut,
getSettings,
} = unlock( select( blockEditorStore ) );

const { getActiveBlockVariation } = select( blocksStore );
Expand All @@ -108,6 +110,7 @@ export function BlockSettingsDropdown( {
getBlockRootClientId( firstBlockClientId );
const parentBlockName =
_firstParentClientId && getBlockName( _firstParentClientId );
const settings = getSettings();

return {
firstParentClientId: _firstParentClientId,
Expand All @@ -125,6 +128,8 @@ export function BlockSettingsDropdown( {
isContentOnly:
getBlockEditingMode( firstBlockClientId ) === 'contentOnly',
isZoomOut: _isZoomOut(),
responsiveEditing:
settings.__experimentalResponsiveEditing ?? false,
};
},
[ firstBlockClientId ]
Expand Down Expand Up @@ -371,7 +376,7 @@ export function BlockSettingsDropdown( {
: Children.map( ( child ) =>
cloneElement( child, { onClose } )
) }
{ canRemove && (
{ canRemove && ! responsiveEditing && (
<MenuGroup>
<MenuItem
onClick={ pipe(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export { default as BlockVisibilityMenuItem } from './menu-item';
export { default as BlockVisibilityToolbar } from './toolbar';
export {
isHiddenForViewport,
hasAnyVisibilitySettings,
VIEWPORT_LABELS,
} from './utils';
Loading
Loading