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
1 change: 1 addition & 0 deletions src/block-components/block-link/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const Edit = props => {
<PanelAdvancedSettings
title={ __( 'Link', i18n ) }
id="link"
responsive={ false }
>
<LinkControls
attrNameTemplate="blockLink%s"
Expand Down
1 change: 1 addition & 0 deletions src/block-components/block-style/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const Edit = memo( props => {
title={ __( 'Styles', i18n ) }
id="styles"
initialOpen={ props.initialOpen }
responsive={ false }
>
<BlockStyles styles={ props.styles } />
{ props.children }
Expand Down
1 change: 1 addition & 0 deletions src/block-components/conditional-display/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const Edit = () => {
title={ __( 'Conditional Display', i18n ) }
id="conditional-display"
isPremiumPanel={ ! isPro }
responsive={ false }
>
{ ! isPro && <ProControl type="conditional-display" /> }
{ isPro &&
Expand Down
1 change: 1 addition & 0 deletions src/block-components/custom-attributes/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const Edit = () => {
<PanelAdvancedSettings
title={ __( 'Custom Attributes', i18n ) }
id="custom-attributes"
responsive={ false }
>
<CustomAttributesControl
label={ __( 'Custom Attributes', i18n ) }
Expand Down
1 change: 1 addition & 0 deletions src/block-components/custom-css/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const Edit = props => {
title={ __( 'Custom CSS', i18n ) }
id="custom-css"
isPremiumPanel={ ! isPro }
responsive={ false }
showModifiedIndicator={ !! customCSSMinified }
>
{ ! isPro && <ProControl type="custom-css" /> }
Expand Down
1 change: 1 addition & 0 deletions src/block-components/effects-animations/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const Edit = props => {
title={ __( 'Motion Effects', i18n ) }
id="effects-animations"
isPremiumPanel={ ! isPro }
responsive={ false }
>
{ ! isPro && <ProControl type="motion-effects" /> }
{ isPro &&
Expand Down
1 change: 1 addition & 0 deletions src/block-components/helpers/backgrounds/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export const BackgroundControls = props => {
{ hasBackgroundMedia &&
<ButtonIconPopoverControl
label={ __( 'Adv. Background Image Settings', i18n ) }
screens="all"
onReset={ () => {
updateAttributes( {
BackgroundPosition: '',
Expand Down
1 change: 1 addition & 0 deletions src/block-components/link/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const Edit = props => {
<PanelAdvancedSettings
title={ __( 'Link', i18n ) }
id="link"
responsive={ false }
hasToggle={ props.hasToggle }
checked={ props.hasToggle ? hasLink : undefined }
onChange={ props.hasToggle ? onChange : undefined }
Expand Down
1 change: 1 addition & 0 deletions src/block-components/transform/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const Edit = () => {
title={ __( 'Transform & Transition', i18n ) }
id="transform-transition"
isPremiumPanel={ ! isPro }
responsive={ false }
>
{ ! isPro && <ProControl type="transforms" /> }
{ isPro &&
Expand Down
1 change: 1 addition & 0 deletions src/block-components/typography/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const Controls = props => {
<ButtonIconPopoverControl
label={ __( 'Typography', i18n ) }
popoverLabel={ __( 'Typography', i18n ) }
screens="all"
onReset={ () => {
updateAttributes( {
[ getAttributeName( 'fontFamily' ) ]: '',
Expand Down
1 change: 1 addition & 0 deletions src/components/advanced-toggle-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const AdvancedToggleControl = memo( props => {
value={ checked }
showReset={ props.defaultValue ? checked !== props.defaultValue : checked }
onChange={ onChange }
screens={ props.responsive }
hasLabel={ false }
defaultValue={ props.defaultValue }
>
Expand Down
11 changes: 11 additions & 0 deletions src/components/base-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/
import BaseControlMultiLabel from '../base-control-multi-label'
import Button from '../button'
import { useRegisterResponsivePanelControl } from '../responsive-control-visibility'
import useResponsiveControlVisibility from '~stackable/hooks/use-responsive-control-visibility'

/**
* External dependencies
Expand All @@ -21,6 +23,11 @@ import { i18n } from 'stackable'
import { __ } from '@wordpress/i18n'

const BaseControl = props => {
const isVisible = useResponsiveControlVisibility( props.screens )
// Register even when this control returns null so its parent panel can tell
// when responsive filtering has removed every control inside it.
useRegisterResponsivePanelControl( isVisible )

const className = classnames( [
'stk-inspector-control',
props.className,
Expand All @@ -33,6 +40,10 @@ const BaseControl = props => {
? props.showReset
: ( typeof props.value !== 'undefined' && props.value !== props.defaultValue && props.value !== props.placeholder )

if ( ! isVisible ) {
return null
}

return (
<_BaseControl
help={ props.help }
Expand Down
10 changes: 10 additions & 0 deletions src/components/base-control2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import ResponsiveToggle from '../responsive-toggle'
import HoverStateToggle from './hover-state-toggle'
import { VisualGuideer } from './use-visual-guide'
import LabelTooltip from './label-tooltip'
import { useRegisterResponsivePanelControl } from '../responsive-control-visibility'
import {
useAttributeName, useBlockAttributesContext, useBlockSetAttributesContext, useDeviceType,
} from '~stackable/hooks'
import useResponsiveControlVisibility from '~stackable/hooks/use-responsive-control-visibility'

/**
* External dependencies
Expand All @@ -36,6 +38,10 @@ const EMPTY_OBJ = {}

export const BaseControl = props => {
const deviceType = useDeviceType()
const isVisible = useResponsiveControlVisibility( props.responsive )
// Register even when this control returns null so its parent panel can tell
// when responsive filtering has removed every control inside it.
useRegisterResponsivePanelControl( isVisible )

const className = classnames( [
'stk-control',
Expand Down Expand Up @@ -64,6 +70,10 @@ export const BaseControl = props => {

const VisualGuide = props.visualGuide !== EMPTY_OBJ ? VisualGuideer : Fragment

if ( ! isVisible ) {
return null
}

return (
<GutBaseControl
help={ props.help }
Expand Down
2 changes: 2 additions & 0 deletions src/components/button-icon-popover-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const ButtonIconPopoverControl = memo( props => {
className={ classnames( 'ugb-button-icon-control', props.className ) }
allowReset={ true }
showReset={ props.allowReset || ( props.onToggle ? props.checked : false ) }
screens={ props.screens }
onReset={ () => {
props.onReset()
if ( props.onToggle ) {
Expand Down Expand Up @@ -109,6 +110,7 @@ ButtonIconPopoverControl.defaultProps = {
onReset: () => {},
checked: false,
onToggle: undefined,
screens: [ 'desktop' ],
}

export default ButtonIconPopoverControl
46 changes: 36 additions & 10 deletions src/components/inspector-tabs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ import { useGlobalState } from '~stackable/util/global-state'
import { __ } from '@wordpress/i18n'
import { getBlockSupport } from '@wordpress/blocks'
import { BlockStylesControl } from '../block-styles-control'
import ResponsiveControlVisibility, { ResponsiveControlFilterProvider } from '../responsive-control-visibility'
import useResponsiveControlVisibility from '~stackable/hooks/use-responsive-control-visibility'
import useCoreResponsiveStylesCompatibility from './use-core-responsive-styles-compatibility'

const { Slot: LayoutPanelSlot, Fill: LayoutPanelFill } = createSlotFill( 'StackableLayoutPanel' )

const InspectorLayoutControls = ( { children } ) => {
return <InspectorControls>
<LayoutPanelFill>{ children }</LayoutPanelFill>
</InspectorControls>
return <ResponsiveControlFilterProvider>
<InspectorControls>
<LayoutPanelFill>{ children }</LayoutPanelFill>
</InspectorControls>
</ResponsiveControlFilterProvider>
}

const InspectorBlockControls = ( { children } ) => {
Expand All @@ -33,7 +38,9 @@ const InspectorBlockControls = ( { children } ) => {
return null
}

return <InspectorControls>{ children }</InspectorControls>
return <ResponsiveControlFilterProvider>
<InspectorControls>{ children }</InspectorControls>
</ResponsiveControlFilterProvider>
}

const InspectorStyleControls = ( { children } ) => {
Expand All @@ -44,7 +51,9 @@ const InspectorStyleControls = ( { children } ) => {
return null
}

return <InspectorControls>{ children }</InspectorControls>
return <ResponsiveControlFilterProvider>
<InspectorControls>{ children }</InspectorControls>
</ResponsiveControlFilterProvider>
}

const InspectorAdvancedControls = ( { children } ) => {
Expand All @@ -55,7 +64,9 @@ const InspectorAdvancedControls = ( { children } ) => {
return null
}

return <InspectorControls>{ children }</InspectorControls>
return <ResponsiveControlFilterProvider>
<InspectorControls>{ children }</InspectorControls>
</ResponsiveControlFilterProvider>
}

export {
Expand All @@ -65,16 +76,31 @@ export {
InspectorAdvancedControls,
}

const ResponsivePanelTabs = props => {
// Core owns its Advanced panel, so expose a scoped marker that lets the
// stylesheet mirror Core's responsive inspector without hiding our tab.
const isResponsiveFiltering = ! useResponsiveControlVisibility( false )

return <PanelTabs
{ ...props }
className={ isResponsiveFiltering ? 'ugb-panel-tabs--is-responsive-filtering' : '' }
/>
}

const InspectorTabs = props => {
const { name, clientId } = useBlockEditContext()
const defaultTab = getBlockSupport( name, 'stkDefaultTab' ) || 'style'
const [ activeTab, setActiveTab ] = useGlobalState( `tabCache-${ name }`, props.tabs.includes( defaultTab ) ? defaultTab : 'style' )

useCoreResponsiveStylesCompatibility( clientId )

return (
<>
<ResponsiveControlFilterProvider>
<InspectorControls>
{ ( isPro || showProNotice ) && <BlockStylesControl blockName={ name } clientId={ clientId } /> }
<PanelTabs
<ResponsiveControlVisibility responsive={ false }>
{ ( isPro || showProNotice ) && <BlockStylesControl blockName={ name } clientId={ clientId } /> }
</ResponsiveControlVisibility>
<ResponsivePanelTabs
tabs={ props.tabs }
initialTab={ activeTab }
onClick={ setActiveTab }
Expand All @@ -94,7 +120,7 @@ const InspectorTabs = props => {
) }
</InspectorBlockControls>

</>
</ResponsiveControlFilterProvider>
)
}

Expand Down
73 changes: 73 additions & 0 deletions src/components/inspector-tabs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Responsive Styles compatibility

This note describes how the Stackable inspector works with the Responsive Styles feature introduced in WordPress 7.1.
It documents the current implementation and does not define a separate responsive attribute system.

## Why the bridge exists

WordPress represents its responsive editing selection as a private block style state such as `@tablet` or `@mobile`.
When one of those states is selected, Core replaces the normal block inspector with its style-state inspector.
That inspector renders controls registered with Core style states, but it does not render Stackable's custom inspector controls.

Stackable already selects its desktop, tablet, and mobile attributes from the editor's visual device type.
The compatibility bridge therefore resets only Core's style-state viewport to `default` while a Stackable block is selected.
It does not change the visual device preview.
Stackable continues to read and write its existing viewport-specific attributes.

When selection leaves the Stackable block, the bridge restores Core's `@tablet` or `@mobile` style-state viewport so native blocks retain their normal behavior.

## Runtime flow

1. [`use-core-responsive-editing.js`](../../hooks/use-core-responsive-editing.js) safely unlocks the private block editor selectors and dispatchers.
2. [`use-core-responsive-styles-compatibility.js`](./use-core-responsive-styles-compatibility.js) keeps the normal Stackable inspector mounted and restores Core state when Stackable no longer owns the selection.
3. [`use-responsive-control-visibility.js`](../../hooks/use-responsive-control-visibility.js) combines Core's Responsive Styles toggle with Stackable's visual device type.
4. [`index.js`](./index.js) scopes that filtering state to Stackable inspector controls.
5. [`base-control/index.js`](../base-control/index.js) and [`base-control2/index.js`](../base-control2/index.js) hide controls that do not support the current viewport.
6. [`responsive-control-visibility/index.js`](../responsive-control-visibility/index.js) lets controls report their visibility to their parent panel.
7. [`panel-advanced-settings/panel-body.js`](../panel-advanced-settings/panel-body.js) hides a panel when all registered controls are filtered, or when the panel has an explicit unsupported capability.
8. [`panel-tabs/editor.scss`](../panel-tabs/editor.scss) hides Core's own Advanced panel while Stackable responsive filtering is active.

## Visibility rules

Filtering is inactive when Responsive Styles is disabled or the visual device is Desktop.
All Stackable controls and panels remain visible in those cases.

Filtering is active when Responsive Styles is enabled and the visual device is Tablet or Mobile.
During filtering, a control is visible only when its existing `screens` or `responsive` metadata includes the current device.

Use `responsive="all"` or `screens="all"` for controls that support Desktop, Tablet, and Mobile.
Use an array when a control supports only specific devices.
An omitted or false control capability is treated as non-responsive during filtering.

The metadata affects inspector visibility only.
It does not select attributes, change values, or alter Stackable's existing responsive write behavior.

## Panel behavior

Panels containing `BaseControl` or `BaseControl2` children normally do not need a `responsive` prop.
Their child controls register their visibility, and the panel hides itself when every registered child is filtered.

Panels with custom, filtered, or third-party content may not have children that participate in registration.
Known desktop-only panels must use `responsive={ false }` so their capability is explicit.
An unannotated panel with no registered controls remains visible because its capability is unknown.
This fallback avoids accidentally hiding a custom panel that may support responsive editing.

## Core Advanced panel

WordPress owns the built-in Advanced panel that contains controls such as HTML anchor and Additional CSS classes.
It is not a Stackable `PanelAdvancedSettings`, so Stackable cannot pass `responsive={ false }` to it.

`ResponsivePanelTabs` adds the `ugb-panel-tabs--is-responsive-filtering` marker while responsive filtering is active.
The panel-tabs stylesheet uses that marker to hide Core's `.block-editor-block-inspector__advanced` panel.
Stackable's own Advanced tab remains available.

## Private API boundary

WordPress 7.1 does not expose the required Responsive Styles state through a public API.
The bridge uses `window.wp.privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules` to unlock the block editor store.
All private API access is isolated and guarded with optional access and `try` blocks.

If the private API is unavailable, the helpers return `undefined` and responsive filtering defaults to inactive.
This prevents an unsupported WordPress version or a future private API change from causing a JavaScript error.

When WordPress exposes a stable public API, replace the private access inside `use-core-responsive-editing.js` while preserving the rest of the visibility interface.
Loading
Loading