diff --git a/plugins/newspack-plugin/includes/class-blocks.php b/plugins/newspack-plugin/includes/class-blocks.php index 6035d14131..0d03216995 100644 --- a/plugins/newspack-plugin/includes/class-blocks.php +++ b/plugins/newspack-plugin/includes/class-blocks.php @@ -41,6 +41,8 @@ public static function init() { require_once NEWSPACK_ABSPATH . 'src/blocks/overlay-menu/trigger/class-overlay-menu-trigger-block.php'; require_once NEWSPACK_ABSPATH . 'src/blocks/overlay-menu/panel/class-overlay-menu-panel-block.php'; require_once NEWSPACK_ABSPATH . 'src/blocks/overlay-search/class-overlay-search-block.php'; + require_once NEWSPACK_ABSPATH . 'src/blocks/responsive-container/class-responsive-container-block.php'; + require_once NEWSPACK_ABSPATH . 'src/blocks/responsive-container/breakpoint/class-responsive-container-breakpoint-block.php'; Social_Icons::init(); } if ( Collections::is_module_active() ) { diff --git a/plugins/newspack-plugin/src/blocks/index.js b/plugins/newspack-plugin/src/blocks/index.js index 6ff2df0540..dfbb953ea6 100644 --- a/plugins/newspack-plugin/src/blocks/index.js +++ b/plugins/newspack-plugin/src/blocks/index.js @@ -25,6 +25,8 @@ import * as overlayMenu from './overlay-menu'; import * as overlayMenuTrigger from './overlay-menu/trigger'; import * as overlayMenuPanel from './overlay-menu/panel'; import * as overlaySearch from './overlay-search'; +import * as responsiveContainer from './responsive-container'; +import * as responsiveContainerBreakpoint from './responsive-container/breakpoint'; /** * Block Scripts @@ -49,6 +51,8 @@ export const blocks = [ overlayMenuTrigger, overlayMenuPanel, overlaySearch, + responsiveContainer, + responsiveContainerBreakpoint, ]; const readerActivationBlocks = [ 'newspack/reader-registration', 'newspack/my-account-button' ]; @@ -67,7 +71,10 @@ const blockThemeBlocks = [ 'newspack/overlay-menu-panel', 'newspack/my-account-button', 'newspack/overlay-search', + 'newspack/responsive-container', + 'newspack/responsive-container-breakpoint', ]; +const siteEditorOnlyBlocks = [ 'newspack/responsive-container', 'newspack/responsive-container-breakpoint' ]; /** * Function to register an individual block. @@ -102,6 +109,10 @@ const registerBlock = block => { if ( blockThemeBlocks.includes( name ) && ! newspack_blocks.is_block_theme ) { return; } + /** Do not register Site Editor-only blocks outside the Site Editor. */ + if ( siteEditorOnlyBlocks.includes( name ) && window.pagenow !== 'site-editor' ) { + return; + } registerBlockType( blockMetadata, settings ); }; diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/block.json b/plugins/newspack-plugin/src/blocks/responsive-container/block.json new file mode 100644 index 0000000000..a086dd8a70 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/block.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "newspack/responsive-container", + "title": "Responsive Container", + "category": "newspack", + "description": "A container that shows one set of blocks on desktop and another on mobile, swapping automatically at a breakpoint. Ideal for responsive headers and footers.", + "keywords": [ "responsive", "adaptive", "header", "footer", "mobile", "desktop" ], + "textdomain": "newspack-plugin", + "supports": { + "html": false, + "anchor": true, + "position": { "sticky": true } + }, + "editorStyle": "file:../../../dist/blocks.css", + "style": "file:../../../dist/blocks.css" +} diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/block.json b/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/block.json new file mode 100644 index 0000000000..dd3e57b606 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/block.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "newspack/responsive-container-breakpoint", + "title": "Breakpoint", + "category": "newspack", + "description": "A single view (desktop or mobile) within a Responsive Container.", + "textdomain": "newspack-plugin", + "parent": [ "newspack/responsive-container" ], + "attributes": { + "view": { + "type": "string", + "enum": [ "desktop", "mobile" ], + "default": "desktop" + }, + "lock": { + "type": "object", + "default": { "move": true, "remove": true } + } + }, + "supports": { + "html": false, + "lock": false, + "inserter": false + }, + "editorStyle": "file:../../../../dist/blocks.css", + "style": "file:../../../../dist/blocks.css" +} diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/class-responsive-container-breakpoint-block.php b/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/class-responsive-container-breakpoint-block.php new file mode 100644 index 0000000000..a387c320be --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/class-responsive-container-breakpoint-block.php @@ -0,0 +1,38 @@ + { + const { getBlockRootClientId, getBlocks, getBlockOrder } = select( 'core/block-editor' ); + const root = getBlockRootClientId( clientId ); + return { + parentClientId: root, + // Guard against a missing root during transitions (e.g. template-part + // switches): getBlocks( falsy ) returns the top-level blocks, not []. + siblings: root ? getBlocks( root ) : [], + isEmpty: getBlockOrder( clientId ).length === 0, + }; + }, + [ clientId ] + ); + + // Key to our own clientId until the parent resolves, so transient state is + // never shared under a `null` key; useView re-subscribes when it changes. + const [ activeView, setView ] = useView( parentClientId || clientId ); + const isActive = activeView === view; + + const { selectBlock } = useDispatch( 'core/block-editor' ); + + const switchView = newView => { + setView( newView ); + // Move selection onto the breakpoint that is becoming visible (falling + // back to the container) so the toolbar always anchors to a visible block. + const target = siblings.find( block => block.attributes?.view === newView ); + selectBlock( target ? target.clientId : parentClientId || clientId ); + }; + + const className = + 'newspack-responsive-container-breakpoint' + + ` newspack-responsive-container-breakpoint--${ view }` + + ( isActive ? '' : ' is-inactive-view' ) + + ( isEmpty ? ' is-empty' : '' ); + + const blockProps = useBlockProps( { className } ); + + return ( + <> + +
+ +
+ + ); +} diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/index.js b/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/index.js new file mode 100644 index 0000000000..94ec683420 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/breakpoint/index.js @@ -0,0 +1,59 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { desktop, mobile } from '@wordpress/icons'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import colors from '../../../../packages/colors/colors.module.scss'; + +export const title = __( 'Breakpoint', 'newspack-plugin' ); + +const { name } = metadata; + +export { metadata, name }; + +const foreground = colors[ 'primary-400' ]; + +export const settings = { + title, + icon: { + src: desktop, + foreground, + }, + // A single block type shares one static icon, so two variations — matched to + // the `view` attribute via `isActive` — give the desktop and mobile breakpoints + // their own icon and label in the List View and breadcrumb. + variations: [ + { + name: 'desktop', + title: __( 'Desktop', 'newspack-plugin' ), + icon: { src: desktop, foreground }, + attributes: { view: 'desktop' }, + isActive: [ 'view' ], + }, + { + name: 'mobile', + title: __( 'Mobile', 'newspack-plugin' ), + icon: { src: mobile, foreground }, + attributes: { view: 'mobile' }, + isActive: [ 'view' ], + }, + ], + edit: Edit, + save: ( { attributes } ) => { + const blockProps = useBlockProps.save( { + className: `newspack-responsive-container-breakpoint--${ attributes.view }`, + } ); + return ( +
+ +
+ ); + }, +}; diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/class-responsive-container-block.php b/plugins/newspack-plugin/src/blocks/responsive-container/class-responsive-container-block.php new file mode 100644 index 0000000000..c644af541d --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/class-responsive-container-block.php @@ -0,0 +1,99 @@ += this width; + * mobile breakpoint shows at <= ( breakpoint - 1 ). + */ + const DEFAULT_BREAKPOINT = 782; + + /** + * Initializes the block. + * + * @return void + */ + public static function init() { + add_action( 'init', [ __CLASS__, 'register_block' ] ); + add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_visibility_style' ] ); + } + + /** + * Registers the block type from metadata (static block, no render callback). + * + * @return void + */ + public static function register_block() { + register_block_type_from_metadata( __DIR__ . '/block.json' ); + } + + /** + * Resolves the swap breakpoint. + * + * Precedence (highest first): the `newspack_responsive_container_breakpoint` + * filter, the theme.json `settings.custom.newspackResponsiveBreakpoint` value, + * then the default. + * + * @return int Breakpoint in pixels. + */ + public static function get_breakpoint() { + $default = self::DEFAULT_BREAKPOINT; + $custom = wp_get_global_settings( [ 'custom', 'newspackResponsiveBreakpoint' ] ); + $value = is_numeric( $custom ) ? (int) $custom : $default; + + /** + * Filters the breakpoint (in pixels) at which the Responsive Container + * swaps between its desktop and mobile breakpoints. + * + * @param int $value Resolved breakpoint in pixels. + */ + $value = (int) apply_filters( 'newspack_responsive_container_breakpoint', $value ); + + // A non-positive breakpoint would produce invalid media queries; fall back. + return $value > 0 ? $value : $default; + } + + /** + * Builds the front-end visibility CSS for the current breakpoint. + * + * @return string CSS rules. + */ + public static function get_visibility_css() { + $breakpoint = self::get_breakpoint(); + return sprintf( + '@media (max-width:%1$dpx){.newspack-responsive-container-breakpoint--desktop{display:none !important;}}@media (min-width:%2$dpx){.newspack-responsive-container-breakpoint--mobile{display:none !important;}}', + $breakpoint - 1, + $breakpoint + ); + } + + /** + * Enqueues the single global visibility stylesheet on the front-end. + * + * Always enqueued (a few bytes) because the block commonly lives in header/ + * footer template parts, which `has_block()` cannot detect from post content. + * + * @return void + */ + public static function enqueue_visibility_style() { + wp_register_style( 'newspack-responsive-container', false, [], NEWSPACK_PLUGIN_VERSION ); + wp_enqueue_style( 'newspack-responsive-container' ); + wp_add_inline_style( 'newspack-responsive-container', self::get_visibility_css() ); + } +} +Responsive_Container_Block::init(); diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/edit.js b/plugins/newspack-plugin/src/blocks/responsive-container/edit.js new file mode 100644 index 0000000000..c0d74fd318 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/edit.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import ViewToggle from './view-toggle'; +import { useView } from './view-state'; + +const ALLOWED_BLOCKS = [ 'newspack/responsive-container-breakpoint' ]; +const BLOCKS_TEMPLATE = [ + [ 'newspack/responsive-container-breakpoint', { view: 'desktop' } ], + [ 'newspack/responsive-container-breakpoint', { view: 'mobile' } ], +]; + +/** + * Edit component for the Responsive Container block. + * + * Renders a locked template of exactly two breakpoints (desktop + mobile) and + * the view toggle. The edited view defaults to desktop and is held in ephemeral + * editor-only state (shared with the breakpoints, so toggling never dirties the + * post); the inactive breakpoint hides itself in the editor. The same toggle is + * rendered by each breakpoint so it can be switched without reselecting the + * container. + * + * @param {Object} props Block props. + * @param {string} props.clientId Block client ID. + * + * @return {JSX.Element} The block editor UI. + */ +export default function ResponsiveContainerEdit( { clientId } ) { + const [ view, setView ] = useView( clientId ); + + const blockProps = useBlockProps( { + className: 'newspack-responsive-container', + } ); + + return ( + <> + +
+ +
+ + ); +} diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/index.js b/plugins/newspack-plugin/src/blocks/responsive-container/index.js new file mode 100644 index 0000000000..8eafae55fe --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/index.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { resizeCornerNE as icon } from '@wordpress/icons'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import colors from '../../../packages/colors/colors.module.scss'; +import './style.scss'; + +export const title = __( 'Responsive Container', 'newspack-plugin' ); + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + title, + icon: { + src: icon, + foreground: colors[ 'primary-400' ], + }, + keywords: [ + __( 'responsive', 'newspack-plugin' ), + __( 'adaptive', 'newspack-plugin' ), + __( 'header', 'newspack-plugin' ), + __( 'footer', 'newspack-plugin' ), + __( 'mobile', 'newspack-plugin' ), + __( 'desktop', 'newspack-plugin' ), + ], + description: __( + 'A container that shows one set of blocks on desktop and another on mobile, swapping automatically at a breakpoint. Ideal for responsive headers and footers.', + 'newspack-plugin' + ), + edit: Edit, + save: () => ( +
+ +
+ ), +}; diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/style.scss b/plugins/newspack-plugin/src/blocks/responsive-container/style.scss new file mode 100644 index 0000000000..375fca6ea0 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/style.scss @@ -0,0 +1,11 @@ +// Editor-only helpers (these classes are added only in the editor). +.newspack-responsive-container-breakpoint { + &.is-inactive-view { + display: none !important; + } + + // Keep an empty breakpoint clickable instead of collapsing. + &.is-empty { + min-height: 48px; + } +} diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/view-state.js b/plugins/newspack-plugin/src/blocks/responsive-container/view-state.js new file mode 100644 index 0000000000..fcdf931715 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/view-state.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; + +/** + * Ephemeral, editor-only store for which view (desktop / mobile) is being edited + * in each Responsive Container instance. + * + * The value is kept in module memory rather than a block attribute so toggling + * views never dirties the post — the same approach the Overlay Menu block uses + * for its preview state. State is keyed by the container's clientId and shared + * between the container and its breakpoint children. + */ +const views = new Map(); +const listeners = new Map(); + +const DEFAULT_VIEW = 'desktop'; + +/** + * Returns the current view for a container, defaulting to desktop. + * + * @param {string} clientId Container clientId. + * @return {string} 'desktop' | 'mobile' + */ +export function getView( clientId ) { + return views.get( clientId ) || DEFAULT_VIEW; +} + +/** + * Sets the view for a container and notifies subscribers. + * + * @param {string} clientId Container clientId. + * @param {string} view 'desktop' | 'mobile' + */ +function setView( clientId, view ) { + views.set( clientId, view ); + listeners.get( clientId )?.forEach( callback => callback( view ) ); +} + +/** + * Subscribe to the view of a container instance and read/update it. + * + * @param {string} clientId Container clientId (the container's own, or a + * breakpoint's parent clientId). + * @return {Array} `[ view, setViewForContainer ]`. + */ +export function useView( clientId ) { + const [ view, setLocal ] = useState( () => getView( clientId ) ); + + useEffect( () => { + // Re-sync in case the value changed between render and subscribe. + setLocal( getView( clientId ) ); + let subscribers = listeners.get( clientId ); + if ( ! subscribers ) { + subscribers = new Set(); + listeners.set( clientId, subscribers ); + } + subscribers.add( setLocal ); + return () => { + subscribers.delete( setLocal ); + if ( subscribers.size === 0 ) { + listeners.delete( clientId ); + } + }; + }, [ clientId ] ); + + return [ view, newView => setView( clientId, newView ) ]; +} diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/view-state.test.js b/plugins/newspack-plugin/src/blocks/responsive-container/view-state.test.js new file mode 100644 index 0000000000..c3a750c573 --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/view-state.test.js @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { renderHook, act } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { getView, useView } from './view-state'; + +/** + * Each test uses a unique clientId because the view store is module-global and + * persists for the lifetime of the editor session (and so across tests here). + */ +describe( 'responsive-container view-state', () => { + it( 'defaults to the desktop view for an unknown container', () => { + expect( getView( 'unknown' ) ).toBe( 'desktop' ); + } ); + + it( 'useView returns the current view and a setter', () => { + const { result } = renderHook( () => useView( 'returns' ) ); + const [ view, setView ] = result.current; + expect( view ).toBe( 'desktop' ); + expect( typeof setView ).toBe( 'function' ); + } ); + + it( 'updates the view and reflects it in getView', () => { + const { result } = renderHook( () => useView( 'updates' ) ); + act( () => result.current[ 1 ]( 'mobile' ) ); + expect( result.current[ 0 ] ).toBe( 'mobile' ); + expect( getView( 'updates' ) ).toBe( 'mobile' ); + } ); + + it( 'shares state across instances of the same container', () => { + // The container and its breakpoint children all subscribe to the same + // clientId; setting from one must update the others. + const container = renderHook( () => useView( 'shared' ) ); + const breakpoint = renderHook( () => useView( 'shared' ) ); + + act( () => container.result.current[ 1 ]( 'mobile' ) ); + + expect( container.result.current[ 0 ] ).toBe( 'mobile' ); + expect( breakpoint.result.current[ 0 ] ).toBe( 'mobile' ); + } ); + + it( 'keeps separate containers independent', () => { + const first = renderHook( () => useView( 'independent-a' ) ); + const second = renderHook( () => useView( 'independent-b' ) ); + + act( () => first.result.current[ 1 ]( 'mobile' ) ); + + expect( first.result.current[ 0 ] ).toBe( 'mobile' ); + expect( second.result.current[ 0 ] ).toBe( 'desktop' ); + } ); + + it( 'unsubscribing one instance does not break the others', () => { + const a = renderHook( () => useView( 'cleanup' ) ); + const b = renderHook( () => useView( 'cleanup' ) ); + + a.unmount(); + act( () => b.result.current[ 1 ]( 'mobile' ) ); + + expect( b.result.current[ 0 ] ).toBe( 'mobile' ); + expect( getView( 'cleanup' ) ).toBe( 'mobile' ); + } ); + + it( 'initializes from the persisted view when remounted', () => { + // View is ephemeral (not a block attribute) but lives in module memory, + // so a remounted instance should pick up the last-set value. + const first = renderHook( () => useView( 'persist' ) ); + act( () => first.result.current[ 1 ]( 'mobile' ) ); + first.unmount(); + + const second = renderHook( () => useView( 'persist' ) ); + expect( second.result.current[ 0 ] ).toBe( 'mobile' ); + } ); +} ); diff --git a/plugins/newspack-plugin/src/blocks/responsive-container/view-toggle.js b/plugins/newspack-plugin/src/blocks/responsive-container/view-toggle.js new file mode 100644 index 0000000000..6689b4289d --- /dev/null +++ b/plugins/newspack-plugin/src/blocks/responsive-container/view-toggle.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { BlockControls } from '@wordpress/block-editor'; +import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; + +/** + * Toolbar toggle for switching the edited view (desktop / mobile). + * + * Rendered in the BlockControls of both the container and its breakpoints so the + * active view can be switched from whichever block is selected — mirroring how + * the Overlay Menu surfaces its toggle on more than one related block, and + * avoiding the need to climb back up to the container to switch views. + * + * @param {Object} props Component props. + * @param {string} props.value Current view ( 'desktop' | 'mobile' ). + * @param {Function} props.onChange Called with the chosen view. + * + * @return {JSX.Element} The toolbar control. + */ +export default function ViewToggle( { value, onChange } ) { + return ( + + + onChange( 'mobile' ) } + /> + onChange( 'desktop' ) } + /> + + + ); +} diff --git a/plugins/newspack-plugin/tests/unit-tests/class-test-responsive-container-block.php b/plugins/newspack-plugin/tests/unit-tests/class-test-responsive-container-block.php new file mode 100644 index 0000000000..322439d4ed --- /dev/null +++ b/plugins/newspack-plugin/tests/unit-tests/class-test-responsive-container-block.php @@ -0,0 +1,125 @@ +theme_json_filter ) { + remove_filter( 'wp_theme_json_data_theme', $this->theme_json_filter ); + $this->theme_json_filter = null; + } + wp_clean_theme_json_cache(); + parent::tear_down(); + } + + /** + * Inject a custom theme.json setting for the breakpoint. + * + * @param mixed $value Value to set for settings.custom.newspackResponsiveBreakpoint. + */ + private function set_theme_json_breakpoint( $value ): void { + $this->theme_json_filter = function ( $theme_json ) use ( $value ) { + return $theme_json->update_with( + [ + 'version' => 2, + 'settings' => [ 'custom' => [ 'newspackResponsiveBreakpoint' => $value ] ], + ] + ); + }; + add_filter( 'wp_theme_json_data_theme', $this->theme_json_filter ); + wp_clean_theme_json_cache(); + } + + /** + * Default breakpoint is 782 when nothing overrides it. + */ + public function test_default_breakpoint(): void { + $this->assertSame( 782, Responsive_Container_Block::get_breakpoint() ); + } + + /** + * The filter overrides the default. + */ + public function test_filter_overrides_breakpoint(): void { + add_filter( 'newspack_responsive_container_breakpoint', fn() => 1024 ); + $this->assertSame( 1024, Responsive_Container_Block::get_breakpoint() ); + } + + /** + * A numeric theme.json custom setting is used. + */ + public function test_theme_json_custom_breakpoint(): void { + $this->set_theme_json_breakpoint( 900 ); + $this->assertSame( 900, Responsive_Container_Block::get_breakpoint() ); + } + + /** + * A non-numeric theme.json custom setting falls back to the default. + */ + public function test_non_numeric_custom_falls_back_to_default(): void { + $this->set_theme_json_breakpoint( 'nope' ); + $this->assertSame( 782, Responsive_Container_Block::get_breakpoint() ); + } + + /** + * The filter beats the theme.json custom setting. + */ + public function test_filter_beats_theme_json(): void { + $this->set_theme_json_breakpoint( 900 ); + add_filter( 'newspack_responsive_container_breakpoint', fn() => 600 ); + $this->assertSame( 600, Responsive_Container_Block::get_breakpoint() ); + } + + /** + * A non-positive breakpoint (from filter or theme.json) falls back to the default. + */ + public function test_non_positive_breakpoint_falls_back_to_default(): void { + add_filter( 'newspack_responsive_container_breakpoint', fn() => 0 ); + $this->assertSame( 782, Responsive_Container_Block::get_breakpoint() ); + } + + /** + * The visibility CSS uses the resolved breakpoint and the breakpoint modifier classes. + */ + public function test_visibility_css_uses_breakpoint(): void { + add_filter( 'newspack_responsive_container_breakpoint', fn() => 800 ); + $css = Responsive_Container_Block::get_visibility_css(); + $this->assertStringContainsString( '(max-width:799px)', $css ); + $this->assertStringContainsString( '(min-width:800px)', $css ); + $this->assertStringContainsString( 'newspack-responsive-container-breakpoint--desktop', $css ); + $this->assertStringContainsString( 'newspack-responsive-container-breakpoint--mobile', $css ); + } +}