diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c0f70070908c1..b80804b824e3c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -135,7 +135,9 @@ jobs: uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: build-assets - path: ./build/ + path: | + ./build/ + ./build-module/ test-php: name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on ubuntu-latest @@ -212,7 +214,6 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: build-assets - path: ./build - name: Docker debug information run: | diff --git a/backport-changelog/6.7/7360.md b/backport-changelog/6.7/7360.md new file mode 100644 index 0000000000000..b2fb8efd624b9 --- /dev/null +++ b/backport-changelog/6.7/7360.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7360 + +* https://github.com/WordPress/gutenberg/pull/65460 diff --git a/lib/client-assets.php b/lib/client-assets.php index 62e874d6b06c8..2343530e5595a 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -601,6 +601,56 @@ function gutenberg_register_vendor_scripts( $scripts ) { } add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); +/** + * Registers or re-registers Gutenberg Script Modules. + * + * Script modules that are registered by Core will be re-registered by Gutenberg. + * + * @since 19.3.0 + */ +function gutenberg_default_script_modules() { + /* + * Expects multidimensional array like: + * + * 'interactivity/index.min.js' => array('dependencies' => array(…), 'version' => '…'), + * 'interactivity/debug.min.js' => array('dependencies' => array(…), 'version' => '…'), + * 'interactivity-router/index.min.js' => … + */ + $assets = include gutenberg_dir_path() . '/build-module/assets.php'; + + foreach ( $assets as $file_name => $script_module_data ) { + /* + * Build the WordPress Script Module ID from the file name. + * Prepend `@wordpress/` and remove extensions and `/index` if present: + * - interactivity/index.min.js => @wordpress/interactivity + * - interactivity/debug.min.js => @wordpress/interactivity/debug + * - block-library/query/view.js => @wordpress/block-library/query/view + */ + $script_module_id = '@wordpress/' . preg_replace( '~(?:/index)?\.min\.js$~D', '', $file_name, 1 ); + switch ( $script_module_id ) { + /* + * Interactivity exposes two entrypoints, "/index" and "/debug". + * "/debug" should replalce "/index" in devlopment. + */ + case '@wordpress/interactivity/debug': + if ( ! SCRIPT_DEBUG ) { + continue 2; + } + $script_module_id = '@wordpress/interactivity'; + break; + case '@wordpress/interactivity': + if ( SCRIPT_DEBUG ) { + continue 2; + } + break; + } + + $path = gutenberg_url( "build-module/{$file_name}" ); + wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'] ); + } +} +remove_action( 'wp_default_scripts', 'wp_default_script_modules' ); +add_action( 'wp_default_scripts', 'gutenberg_default_script_modules' ); /* * Always remove the Core action hook while gutenberg_enqueue_stored_styles() exists to avoid styles being printed twice. diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php index f65bc1704dd89..fe23786fc0362 100644 --- a/lib/experimental/script-modules.php +++ b/lib/experimental/script-modules.php @@ -239,26 +239,5 @@ function gutenberg_a11y_script_module_html() { . '
' . ''; } - -/** - * Registers Gutenberg Script Modules. - * - * @since 19.3 - */ -function gutenberg_register_script_modules() { - // When in production, use the plugin's version as the default asset version; - // else (for development or test) default to use the current time. - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - - wp_deregister_script_module( '@wordpress/a11y' ); - wp_register_script_module( - '@wordpress/a11y', - gutenberg_url( 'build-module/a11y/index.min.js' ), - array(), - $default_version - ); - - add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' ); - add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' ); -} -add_action( 'init', 'gutenberg_register_script_modules' ); +add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' ); +add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' ); diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index c00d68bc70e8e..ff68936f054a7 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -5,37 +5,6 @@ * @package gutenberg */ -/** - * Deregisters the Core Interactivity API Modules and replace them - * with the ones from the Gutenberg plugin. - */ -function gutenberg_reregister_interactivity_script_modules() { - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - wp_deregister_script_module( '@wordpress/interactivity' ); - wp_deregister_script_module( '@wordpress/interactivity-router' ); - - wp_register_script_module( - '@wordpress/interactivity', - gutenberg_url( '/build-module/' . ( SCRIPT_DEBUG ? 'interactivity/debug.min.js' : 'interactivity/index.min.js' ) ), - array(), - $default_version - ); - - wp_register_script_module( - '@wordpress/interactivity-router', - gutenberg_url( '/build-module/interactivity-router/index.min.js' ), - array( - array( - 'id' => '@wordpress/a11y', - 'import' => 'dynamic', - ), - '@wordpress/interactivity', - ), - $default_version - ); -} -add_action( 'init', 'gutenberg_reregister_interactivity_script_modules' ); - /** * Adds script data to the interactivity-router script module. * diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index a474d1df7e56d..75d7e8879572d 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -1027,11 +1027,11 @@ _Parameters_ ### useZoomOut -A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. +A hook used to set the zoomed out view, invoking the hook sets the mode. _Parameters_ -- _zoomOut_ `boolean`: If we should enter into zoomOut mode or not +- _zoomOut_ `boolean`: If we should zoom out or not. ### Warning diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js index d0001bd3b33c6..92c54bac9b806 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js @@ -16,14 +16,17 @@ import { unlock } from '../../../lock-unlock'; * @param {string} clientId Block client ID. */ export function useZoomOutModeExit( { editorMode } ) { - const { getSettings } = useSelect( blockEditorStore ); - const { __unstableSetEditorMode } = unlock( + const { getSettings, isZoomOut } = unlock( useSelect( blockEditorStore ) ); + const { __unstableSetEditorMode, resetZoomLevel } = unlock( useDispatch( blockEditorStore ) ); return useRefEffect( ( node ) => { - if ( editorMode !== 'zoom-out' ) { + // In "compose" mode. + const composeMode = editorMode === 'zoom-out' && isZoomOut(); + + if ( ! composeMode ) { return; } @@ -39,6 +42,7 @@ export function useZoomOutModeExit( { editorMode } ) { __experimentalSetIsInserterOpened( false ); } __unstableSetEditorMode( 'edit' ); + resetZoomLevel(); } } diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index bb307816fd150..2b76804785a57 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -11,6 +11,7 @@ import { isRTL } from '@wordpress/i18n'; */ import { store as blockEditorStore } from '../../store'; import { InsertionPointOpenRef } from '../block-tools/insertion-point'; +import { unlock } from '../../lock-unlock'; export function useInBetweenInserter() { const openRef = useContext( InsertionPointOpenRef ); @@ -31,7 +32,8 @@ export function useInBetweenInserter() { getBlockEditingMode, getBlockName, getBlockAttributes, - } = useSelect( blockEditorStore ); + getParentSectionBlock, + } = unlock( useSelect( blockEditorStore ) ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); @@ -133,7 +135,8 @@ export function useInBetweenInserter() { const clientId = element.id.slice( 'block-'.length ); if ( ! clientId || - __unstableIsWithinBlockOverlay( clientId ) + __unstableIsWithinBlockOverlay( clientId ) || + !! getParentSectionBlock( clientId ) ) { return; } diff --git a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js index 560bfc6ebb58d..b8736de11481a 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js @@ -15,6 +15,7 @@ import BlockDraggable from '../block-draggable'; import BlockMover from '../block-mover'; import Shuffle from '../block-toolbar/shuffle'; import NavigableToolbar from '../navigable-toolbar'; +import { unlock } from '../../lock-unlock'; export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { const selected = useSelect( @@ -76,8 +77,9 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { setIsInserterOpened, } = selected; - const { removeBlock, __unstableSetEditorMode } = - useDispatch( blockEditorStore ); + const { removeBlock, __unstableSetEditorMode, resetZoomLevel } = unlock( + useDispatch( blockEditorStore ) + ); const showBlockDraggable = canMove && ! isBlockTemplatePart; @@ -132,6 +134,7 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { setIsInserterOpened( false ); } __unstableSetEditorMode( 'edit' ); + resetZoomLevel(); __unstableContentRef.current?.focus(); } } /> diff --git a/packages/block-editor/src/components/tool-selector/index.js b/packages/block-editor/src/components/tool-selector/index.js index bf55c78caf489..53fab037ba368 100644 --- a/packages/block-editor/src/components/tool-selector/index.js +++ b/packages/block-editor/src/components/tool-selector/index.js @@ -18,6 +18,7 @@ import { Icon, edit as editIcon } from '@wordpress/icons'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; const selectIcon = ( select( blockEditorStore ).__unstableGetEditorMode(), [] ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { __unstableSetEditorMode } = unlock( + useDispatch( blockEditorStore ) + ); return ( { - // Default to the `key` or the source label when `getValues` doesn't exist - values[ attr ] = - bindings[ attr ].args?.key || source.label; + // Default to the the source label when `getValues` doesn't exist. + values[ attr ] = source.label; } ); } else { values = source.getValues( { diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index d7e21ec1be057..2a1b43060c00a 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -8,46 +8,40 @@ import { useEffect, useRef } from '@wordpress/element'; * Internal dependencies */ import { store as blockEditorStore } from '../store'; +import { unlock } from '../lock-unlock'; /** - * A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. + * A hook used to set the zoomed out view, invoking the hook sets the mode. * - * @param {boolean} zoomOut If we should enter into zoomOut mode or not + * @param {boolean} zoomOut If we should zoom out or not. */ export function useZoomOut( zoomOut = true ) { - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const { __unstableGetEditorMode } = useSelect( blockEditorStore ); + const { setZoomLevel } = unlock( useDispatch( blockEditorStore ) ); + const { isZoomOut } = unlock( useSelect( blockEditorStore ) ); - const originalEditingModeRef = useRef( null ); - const mode = __unstableGetEditorMode(); + const originalIsZoomOutRef = useRef( null ); useEffect( () => { // Only set this on mount so we know what to return to when we unmount. - if ( ! originalEditingModeRef.current ) { - originalEditingModeRef.current = mode; + if ( ! originalIsZoomOutRef.current ) { + originalIsZoomOutRef.current = isZoomOut(); } - return () => { - // We need to use __unstableGetEditorMode() here and not `mode`, as mode may not update on unmount - if ( - __unstableGetEditorMode() === 'zoom-out' && - __unstableGetEditorMode() !== originalEditingModeRef.current - ) { - __unstableSetEditorMode( originalEditingModeRef.current ); - } - }; - }, [] ); - - // The effect opens the zoom-out view if we want it open and it's not currently in zoom-out mode. - useEffect( () => { - if ( zoomOut && mode !== 'zoom-out' ) { - __unstableSetEditorMode( 'zoom-out' ); + // The effect opens the zoom-out view if we want it open and the canvas is not currently zoomed-out. + if ( zoomOut && isZoomOut() === false ) { + setZoomLevel( 50 ); } else if ( ! zoomOut && - __unstableGetEditorMode() === 'zoom-out' && - originalEditingModeRef.current !== mode + isZoomOut() && + originalIsZoomOutRef.current !== isZoomOut() ) { - __unstableSetEditorMode( originalEditingModeRef.current ); + setZoomLevel( originalIsZoomOutRef.current ? 50 : 100 ); } - }, [ __unstableGetEditorMode, __unstableSetEditorMode, zoomOut ] ); // Mode is deliberately excluded from the dependencies so that the effect does not run when mode changes. + + return () => { + if ( isZoomOut() && isZoomOut() !== originalIsZoomOutRef.current ) { + setZoomLevel( originalIsZoomOutRef.current ? 50 : 100 ); + } + }; + }, [ isZoomOut, setZoomLevel, zoomOut ] ); } diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index dc57d61fd6b76..441a07202c42a 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -383,3 +383,26 @@ export const modifyContentLockBlock = focusModeToRevert ); }; + +/** + * Sets the zoom level. + * + * @param {number} zoom the new zoom level + * @return {Object} Action object. + */ +export function setZoomLevel( zoom = 100 ) { + return { + type: 'SET_ZOOM_LEVEL', + zoom, + }; +} + +/** + * Resets the Zoom state. + * @return {Object} Action object. + */ +export function resetZoomLevel() { + return { + type: 'RESET_ZOOM_LEVEL', + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 0e77e8e2ed433..d8955bd6342c4 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -616,3 +616,23 @@ export function isZoomOutMode( state ) { export function getSectionRootClientId( state ) { return state.settings?.[ sectionRootClientIdKey ]; } + +/** + * Returns the zoom out state. + * + * @param {Object} state Global application state. + * @return {boolean} The zoom out state. + */ +export function getZoomLevel( state ) { + return state.zoomLevel; +} + +/** + * Returns whether the editor is considered zoomed out. + * + * @param {Object} state Global application state. + * @return {boolean} Whether the editor is zoomed. + */ +export function isZoomOut( state ) { + return getZoomLevel( state ) < 100; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 3a99d18ef17ee..827a2141f44c1 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2060,6 +2060,25 @@ export function hoveredBlockClientId( state = false, action ) { return state; } +/** + * Reducer setting zoom out state. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function zoomLevel( state = 100, action ) { + switch ( action.type ) { + case 'SET_ZOOM_LEVEL': + return action.zoom; + case 'RESET_ZOOM_LEVEL': + return 100; + } + + return state; +} + const combinedReducers = combineReducers( { blocks, isDragging, @@ -2092,6 +2111,7 @@ const combinedReducers = combineReducers( { openedBlockSettingsMenu, registeredInserterMediaCategories, hoveredBlockClientId, + zoomLevel, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 806e2b5f5993e..20d6627398886 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2901,6 +2901,14 @@ export function __unstableIsWithinBlockOverlay( state, clientId ) { return false; } +function isWithinBlock( state, clientId, parentClientId ) { + let parent = state.blocks.parents.get( clientId ); + while ( !! parent && parent !== parentClientId ) { + parent = state.blocks.parents.get( parent ); + } + return parent === parentClientId; +} + /** * @typedef {import('../components/block-editing-mode').BlockEditingMode} BlockEditingMode */ @@ -2968,36 +2976,58 @@ export const getBlockEditingMode = createRegistrySelector( return 'disabled'; } + if ( editorMode === 'navigation' ) { + const sectionRootClientId = getSectionRootClientId( state ); + + // The root section is "default mode" + if ( clientId === sectionRootClientId ) { + return 'default'; + } + + // Sections should always be contentOnly in navigation mode. + const sectionsClientIds = getBlockOrder( + state, + sectionRootClientId + ); + if ( sectionsClientIds.includes( clientId ) ) { + return 'contentOnly'; + } + + // Blocks outside sections should be disabled. + const isWithinSectionRoot = isWithinBlock( + state, + clientId, + sectionRootClientId + ); + if ( ! isWithinSectionRoot ) { + return 'disabled'; + } + + // The rest of the blocks depend on whether they are content blocks or not. + // This "flattens" the sections tree. + const name = getBlockName( state, clientId ); + const isContent = + select( blocksStore ).__experimentalHasContentRoleAttribute( + name + ); + return isContent ? 'contentOnly' : 'disabled'; + } + + // In normal mode, consider that an explicitely set editing mode takes over. const blockEditingMode = state.blockEditingModes.get( clientId ); if ( blockEditingMode ) { return blockEditingMode; } + + // In normal mode, top level is default mode. if ( ! clientId ) { return 'default'; } - const sectionRootClientId = getSectionRootClientId( state ); - if ( - editorMode === 'navigation' && - clientId === sectionRootClientId - ) { - return 'default'; - } - const sectionsClientIds = getBlockOrder( - state, - sectionRootClientId - ); + const rootClientId = getBlockRootClientId( state, clientId ); const templateLock = getTemplateLock( state, rootClientId ); - if ( - templateLock === 'contentOnly' || - editorMode === 'navigation' - ) { - // Sections should always be contentOnly in navigation mode. - // This will also cause them to display in List View providing - // a structure. - if ( sectionsClientIds.includes( clientId ) ) { - return 'contentOnly'; - } + // If the parent of the block is contentOnly locked, check whether it's a content block. + if ( templateLock === 'contentOnly' ) { const name = getBlockName( state, clientId ); const isContent = select( blocksStore ).__experimentalHasContentRoleAttribute( @@ -3005,6 +3035,7 @@ export const getBlockEditingMode = createRegistrySelector( ); return isContent ? 'contentOnly' : 'disabled'; } + // Otherwise, check if there's an ancestor that is contentOnly const parentMode = getBlockEditingMode( state, rootClientId ); return parentMode === 'contentOnly' ? 'default' : parentMode; } diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 04899a9ee243f..2895f6573dc54 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -15,6 +15,7 @@ import { select, dispatch } from '@wordpress/data'; */ import * as selectors from '../selectors'; import { store } from '../'; +import { sectionRootClientIdKey } from '../private-keys'; const { getBlockName, @@ -4382,12 +4383,28 @@ describe( 'getBlockEditingMode', () => { settings: {}, blocks: { byClientId: new Map( [ - [ '6cf70164-9097-4460-bcbf-200560546988', {} ], // Header - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', {} ], // Group - [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', {} ], // | Post Title - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', {} ], // | Post Content - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', {} ], // | | Paragraph - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', {} ], // | | Paragraph + [ + '6cf70164-9097-4460-bcbf-200560546988', + { name: 'core/template-part' }, + ], // Header + [ + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', + { name: 'core/group' }, + ], // Group + [ + 'b26fc763-417d-4f01-b81c-2ec61e14a972', + { name: 'core/post-title' }, + ], // | Post Title + [ + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + { name: 'core/group' }, + ], // | Group + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', { name: 'core/p' } ], // | | Paragraph + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', { name: 'core/p' } ], // | | Paragraph + [ + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', + { name: 'core/group' }, + ], // | | Group ] ), order: new Map( [ [ @@ -4411,10 +4428,12 @@ describe( 'getBlockEditingMode', () => { [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', ], ], [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', [] ], [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', [] ], + [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', [] ], ] ), parents: new Map( [ [ '6cf70164-9097-4460-bcbf-200560546988', '' ], @@ -4435,6 +4454,10 @@ describe( 'getBlockEditingMode', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', ], + [ + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + ], ] ), }, blockListSettings: { @@ -4444,7 +4467,18 @@ describe( 'getBlockEditingMode', () => { blockEditingModes: new Map( [] ), }; - const __experimentalHasContentRoleAttribute = jest.fn( () => false ); + const navigationModeStateWithRootSection = { + ...baseState, + editorMode: 'navigation', + settings: { + [ sectionRootClientIdKey ]: 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', // The group is the "main" container + }, + }; + + const __experimentalHasContentRoleAttribute = jest.fn( ( name ) => { + // consider paragraphs as content blocks. + return name === 'core/p'; + } ); getBlockEditingMode.registry = { select: jest.fn( () => ( { __experimentalHasContentRoleAttribute, @@ -4573,4 +4607,61 @@ describe( 'getBlockEditingMode', () => { getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) ).toBe( 'contentOnly' ); } ); + + it( 'in navigation mode, the root section container is default', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' + ) + ).toBe( 'default' ); + } ); + + it( 'in navigation mode, anything outside the section container is disabled', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + '6cf70164-9097-4460-bcbf-200560546988' + ) + ).toBe( 'disabled' ); + } ); + + it( 'in navigation mode, sections are contentOnly', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'b26fc763-417d-4f01-b81c-2ec61e14a972' + ) + ).toBe( 'contentOnly' ); + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' + ) + ).toBe( 'contentOnly' ); + } ); + + it( 'in navigation mode, blocks with content attributes within sections are contentOnly', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'contentOnly' ); + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c' + ) + ).toBe( 'contentOnly' ); + } ); + + it( 'in navigation mode, blocks without content attributes within sections are disabled', () => { + expect( + getBlockEditingMode( + navigationModeStateWithRootSection, + '9b9c5c3f-2e46-4f02-9e14-9fed515b958s' + ) + ).toBe( 'disabled' ); + } ); } ); diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 85cc840201da5..8ea668d56d854 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -19,18 +19,7 @@ function render_block_core_file( $attributes, $content ) { // If it's interactive, enqueue the script module and add the directives. if ( ! empty( $attributes['displayPreview'] ) ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/file/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/file', - isset( $module_url ) ? $module_url : includes_url( "blocks/file/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/file' ); + wp_enqueue_script_module( '@wordpress/block-library/file/view' ); $processor = new WP_HTML_Tag_Processor( $content ); $processor->next_tag(); diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index abbb03c095245..5d7815a1f2f3f 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -70,19 +70,7 @@ function render_block_core_image( $attributes, $content, $block ) { isset( $lightbox_settings['enabled'] ) && true === $lightbox_settings['enabled'] ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/image/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/image', - isset( $module_url ) ? $module_url : includes_url( "blocks/image/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - - wp_enqueue_script_module( '@wordpress/block-library/image' ); + wp_enqueue_script_module( '@wordpress/block-library/image/view' ); /* * This render needs to happen in a filter with priority 15 to ensure that diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index ec72b03b6906f..10fec84ed59d9 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -622,18 +622,7 @@ private static function get_nav_element_directives( $is_interactive ) { */ private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { if ( static::is_interactive( $attributes, $inner_blocks ) ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/navigation/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/navigation', - isset( $module_url ) ? $module_url : includes_url( "blocks/navigation/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/navigation' ); + wp_enqueue_script_module( '@wordpress/block-library/navigation/view' ); } } diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php index 64cdd156a5431..9126355c096a5 100644 --- a/packages/block-library/src/post-template/index.php +++ b/packages/block-library/src/post-template/index.php @@ -64,11 +64,6 @@ function render_block_core_post_template( $attributes, $content, $block ) { if ( in_the_loop() ) { $query = clone $wp_query; $query->rewind_posts(); - - // If in a single post of any post type, default to the 'post' post type. - if ( is_singular() ) { - query_posts( array( 'post_type' => 'post' ) ); - } } else { $query = $wp_query; } diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index 22bfa7b713801..b2225192c6b21 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -5,6 +5,7 @@ "title": "Query Loop", "category": "theme", "description": "An advanced block that allows displaying post types based on different query parameters and visual configurations.", + "keywords": [ "posts", "list", "blog", "blogs", "custom post types" ], "textdomain": "default", "attributes": { "queryId": { diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index d10db26529854..043f351e11d7f 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -24,27 +24,7 @@ function render_block_core_query( $attributes, $content, $block ) { // Enqueue the script module and add the necessary directives if the block is // interactive. if ( $is_interactive ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/query/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/query', - isset( $module_url ) ? $module_url : includes_url( "blocks/query/view{$suffix}.js" ), - array( - array( - 'id' => '@wordpress/interactivity', - 'import' => 'static', - ), - array( - 'id' => '@wordpress/interactivity-router', - 'import' => 'dynamic', - ), - ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/query' ); + wp_enqueue_script_module( '@wordpress/block-library/query/view' ); $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag() ) { diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index fb09cdd36406e..e4259bb0ce2c7 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -80,18 +80,7 @@ function render_block_core_search( $attributes ) { // If it's interactive, enqueue the script module and add the directives. $is_expandable_searchfield = 'button-only' === $button_position; if ( $is_expandable_searchfield ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/search/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/search', - isset( $module_url ) ? $module_url : includes_url( "blocks/search/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/search' ); + wp_enqueue_script_module( '@wordpress/block-library/search/view' ); $input->set_attribute( 'data-wp-bind--aria-hidden', '!context.isSearchInputVisible' ); $input->set_attribute( 'data-wp-bind--tabindex', 'state.tabindex' ); diff --git a/packages/core-commands/src/admin-navigation-commands.js b/packages/core-commands/src/admin-navigation-commands.js index be5e19386b65b..c0d8bb084b46a 100644 --- a/packages/core-commands/src/admin-navigation-commands.js +++ b/packages/core-commands/src/admin-navigation-commands.js @@ -6,8 +6,8 @@ import { __ } from '@wordpress/i18n'; import { plus } from '@wordpress/icons'; import { getPath } from '@wordpress/url'; import { store as coreStore } from '@wordpress/core-data'; -import { useDispatch } from '@wordpress/data'; -import { useMemo } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useCallback, useMemo } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as routerPrivateApis } from '@wordpress/router'; @@ -23,46 +23,55 @@ function useAddNewPageCommand() { 'site-editor.php' ); const history = useHistory(); + const isBlockBasedTheme = useSelect( ( select ) => { + return select( coreStore ).getCurrentTheme()?.is_block_theme; + }, [] ); const { saveEntityRecord } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const createPageEntity = async ( { close } ) => { - try { - const page = await saveEntityRecord( - 'postType', - 'page', - { - status: 'draft', - }, - { - throwOnError: true, + const createPageEntity = useCallback( + async ( { close } ) => { + try { + const page = await saveEntityRecord( + 'postType', + 'page', + { + status: 'draft', + }, + { + throwOnError: true, + } + ); + if ( page?.id ) { + history.push( { + postId: page.id, + postType: 'page', + canvas: 'edit', + } ); } - ); - if ( page?.id ) { - history.push( { - postId: page.id, - postType: 'page', - canvas: 'edit', + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while creating the item.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', } ); + } finally { + close(); } - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while creating the item.' ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } finally { - close(); - } - }; + }, + [ createErrorNotice, history, saveEntityRecord ] + ); const commands = useMemo( () => { - const addNewPage = isSiteEditor - ? createPageEntity - : () => ( document.location.href = 'post-new.php?post_type=page' ); + const addNewPage = + isSiteEditor && isBlockBasedTheme + ? createPageEntity + : () => + ( document.location.href = + 'post-new.php?post_type=page' ); return [ { name: 'core/add-new-page', @@ -71,7 +80,7 @@ function useAddNewPageCommand() { callback: addNewPage, }, ]; - }, [ createPageEntity, isSiteEditor ] ); + }, [ createPageEntity, isSiteEditor, isBlockBasedTheme ] ); return { isLoading: false, diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 3ce895680a15c..95b8fc898555c 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -2,6 +2,10 @@ DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.). +DataViews is data agnostic, it can work with data coming from a static (JSON file) or dynamic source (HTTP Request) — it just requires the data to be an array of objects that have an unique identifier. Consumers are responsible to query the data source appropiately based on the DataViews props: + +![DataViews flow](https://developer.wordpress.org/files/2024/09/368600071-20aa078f-7c3d-406d-8dd0-8b764addd22a.png "DataViews flow") + ## Installation Install the module diff --git a/packages/dataviews/src/components/dataviews/style.scss b/packages/dataviews/src/components/dataviews/style.scss index 8909c7cf1c7cf..aa8fbcfb009c0 100644 --- a/packages/dataviews/src/components/dataviews/style.scss +++ b/packages/dataviews/src/components/dataviews/style.scss @@ -80,7 +80,6 @@ padding: $grid-unit-15 $grid-unit-30; } - .dataviews-view-grid, .dataviews-no-results, .dataviews-loading { padding-left: $grid-unit-30; diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index 5fab362b0b47b..6286ed94685a0 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -163,3 +163,11 @@ .dataviews-view-grid__card.is-selected .dataviews-selection-checkbox { top: $grid-unit-10; } + +/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ +@container (max-width: 430px) { + .dataviews-view-grid { + padding-left: $grid-unit-30; + padding-right: $grid-unit-30; + } +} diff --git a/packages/editor/src/components/document-tools/index.js b/packages/editor/src/components/document-tools/index.js index 54121652bbf13..6a8c20c8d7055 100644 --- a/packages/editor/src/components/document-tools/index.js +++ b/packages/editor/src/components/document-tools/index.js @@ -38,10 +38,8 @@ function DocumentTools( { className, disableBlockTools = false } ) { listViewShortcut, inserterSidebarToggleRef, listViewToggleRef, - hasFixedToolbar, showIconLabels, } = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); const { get } = select( preferencesStore ); const { isListViewOpened, @@ -60,7 +58,6 @@ function DocumentTools( { className, disableBlockTools = false } ) { ), inserterSidebarToggleRef: getInserterSidebarToggleRef(), listViewToggleRef: getListViewToggleRef(), - hasFixedToolbar: getSettings().hasFixedToolbar, showIconLabels: get( 'core', 'showIconLabels' ), isDistractionFree: get( 'core', 'distractionFree' ), isVisualMode: getEditorMode() === 'visual', @@ -137,7 +134,7 @@ function DocumentTools( { className, disableBlockTools = false } ) { ) } { ( isWideViewport || ! showIconLabels ) && ( <> - { isLargeViewport && ! hasFixedToolbar && ( + { isLargeViewport && ( { const { get } = select( preferencesStore ); const { getEditorSettings, getPostTypeLabel } = select( editorStore ); const editorSettings = getEditorSettings(); const postTypeLabel = getPostTypeLabel(); + const { isZoomOut: _isZoomOut } = unlock( select( blockEditorStore ) ); return { mode: select( editorStore ).getEditorMode(), @@ -94,8 +97,7 @@ export default function EditorInterface( { showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), // translators: Default label for the Document in the Block Breadcrumb. documentLabel: postTypeLabel || _x( 'Document', 'noun' ), - blockEditorMode: - select( blockEditorStore ).__unstableGetEditorMode(), + isZoomOut: _isZoomOut(), }; }, [] ); const isLargeViewport = useViewportMatch( 'medium' ); @@ -206,7 +208,7 @@ export default function EditorInterface( { isLargeViewport && showBlockBreadcrumbs && isRichEditingEnabled && - blockEditorMode !== 'zoom-out' && + ! isZoomOut && mode === 'visual' && ( ) diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index 2ff115272d614..88d2dac8ffd77 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -174,17 +174,19 @@ function VisualEditor( { hasRootPaddingAwareAlignments, themeHasDisabledLayoutStyles, themeSupportsLayout, - isZoomOutMode, + isZoomedOut, } = useSelect( ( select ) => { - const { getSettings, __unstableGetEditorMode } = - select( blockEditorStore ); + const { getSettings, isZoomOut: _isZoomOut } = unlock( + select( blockEditorStore ) + ); + const _settings = getSettings(); return { themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, themeSupportsLayout: _settings.supportsLayout, hasRootPaddingAwareAlignments: _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + isZoomedOut: _isZoomOut(), }; }, [] ); @@ -336,7 +338,7 @@ function VisualEditor( { ] ); const zoomOutProps = - isZoomOutMode && ! isTabletViewport + isZoomedOut && ! isTabletViewport ? { scale: 'default', frameSize: '48px', @@ -355,7 +357,7 @@ function VisualEditor( { // Disable resizing in mobile viewport. ! isMobileViewport && // Dsiable resizing in zoomed-out mode. - ! isZoomOutMode; + ! isZoomedOut; const shouldIframe = ! disableIframe || [ 'Tablet', 'Mobile' ].includes( deviceType ); diff --git a/packages/editor/src/components/zoom-out-toggle/index.js b/packages/editor/src/components/zoom-out-toggle/index.js index e8c7b1e50510a..214b1c51fd625 100644 --- a/packages/editor/src/components/zoom-out-toggle/index.js +++ b/packages/editor/src/components/zoom-out-toggle/index.js @@ -8,16 +8,27 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { square as zoomOutIcon } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + const ZoomOutToggle = () => { - const { isZoomOutMode } = useSelect( ( select ) => ( { - isZoomOutMode: - select( blockEditorStore ).__unstableGetEditorMode() === 'zoom-out', + const { isZoomOut } = useSelect( ( select ) => ( { + isZoomOut: unlock( select( blockEditorStore ) ).isZoomOut(), } ) ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { resetZoomLevel, setZoomLevel, __unstableSetEditorMode } = unlock( + useDispatch( blockEditorStore ) + ); const handleZoomOut = () => { - __unstableSetEditorMode( isZoomOutMode ? 'edit' : 'zoom-out' ); + if ( isZoomOut ) { + resetZoomLevel(); + } else { + setZoomLevel( 50 ); + } + __unstableSetEditorMode( isZoomOut ? 'edit' : 'zoom-out' ); }; return ( @@ -25,7 +36,7 @@ const ZoomOutToggle = () => { onClick={ handleZoomOut } icon={ zoomOutIcon } label={ __( 'Toggle Zoom Out' ) } - isPressed={ isZoomOutMode } + isPressed={ isZoomOut } size="compact" /> ); diff --git a/phpunit/blocks/render-post-template-test.php b/phpunit/blocks/render-post-template-test.php index 6241f6f060516..e929e459654fe 100644 --- a/phpunit/blocks/render-post-template-test.php +++ b/phpunit/blocks/render-post-template-test.php @@ -122,7 +122,7 @@ public function test_rendering_post_template_with_main_query_loop_already_starte global $wp_query, $wp_the_query; // Query block with post template block. - $content = ''; + $content = ''; $content .= ''; $content .= ''; $content .= ''; diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index c556c469698eb..90a5d2b1da9f1 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -66,7 +66,7 @@ test.describe( 'Block bindings', () => { ); } ); - test( 'should show the key of the custom field in server sources with key', async ( { + test( 'should always show the source label in server-only sources', async ( { editor, } ) => { await editor.insertBlock( { @@ -86,30 +86,6 @@ test.describe( 'Block bindings', () => { const paragraphBlock = editor.canvas.getByRole( 'document', { name: 'Block: Paragraph', } ); - await expect( paragraphBlock ).toHaveText( - 'text_custom_field' - ); - } ); - - test( 'should show the source label in server sources without key', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/server-source', - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); await expect( paragraphBlock ).toHaveText( 'Server Source' ); } ); diff --git a/tools/webpack/script-modules.js b/tools/webpack/script-modules.js index 18287c96d83c8..021f11f5f5ed9 100644 --- a/tools/webpack/script-modules.js +++ b/tools/webpack/script-modules.js @@ -89,11 +89,11 @@ module.exports = { }, output: { devtoolNamespace: 'wp', - filename: './build-module/[name].min.js', + filename: '[name].min.js', library: { type: 'module', }, - path: join( __dirname, '..', '..' ), + path: join( __dirname, '..', '..', 'build-module' ), environment: { module: true }, module: true, chunkFormat: 'module', @@ -102,7 +102,13 @@ module.exports = { resolve: { extensions: [ '.js', '.ts', '.tsx' ], }, - plugins: [ ...plugins, new DependencyExtractionWebpackPlugin() ], + plugins: [ + ...plugins, + new DependencyExtractionWebpackPlugin( { + combineAssets: true, + combinedOutputFile: `./assets.php`, + } ), + ], watchOptions: { ignored: [ '**/node_modules' ], aggregateTimeout: 500,