From 726d86a201f0ee19d99a62fd9f4558aa6aa72431 Mon Sep 17 00:00:00 2001 From: Benni <109149472+benniledl@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:37:22 +0100 Subject: [PATCH 001/444] Add WordPress 6.7.1 version to "Versions in WordPress" documentation (#67298) Co-authored-by: benniledl Co-authored-by: fabiankaegy --- docs/contributors/versions-in-wordpress.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index 4ba7b34da1555..c2be7e3fa4e88 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 18.6-19.3 | 6.7.1 | | 18.6-19.3 | 6.7 | | 17.8-18.5 | 6.6.2 | | 17.8-18.5 | 6.6.1 | From f7a4f3f054e99e204b76393293fc4775b9596313 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Tue, 26 Nov 2024 09:58:08 +0100 Subject: [PATCH 002/444] Move default template types and template part areas to REST API (#66459) * move default template types and template part areas to REST API * fix logic * fix E2E test * move default template types and template part areas to REST API * fix error * remove not necessary file * fix naming * remove duplicate code * remove duplicated code * improve logic * fix naming * fix unit test * update doc * add unit test for getTemplateInfo function * restore not necessary changes * fix e2e test * remove not necessary variable * replace add_action with add_filter * improve readibility code * make getTemplateInfo private * make templateAreas optional * add default_template_part_areas and default_template_types * move code to rest-api.php file * remove not used import --- lib/compat/wordpress-6.8/preload.php | 2 + lib/compat/wordpress-6.8/rest-api.php | 38 +++ .../use-template-part-area-label.js | 20 +- .../template-part/edit/advanced-controls.js | 26 +- .../edit/utils/get-template-part-icon.js | 20 ++ .../src/template-part/edit/utils/hooks.js | 9 +- .../src/template-part/variations.js | 20 +- packages/core-data/src/entities.js | 2 + .../src/components/add-new-template/utils.js | 4 +- .../src/components/editor/use-editor-title.js | 21 +- .../src/components/page-patterns/header.js | 5 +- .../components/page-patterns/use-patterns.js | 12 +- .../use-template-part-areas.js | 8 +- packages/edit-site/src/index.js | 14 +- .../create-template-part-modal/index.js | 5 +- .../src/components/document-bar/index.js | 13 +- .../entity-record-item.js | 16 +- .../src/components/post-card-panel/index.js | 23 +- packages/editor/src/private-apis.js | 3 +- .../editor/src/store/private-selectors.js | 16 +- packages/editor/src/store/selectors.js | 142 +++++----- packages/editor/src/store/test/selectors.js | 255 ------------------ .../editor/src/utils/get-template-info.js | 52 ++++ .../src/utils/test/get-template-info.js | 224 +++++++++++++++ 24 files changed, 535 insertions(+), 415 deletions(-) create mode 100644 packages/block-library/src/template-part/edit/utils/get-template-part-icon.js create mode 100644 packages/editor/src/utils/get-template-info.js create mode 100644 packages/editor/src/utils/test/get-template-info.js diff --git a/lib/compat/wordpress-6.8/preload.php b/lib/compat/wordpress-6.8/preload.php index aabe0d4fb574c..ae6c738c6627c 100644 --- a/lib/compat/wordpress-6.8/preload.php +++ b/lib/compat/wordpress-6.8/preload.php @@ -32,6 +32,8 @@ function gutenberg_block_editor_preload_paths_6_8( $paths, $context ) { 'site_logo', 'timezone_string', 'url', + 'default_template_part_areas', + 'default_template_types', ) ); $paths[] = '/wp/v2/templates/lookup?slug=front-page'; diff --git a/lib/compat/wordpress-6.8/rest-api.php b/lib/compat/wordpress-6.8/rest-api.php index 080e4003f57b3..b94e42d5f2ccd 100644 --- a/lib/compat/wordpress-6.8/rest-api.php +++ b/lib/compat/wordpress-6.8/rest-api.php @@ -49,3 +49,41 @@ function ( $taxonomy ) { remove_filter( "rest_{$taxonomy}_query", 'gutenberg_respect_taxonomy_default_args_in_rest_api' ); } ); + +/** + * Adds the default template part areas to the REST API index. + * + * This function exposes the default template part areas through the WordPress REST API. + * Note: This function backports into the wp-includes/rest-api/class-wp-rest-server.php file. + * + * @param WP_REST_Response $response REST API response. + * @return WP_REST_Response Modified REST API response with default template part areas. + */ +function gutenberg_add_default_template_part_areas_to_index( WP_REST_Response $response ) { + $response->data['default_template_part_areas'] = get_allowed_block_template_part_areas(); + return $response; +} + +add_filter( 'rest_index', 'gutenberg_add_default_template_part_areas_to_index' ); + +/** + * Adds the default template types to the REST API index. + * + * This function exposes the default template types through the WordPress REST API. + * Note: This function backports into the wp-includes/rest-api/class-wp-rest-server.php file. + * + * @param WP_REST_Response $response REST API response. + * @return WP_REST_Response Modified REST API response with default template part areas. + */ +function gutenberg_add_default_template_types_to_index( WP_REST_Response $response ) { + $indexed_template_types = array(); + foreach ( get_default_block_template_types() as $slug => $template_type ) { + $template_type['slug'] = (string) $slug; + $indexed_template_types[] = $template_type; + } + + $response->data['default_template_types'] = $indexed_template_types; + return $response; +} + +add_filter( 'rest_index', 'gutenberg_add_default_template_types_to_index' ); diff --git a/packages/block-library/src/navigation/use-template-part-area-label.js b/packages/block-library/src/navigation/use-template-part-area-label.js index 48763a38ac62d..7b4d514975e11 100644 --- a/packages/block-library/src/navigation/use-template-part-area-label.js +++ b/packages/block-library/src/navigation/use-template-part-area-label.js @@ -11,6 +11,7 @@ import { useSelect } from '@wordpress/data'; // TODO: this util should perhaps be refactored somewhere like core-data. import { createTemplatePartId } from '../template-part/edit/utils/create-template-part-id'; +import { getTemplatePartIcon } from '../template-part/edit/utils/get-template-part-icon'; export default function useTemplatePartAreaLabel( clientId ) { return useSelect( @@ -35,16 +36,15 @@ export default function useTemplatePartAreaLabel( clientId ) { return; } - // FIXME: @wordpress/block-library should not depend on @wordpress/editor. - // Blocks can be loaded into a *non-post* block editor. - // This code is lifted from this file: - // packages/block-library/src/template-part/edit/advanced-controls.js - /* eslint-disable @wordpress/data-no-store-string-literals */ - const definedAreas = - select( - 'core/editor' - ).__experimentalGetDefaultTemplatePartAreas(); - /* eslint-enable @wordpress/data-no-store-string-literals */ + const defaultTemplatePartAreas = + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || []; + + const definedAreas = defaultTemplatePartAreas.map( ( item ) => ( { + ...item, + icon: getTemplatePartIcon( item.icon ), + } ) ); + const { getCurrentTheme, getEditedEntityRecord } = select( coreStore ); diff --git a/packages/block-library/src/template-part/edit/advanced-controls.js b/packages/block-library/src/template-part/edit/advanced-controls.js index 3c319a7ec0fe7..04c6d3387346a 100644 --- a/packages/block-library/src/template-part/edit/advanced-controls.js +++ b/packages/block-library/src/template-part/edit/advanced-controls.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useEntityProp } from '@wordpress/core-data'; +import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import { SelectControl, TextControl } from '@wordpress/components'; import { sprintf, __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; @@ -54,19 +54,19 @@ export function TemplatePartAdvancedControls( { templatePartId ); - const definedAreas = useSelect( ( select ) => { - // FIXME: @wordpress/block-library should not depend on @wordpress/editor. - // Blocks can be loaded into a *non-post* block editor. - /* eslint-disable-next-line @wordpress/data-no-store-string-literals */ - return select( - 'core/editor' - ).__experimentalGetDefaultTemplatePartAreas(); - }, [] ); + const defaultTemplatePartAreas = useSelect( + ( select ) => + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || [], + [] + ); - const areaOptions = definedAreas.map( ( { label, area: _area } ) => ( { - label, - value: _area, - } ) ); + const areaOptions = defaultTemplatePartAreas.map( + ( { label, area: _area } ) => ( { + label, + value: _area, + } ) + ); return ( <> diff --git a/packages/block-library/src/template-part/edit/utils/get-template-part-icon.js b/packages/block-library/src/template-part/edit/utils/get-template-part-icon.js new file mode 100644 index 0000000000000..bb13a8840c945 --- /dev/null +++ b/packages/block-library/src/template-part/edit/utils/get-template-part-icon.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { + header as headerIcon, + footer as footerIcon, + sidebar as sidebarIcon, + symbolFilled as symbolFilledIcon, +} from '@wordpress/icons'; + +export const getTemplatePartIcon = ( iconName ) => { + if ( 'header' === iconName ) { + return headerIcon; + } else if ( 'footer' === iconName ) { + return footerIcon; + } else if ( 'sidebar' === iconName ) { + return sidebarIcon; + } + return symbolFilledIcon; +}; diff --git a/packages/block-library/src/template-part/edit/utils/hooks.js b/packages/block-library/src/template-part/edit/utils/hooks.js index 39daa4080c816..c71327db0290c 100644 --- a/packages/block-library/src/template-part/edit/utils/hooks.js +++ b/packages/block-library/src/template-part/edit/utils/hooks.js @@ -136,14 +136,9 @@ export function useCreateTemplatePartFromBlocks( area, setAttributes ) { export function useTemplatePartArea( area ) { return useSelect( ( select ) => { - // FIXME: @wordpress/block-library should not depend on @wordpress/editor. - // Blocks can be loaded into a *non-post* block editor. - /* eslint-disable @wordpress/data-no-store-string-literals */ const definedAreas = - select( - 'core/editor' - ).__experimentalGetDefaultTemplatePartAreas(); - /* eslint-enable @wordpress/data-no-store-string-literals */ + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || []; const selectedArea = definedAreas.find( ( definedArea ) => definedArea.area === area diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index acd8af13508ba..6f62057bec33d 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -3,23 +3,11 @@ */ import { store as coreDataStore } from '@wordpress/core-data'; import { select } from '@wordpress/data'; -import { - header as headerIcon, - footer as footerIcon, - sidebar as sidebarIcon, - symbolFilled as symbolFilledIcon, -} from '@wordpress/icons'; -function getTemplatePartIcon( iconName ) { - if ( 'header' === iconName ) { - return headerIcon; - } else if ( 'footer' === iconName ) { - return footerIcon; - } else if ( 'sidebar' === iconName ) { - return sidebarIcon; - } - return symbolFilledIcon; -} +/** + * Internal dependencies + */ +import { getTemplatePartIcon } from './edit/utils/get-template-part-icon'; export function enhanceTemplatePartVariations( settings, name ) { if ( name !== 'core/template-part' ) { diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 3c10676750a2a..2730cdf3d64bf 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -31,6 +31,8 @@ export const rootEntitiesConfig = [ 'site_icon_url', 'site_logo', 'timezone_string', + 'default_template_part_areas', + 'default_template_types', 'url', ].join( ',' ), }, diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index e3e2faf945792..759f3f478cada 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -3,7 +3,6 @@ */ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; import { decodeEntities } from '@wordpress/html-entities'; import { useMemo, useCallback } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; @@ -69,7 +68,8 @@ export const useExistingTemplates = () => { export const useDefaultTemplateTypes = () => { return useSelect( ( select ) => - select( editorStore ).__experimentalGetDefaultTemplateTypes(), + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_types || [], [] ); }; diff --git a/packages/edit-site/src/components/editor/use-editor-title.js b/packages/edit-site/src/components/editor/use-editor-title.js index 2ad4e94ccc3e8..727b190117e02 100644 --- a/packages/edit-site/src/components/editor/use-editor-title.js +++ b/packages/edit-site/src/components/editor/use-editor-title.js @@ -4,33 +4,46 @@ import { _x, sprintf } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; import { decodeEntities } from '@wordpress/html-entities'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ import useTitle from '../routes/use-title'; import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../utils/constants'; +import { unlock } from '../../lock-unlock'; + +const { getTemplateInfo } = unlock( editorPrivateApis ); function useEditorTitle( postType, postId ) { const { title, isLoaded } = useSelect( ( select ) => { const { getEditedEntityRecord, hasFinishedResolution } = select( coreStore ); - const { __experimentalGetTemplateInfo: getTemplateInfo } = - select( editorStore ); + const _record = getEditedEntityRecord( 'postType', postType, postId ); + + const { default_template_types: templateTypes = [] } = + select( coreStore ).getEntityRecord( + 'root', + '__unstableBase' + ) ?? {}; + + const templateInfo = getTemplateInfo( { + template: _record, + templateTypes, + } ); + const _isLoaded = hasFinishedResolution( 'getEditedEntityRecord', [ 'postType', postType, postId, ] ); - const templateInfo = getTemplateInfo( _record ); return { title: templateInfo.title, diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js index 641dc577cb7fe..0d3763aec62c1 100644 --- a/packages/edit-site/src/components/page-patterns/header.js +++ b/packages/edit-site/src/components/page-patterns/header.js @@ -9,7 +9,7 @@ import { __experimentalText as Text, __experimentalVStack as VStack, } from '@wordpress/components'; -import { store as editorStore } from '@wordpress/editor'; +import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import { moreVertical } from '@wordpress/icons'; @@ -32,7 +32,8 @@ export default function PatternsHeader( { const { patternCategories } = usePatternCategories(); const templatePartAreas = useSelect( ( select ) => - select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || [], [] ); diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index e226298857c4d..3b3e33d5650e6 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -4,7 +4,6 @@ import { parse } from '@wordpress/blocks'; import { useSelect, createSelector } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; import { useMemo } from '@wordpress/element'; /** @@ -28,8 +27,7 @@ const selectTemplateParts = createSelector( ( select, categoryId, search = '' ) => { const { getEntityRecords, isResolving: isResolvingSelector } = select( coreStore ); - const { __experimentalGetDefaultTemplatePartAreas } = - select( editorStore ); + const query = { per_page: -1 }; const templateParts = getEntityRecords( 'postType', TEMPLATE_PART_POST_TYPE, query ) ?? @@ -38,7 +36,10 @@ const selectTemplateParts = createSelector( // In the case where a custom template part area has been removed we need // the current list of areas to cross check against so orphaned template // parts can be treated as uncategorized. - const knownAreas = __experimentalGetDefaultTemplatePartAreas() || []; + const knownAreas = + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || []; + const templatePartAreas = knownAreas.map( ( area ) => area.area ); const templatePartHasCategory = ( item, category ) => { @@ -78,7 +79,8 @@ const selectTemplateParts = createSelector( TEMPLATE_PART_POST_TYPE, { per_page: -1 }, ] ), - select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas, ] ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js index 77cbf87b3d439..6a67a8f8a30fb 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js @@ -1,9 +1,8 @@ /** * WordPress dependencies */ -import { useEntityRecords } from '@wordpress/core-data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -18,7 +17,8 @@ const useTemplatePartsGroupedByArea = ( items ) => { const templatePartAreas = useSelect( ( select ) => - select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || [], [] ); @@ -43,7 +43,7 @@ const useTemplatePartsGroupedByArea = ( items ) => { const key = accumulator[ item.area ] ? item.area : TEMPLATE_PART_AREA_DEFAULT_CATEGORY; - accumulator[ key ].templateParts.push( item ); + accumulator[ key ]?.templateParts?.push( item ); return accumulator; }, knownAreas ); diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 7f124e6b5f7ac..b96ce5cb67f5e 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -10,10 +10,7 @@ import { import { dispatch } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { createRoot, StrictMode } from '@wordpress/element'; -import { - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { store as preferencesStore } from '@wordpress/preferences'; import { registerLegacyWidgetBlock, @@ -88,15 +85,6 @@ export function initializeEditor( id, settings ) { dispatch( editSiteStore ).updateSettings( settings ); - // Keep the defaultTemplateTypes in the core/editor settings too, - // so that they can be selected with core/editor selectors in any editor. - // This is needed because edit-site doesn't initialize with EditorProvider, - // which internally uses updateEditorSettings as well. - dispatch( editorStore ).updateEditorSettings( { - defaultTemplateTypes: settings.defaultTemplateTypes, - defaultTemplatePartAreas: settings.defaultTemplatePartAreas, - } ); - // Prevent the default browser action for files dropped outside of dropzones. window.addEventListener( 'dragover', ( e ) => e.preventDefault(), false ); window.addEventListener( 'drop', ( e ) => e.preventDefault(), false ); diff --git a/packages/editor/src/components/create-template-part-modal/index.js b/packages/editor/src/components/create-template-part-modal/index.js index 5d594cd646cc1..660439ded2300 100644 --- a/packages/editor/src/components/create-template-part-modal/index.js +++ b/packages/editor/src/components/create-template-part-modal/index.js @@ -27,7 +27,6 @@ import { serialize } from '@wordpress/blocks'; /** * Internal dependencies */ -import { store as editorStore } from '../../store'; import { TEMPLATE_PART_POST_TYPE, TEMPLATE_PART_AREA_DEFAULT_CATEGORY, @@ -81,9 +80,11 @@ export function CreateTemplatePartModalContents( { const templatePartAreas = useSelect( ( select ) => - select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || [], [] ); + async function createTemplatePart() { if ( ! title || isSubmitting ) { return; diff --git a/packages/editor/src/components/document-bar/index.js b/packages/editor/src/components/document-bar/index.js index 9fffba941a435..7b94a6fbeb3be 100644 --- a/packages/editor/src/components/document-bar/index.js +++ b/packages/editor/src/components/document-bar/index.js @@ -29,6 +29,7 @@ import { decodeEntities } from '@wordpress/html-entities'; import { TEMPLATE_POST_TYPES } from '../../store/constants'; import { store as editorStore } from '../../store'; import usePageTypeBadge from '../../utils/pageTypeBadge'; +import { getTemplateInfo } from '../../utils/get-template-info'; /** @typedef {import("@wordpress/components").IconType} IconType */ @@ -65,9 +66,9 @@ export default function DocumentBar( props ) { getCurrentPostType, getCurrentPostId, getEditorSettings, - __experimentalGetTemplateInfo: getTemplateInfo, getRenderingMode, } = select( editorStore ); + const { getEditedEntityRecord, getPostType, @@ -80,7 +81,15 @@ export default function DocumentBar( props ) { _postType, _postId ); - const _templateInfo = getTemplateInfo( _document ); + + const { default_template_types: templateTypes = [] } = + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) ?? + {}; + + const _templateInfo = getTemplateInfo( { + templateTypes, + template: _document, + } ); const _postTypeLabel = getPostType( _postType )?.labels?.singular_name; return { diff --git a/packages/editor/src/components/entities-saved-states/entity-record-item.js b/packages/editor/src/components/entities-saved-states/entity-record-item.js index ca9fb2e0b169c..e8219c4cca7ae 100644 --- a/packages/editor/src/components/entities-saved-states/entity-record-item.js +++ b/packages/editor/src/components/entities-saved-states/entity-record-item.js @@ -12,6 +12,7 @@ import { decodeEntities } from '@wordpress/html-entities'; */ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import { getTemplateInfo } from '../../utils/get-template-info'; export default function EntityRecordItem( { record, checked, onChange } ) { const { name, kind, title, key } = record; @@ -33,11 +34,18 @@ export default function EntityRecordItem( { record, checked, onChange } ) { name, key ); + + const { default_template_types: templateTypes = [] } = + select( coreStore ).getEntityRecord( + 'root', + '__unstableBase' + ) ?? {}; + return { - entityRecordTitle: - select( editorStore ).__experimentalGetTemplateInfo( - template - ).title, + entityRecordTitle: getTemplateInfo( { + template, + templateTypes, + } ).title, hasPostMetaChanges: unlock( select( editorStore ) ).hasPostMetaChanges( name, key ), diff --git a/packages/editor/src/components/post-card-panel/index.js b/packages/editor/src/components/post-card-panel/index.js index 410b8cfd4447d..8fcca6c6bd6d4 100644 --- a/packages/editor/src/components/post-card-panel/index.js +++ b/packages/editor/src/components/post-card-panel/index.js @@ -22,6 +22,7 @@ import { import { unlock } from '../../lock-unlock'; import PostActions from '../post-actions'; import usePageTypeBadge from '../../utils/pageTypeBadge'; +import { getTemplateInfo } from '../../utils/get-template-info'; export default function PostCardPanel( { postType, @@ -30,17 +31,29 @@ export default function PostCardPanel( { } ) { const { title, icon } = useSelect( ( select ) => { - const { __experimentalGetTemplateInfo } = select( editorStore ); const { getEditedEntityRecord } = select( coreStore ); const _record = getEditedEntityRecord( 'postType', postType, postId ); - const _templateInfo = - [ TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE ].includes( - postType - ) && __experimentalGetTemplateInfo( _record ); + + const { default_template_types: templateTypes = [] } = + select( coreStore ).getEntityRecord( + 'root', + '__unstableBase' + ) ?? {}; + + const _templateInfo = [ + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + ].includes( postType ) + ? getTemplateInfo( { + template: _record, + templateTypes, + } ) + : {}; + return { title: _templateInfo?.title || _record?.title, icon: unlock( select( editorStore ) ).getPostIcon( postType, { diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index b49b2a69a3bf2..2699858b3164f 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -25,6 +25,7 @@ import { GlobalStylesProvider, } from './components/global-styles-provider'; import { registerCoreBlockBindingsSources } from './bindings/api'; +import { getTemplateInfo } from './utils/get-template-info'; const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis; @@ -46,7 +47,7 @@ lock( privateApis, { ViewMoreMenuGroup, ResizableEditor, registerCoreBlockBindingsSources, - + getTemplateInfo, // This is a temporary private API while we're updating the site editor to use EditorProvider. interfaceStore, ...remainingInterfaceApis, diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index 9af0512c19dbd..56fa672837148 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -20,11 +20,7 @@ import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ -import { - getRenderingMode, - getCurrentPost, - __experimentalGetDefaultTemplatePartAreas, -} from './selectors'; +import { getRenderingMode, getCurrentPost } from './selectors'; import { getEntityActions as _getEntityActions, getEntityFields as _getEntityFields, @@ -102,9 +98,13 @@ export const getPostIcon = createRegistrySelector( postType === 'wp_template' ) { return ( - __experimentalGetDefaultTemplatePartAreas( state ).find( - ( item ) => options.area === item.area - )?.icon || layout + ( + select( coreStore ).getEntityRecord( + 'root', + '__unstableBase' + )?.default_template_part_areas || [] + ).find( ( item ) => options.area === item.area )?.icon || + layout ); } if ( CARD_ICONS[ postType ] ) { diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index db452be5c17ab..c17e80f771ea3 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -12,7 +12,6 @@ import { addQueryArgs, cleanForSlug } from '@wordpress/url'; import { createSelector, createRegistrySelector } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { Platform } from '@wordpress/element'; -import { layout } from '@wordpress/icons'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -29,6 +28,7 @@ import { import { getPostRawValue } from './reducer'; import { getTemplatePartIcon } from '../utils/get-template-part-icon'; import { unlock } from '../lock-unlock'; +import { getTemplateInfo } from '../utils/get-template-info'; /** * Shared reference to an empty object for cases where it is important to avoid @@ -1702,16 +1702,20 @@ export const getBlockListSettings = getBlockEditorSelector( 'getBlockListSettings' ); -/** - * Returns the default template types. - * - * @param {Object} state Global application state. - * - * @return {Object} The template types. - */ -export function __experimentalGetDefaultTemplateTypes( state ) { - return getEditorSettings( state )?.defaultTemplateTypes; -} +export const __experimentalGetDefaultTemplateTypes = createRegistrySelector( + ( select ) => () => { + deprecated( + "select('core/editor').__experimentalGetDefaultTemplateTypes", + { + since: '6.7', + alternative: + "select('core/core-data').getEntityRecord( 'root', '__unstableBase' )?.default_template_types", + } + ); + return select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_types; + } +); /** * Returns the default template part areas. @@ -1720,15 +1724,26 @@ export function __experimentalGetDefaultTemplateTypes( state ) { * * @return {Array} The template part areas. */ -export const __experimentalGetDefaultTemplatePartAreas = createSelector( - ( state ) => { - const areas = - getEditorSettings( state )?.defaultTemplatePartAreas ?? []; - return areas.map( ( item ) => { - return { ...item, icon: getTemplatePartIcon( item.icon ) }; - } ); - }, - ( state ) => [ getEditorSettings( state )?.defaultTemplatePartAreas ] +export const __experimentalGetDefaultTemplatePartAreas = createRegistrySelector( + ( select ) => + createSelector( () => { + deprecated( + "select('core/editor').__experimentalGetDefaultTemplatePartAreas", + { + since: '6.7', + alternative: + "select('core/core-data').getEntityRecord( 'root', '__unstableBase' )?.default_template_part_areas", + } + ); + + const areas = + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || []; + + return areas.map( ( item ) => { + return { ...item, icon: getTemplatePartIcon( item.icon ) }; + } ); + } ) ); /** @@ -1739,20 +1754,30 @@ export const __experimentalGetDefaultTemplatePartAreas = createSelector( * * @return {Object} The template type. */ -export const __experimentalGetDefaultTemplateType = createSelector( - ( state, slug ) => { - const templateTypes = __experimentalGetDefaultTemplateTypes( state ); - if ( ! templateTypes ) { - return EMPTY_OBJECT; - } +export const __experimentalGetDefaultTemplateType = createRegistrySelector( + ( select ) => + createSelector( ( state, slug ) => { + deprecated( + "select('core/editor').__experimentalGetDefaultTemplateType", + { + since: '6.7', + } + ); + const templateTypes = select( coreStore ).getEntityRecord( + 'root', + '__unstableBase' + )?.default_template_types; + + if ( ! templateTypes ) { + return EMPTY_OBJECT; + } - return ( - Object.values( templateTypes ).find( - ( type ) => type.slug === slug - ) ?? EMPTY_OBJECT - ); - }, - ( state ) => [ __experimentalGetDefaultTemplateTypes( state ) ] + return ( + Object.values( templateTypes ).find( + ( type ) => type.slug === slug + ) ?? EMPTY_OBJECT + ); + } ) ); /** @@ -1763,38 +1788,31 @@ export const __experimentalGetDefaultTemplateType = createSelector( * @param {Object} template The template for which we need information. * @return {Object} Information about the template, including title, description, and icon. */ -export const __experimentalGetTemplateInfo = createSelector( - ( state, template ) => { - if ( ! template ) { - return EMPTY_OBJECT; - } +export const __experimentalGetTemplateInfo = createRegistrySelector( + ( select ) => + createSelector( ( state, template ) => { + deprecated( "select('core/editor').__experimentalGetTemplateInfo", { + since: '6.7', + } ); - const { description, slug, title, area } = template; - const { title: defaultTitle, description: defaultDescription } = - __experimentalGetDefaultTemplateType( state, slug ); + if ( ! template ) { + return EMPTY_OBJECT; + } - const templateTitle = - typeof title === 'string' ? title : title?.rendered; - const templateDescription = - typeof description === 'string' ? description : description?.raw; - const templateIcon = - __experimentalGetDefaultTemplatePartAreas( state ).find( - ( item ) => area === item.area - )?.icon || layout; + const templateTypes = + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_types || []; - return { - title: - templateTitle && templateTitle !== slug - ? templateTitle - : defaultTitle || slug, - description: templateDescription || defaultDescription, - icon: templateIcon, - }; - }, - ( state ) => [ - __experimentalGetDefaultTemplateTypes( state ), - __experimentalGetDefaultTemplatePartAreas( state ), - ] + const templateAreas = + select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.default_template_part_areas || []; + + return getTemplateInfo( { + template, + templateAreas, + templateTypes, + } ); + } ) ); /** diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 1de25604ebd7e..2a36fabcd2ec9 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -16,7 +16,6 @@ import { getBlockTypes, } from '@wordpress/blocks'; import { RawHTML } from '@wordpress/element'; -import { layout, footer, header } from '@wordpress/icons'; /** * Internal dependencies @@ -188,43 +187,11 @@ const { isPostAutosavingLocked, canUserUseUnfilteredHTML, getPostTypeLabel, - __experimentalGetDefaultTemplateType, - __experimentalGetDefaultTemplateTypes, - __experimentalGetTemplateInfo, - __experimentalGetDefaultTemplatePartAreas, isEditorPanelRemoved, isInserterOpened, isListViewOpened, } = selectors; -const defaultTemplateTypes = [ - { - title: 'Default (Index)', - description: 'Main template', - slug: 'index', - }, - { - title: '404 (Not Found)', - description: 'Applied when content cannot be found', - slug: '404', - }, -]; - -const defaultTemplatePartAreas = [ - { - area: 'header', - label: 'Header', - description: 'Some description of a header', - icon: 'header', - }, - { - area: 'footer', - label: 'Footer', - description: 'Some description of a footer', - icon: 'footer', - }, -]; - describe( 'selectors', () => { let cachedSelectors; @@ -2766,228 +2733,6 @@ describe( 'selectors', () => { } ); } ); - describe( '__experimentalGetDefaultTemplateTypes', () => { - const state = { editorSettings: { defaultTemplateTypes } }; - - it( 'returns undefined if there are no default template types', () => { - const emptyState = { editorSettings: {} }; - expect( - __experimentalGetDefaultTemplateTypes( emptyState ) - ).toBeUndefined(); - } ); - - it( 'returns a list of default template types if present in state', () => { - expect( - __experimentalGetDefaultTemplateTypes( state ) - ).toHaveLength( 2 ); - } ); - } ); - - describe( '__experimentalGetDefaultTemplatePartAreas', () => { - const state = { editorSettings: { defaultTemplatePartAreas } }; - - it( 'returns empty array if there are no default template part areas', () => { - const emptyState = { editorSettings: {} }; - expect( - __experimentalGetDefaultTemplatePartAreas( emptyState ) - ).toHaveLength( 0 ); - } ); - - it( 'returns a list of default template part areas if present in state', () => { - expect( - __experimentalGetDefaultTemplatePartAreas( state ) - ).toHaveLength( 2 ); - } ); - - it( 'assigns an icon to each area', () => { - const templatePartAreas = - __experimentalGetDefaultTemplatePartAreas( state ); - templatePartAreas.forEach( ( area ) => - expect( area.icon ).not.toBeNull() - ); - } ); - } ); - - describe( '__experimentalGetDefaultTemplateType', () => { - const state = { editorSettings: { defaultTemplateTypes } }; - - it( 'returns an empty object if there are no default template types', () => { - const emptyState = { editorSettings: {} }; - expect( - __experimentalGetDefaultTemplateType( emptyState, 'slug' ) - ).toEqual( {} ); - } ); - - it( 'returns an empty object if the requested slug is not found', () => { - expect( - __experimentalGetDefaultTemplateType( state, 'foobar' ) - ).toEqual( {} ); - } ); - - it( 'returns the requested default template type', () => { - expect( - __experimentalGetDefaultTemplateType( state, 'index' ) - ).toEqual( { - title: 'Default (Index)', - description: 'Main template', - slug: 'index', - } ); - } ); - - it( 'returns the requested default template type even when the slug is numeric', () => { - expect( - __experimentalGetDefaultTemplateType( state, '404' ) - ).toEqual( { - title: '404 (Not Found)', - description: 'Applied when content cannot be found', - slug: '404', - } ); - } ); - } ); - - describe( '__experimentalGetTemplateInfo', () => { - const state = { - editorSettings: { defaultTemplateTypes, defaultTemplatePartAreas }, - }; - - it( 'should return an empty object if no template is passed', () => { - expect( __experimentalGetTemplateInfo( state, null ) ).toEqual( - {} - ); - expect( __experimentalGetTemplateInfo( state, undefined ) ).toEqual( - {} - ); - expect( __experimentalGetTemplateInfo( state, false ) ).toEqual( - {} - ); - } ); - - it( 'should return the default title if none is defined on the template', () => { - expect( - __experimentalGetTemplateInfo( state, { slug: 'index' } ).title - ).toEqual( 'Default (Index)' ); - } ); - - it( 'should return the rendered title if defined on the template', () => { - expect( - __experimentalGetTemplateInfo( state, { - slug: 'index', - title: { rendered: 'test title' }, - } ).title - ).toEqual( 'test title' ); - } ); - - it( 'should return the slug if no title is found', () => { - expect( - __experimentalGetTemplateInfo( state, { - slug: 'not a real template', - } ).title - ).toEqual( 'not a real template' ); - } ); - - it( 'should return the default description if none is defined on the template', () => { - expect( - __experimentalGetTemplateInfo( state, { slug: 'index' } ) - .description - ).toEqual( 'Main template' ); - } ); - - it( 'should return the raw excerpt as description if defined on the template', () => { - expect( - __experimentalGetTemplateInfo( state, { - slug: 'index', - description: { raw: 'test description' }, - } ).description - ).toEqual( 'test description' ); - } ); - - it( 'should return a title, description, and icon', () => { - expect( - __experimentalGetTemplateInfo( state, { slug: 'index' } ) - ).toEqual( { - title: 'Default (Index)', - description: 'Main template', - icon: layout, - } ); - - expect( - __experimentalGetTemplateInfo( state, { - slug: 'index', - title: { rendered: 'test title' }, - } ) - ).toEqual( { - title: 'test title', - description: 'Main template', - icon: layout, - } ); - - expect( - __experimentalGetTemplateInfo( state, { - slug: 'index', - description: { raw: 'test description' }, - } ) - ).toEqual( { - title: 'Default (Index)', - description: 'test description', - icon: layout, - } ); - - expect( - __experimentalGetTemplateInfo( state, { - slug: 'index', - title: { rendered: 'test title' }, - description: { raw: 'test description' }, - } ) - ).toEqual( { - title: 'test title', - description: 'test description', - icon: layout, - } ); - } ); - - it( 'should return correct icon based on area', () => { - expect( - __experimentalGetTemplateInfo( state, { - slug: 'template part, area = uncategorized', - area: 'uncategorized', - } ) - ).toEqual( { - title: 'template part, area = uncategorized', - icon: layout, - } ); - - expect( - __experimentalGetTemplateInfo( state, { - slug: 'template part, area = invalid', - area: 'invalid', - } ) - ).toEqual( { - title: 'template part, area = invalid', - icon: layout, - } ); - - expect( - __experimentalGetTemplateInfo( state, { - slug: 'template part, area = header', - area: 'header', - } ) - ).toEqual( { - title: 'template part, area = header', - icon: header, - } ); - - expect( - __experimentalGetTemplateInfo( state, { - slug: 'template part, area = footer', - area: 'footer', - } ) - ).toEqual( { - title: 'template part, area = footer', - icon: footer, - } ); - } ); - } ); - describe( 'getPostTypeLabel', () => { it( 'should return the correct label for the current post type', () => { const postTypes = [ diff --git a/packages/editor/src/utils/get-template-info.js b/packages/editor/src/utils/get-template-info.js new file mode 100644 index 0000000000000..bc84c06c9399d --- /dev/null +++ b/packages/editor/src/utils/get-template-info.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { layout } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import { getTemplatePartIcon } from './get-template-part-icon'; +const EMPTY_OBJECT = {}; + +/** + * Helper function to retrieve the corresponding template info for a given template. + * @param {Object} params + * @param {Array} params.templateTypes + * @param {Array} [params.templateAreas] + * @param {Object} params.template + */ +export const getTemplateInfo = ( params ) => { + if ( ! params ) { + return EMPTY_OBJECT; + } + + const { templateTypes, templateAreas, template } = params; + + const { description, slug, title, area } = template; + + const { title: defaultTitle, description: defaultDescription } = + Object.values( templateTypes ).find( ( type ) => type.slug === slug ) ?? + EMPTY_OBJECT; + + const templateTitle = typeof title === 'string' ? title : title?.rendered; + const templateDescription = + typeof description === 'string' ? description : description?.raw; + + const templateAreasWithIcon = templateAreas?.map( ( item ) => ( { + ...item, + icon: getTemplatePartIcon( item.icon ), + } ) ); + + const templateIcon = + templateAreasWithIcon?.find( ( item ) => area === item.area )?.icon || + layout; + + return { + title: + templateTitle && templateTitle !== slug + ? templateTitle + : defaultTitle || slug, + description: templateDescription || defaultDescription, + icon: templateIcon, + }; +}; diff --git a/packages/editor/src/utils/test/get-template-info.js b/packages/editor/src/utils/test/get-template-info.js new file mode 100644 index 0000000000000..d0ffe15aa902a --- /dev/null +++ b/packages/editor/src/utils/test/get-template-info.js @@ -0,0 +1,224 @@ +/** + * WordPress dependencies + */ +import { footer, header, layout } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import { getTemplateInfo } from '../get-template-info'; + +describe( '__experimentalGetTemplateInfo', () => { + const defaultTemplateTypes = [ + { + title: 'Default (Index)', + description: 'Main template', + slug: 'index', + }, + { + title: '404 (Not Found)', + description: 'Applied when content cannot be found', + slug: '404', + }, + ]; + + const defaultTemplatePartAreas = [ + { + area: 'header', + label: 'Header', + description: 'Some description of a header', + icon: 'header', + }, + { + area: 'footer', + label: 'Footer', + description: 'Some description of a footer', + icon: 'footer', + }, + ]; + + it( 'should return an empty object if no template is passed', () => { + expect( getTemplateInfo( undefined ) ).toEqual( {} ); + expect( getTemplateInfo( null ) ).toEqual( {} ); + expect( getTemplateInfo( false ) ).toEqual( {} ); + } ); + + it( 'should return the default title if none is defined on the template', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + }, + } ).title + ).toEqual( 'Default (Index)' ); + } ); + + it( 'should return the rendered title if defined on the template', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + title: { rendered: 'test title' }, + }, + } ).title + ).toEqual( 'test title' ); + } ); + + it( 'should return the slug if no title is found', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'not a real template', + }, + } ).title + ).toEqual( 'not a real template' ); + } ); + + it( 'should return the default description if none is defined on the template', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + }, + } ).description + ).toEqual( 'Main template' ); + } ); + + it( 'should return the raw excerpt as description if defined on the template', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + description: { raw: 'test description' }, + }, + } ).description + ).toEqual( 'test description' ); + } ); + + it( 'should return a title, description, and icon', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { slug: 'index' }, + } ) + ).toEqual( { + title: 'Default (Index)', + description: 'Main template', + icon: layout, + } ); + + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + title: { rendered: 'test title' }, + }, + } ) + ).toEqual( { + title: 'test title', + description: 'Main template', + icon: layout, + } ); + + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + description: { raw: 'test description' }, + }, + } ) + ).toEqual( { + title: 'Default (Index)', + description: 'test description', + icon: layout, + } ); + + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'index', + title: { rendered: 'test title' }, + description: { raw: 'test description' }, + }, + } ) + ).toEqual( { + title: 'test title', + description: 'test description', + icon: layout, + } ); + } ); + + it( 'should return correct icon based on area', () => { + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'template part, area = uncategorized', + area: 'uncategorized', + }, + } ) + ).toEqual( { + title: 'template part, area = uncategorized', + icon: layout, + } ); + + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'template part, area = invalid', + area: 'invalid', + }, + } ) + ).toEqual( { + title: 'template part, area = invalid', + icon: layout, + } ); + + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'template part, area = header', + area: 'header', + }, + } ) + ).toEqual( { + title: 'template part, area = header', + icon: header, + } ); + + expect( + getTemplateInfo( { + templateAreas: defaultTemplatePartAreas, + templateTypes: defaultTemplateTypes, + template: { + slug: 'template part, area = footer', + area: 'footer', + }, + } ) + ).toEqual( { + title: 'template part, area = footer', + icon: footer, + } ); + } ); +} ); From ddd239ab921f51835938b10aacd33bd933cccd1f Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 26 Nov 2024 14:02:36 +0400 Subject: [PATCH 003/444] Block Editor: Correctly mark Block Comment SlotFills private (#67271) * Block Editor: Correctly mark Block Comment SlotFills private * Drop '__unstable' prefix * Simplify private API exports Co-authored-by: Mamaduka Co-authored-by: jsnajdr --- .../block-settings-menu/block-settings-dropdown.js | 4 ++-- .../src/components/block-settings-menu/index.js | 4 ++-- .../src/components/collab/block-comment-icon-slot.js | 8 ++------ .../components/collab/block-comment-icon-toolbar-slot.js | 8 +++----- packages/block-editor/src/private-apis.js | 8 ++++---- .../components/collab-sidebar/comment-button-toolbar.js | 6 +++--- .../src/components/collab-sidebar/comment-button.js | 6 +++--- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index 5b5360cc48a8f..a0bbb6c708fea 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -19,7 +19,7 @@ import { pipe, useCopyToClipboard } from '@wordpress/compose'; * Internal dependencies */ import BlockActions from '../block-actions'; -import __unstableCommentIconFill from '../../components/collab/block-comment-icon-slot'; +import CommentIconSlotFill from '../../components/collab/block-comment-icon-slot'; import BlockHTMLConvertButton from './block-html-convert-button'; import __unstableBlockSettingsMenuFirstItem from './block-settings-menu-first-item'; import BlockSettingsMenuControls from '../block-settings-menu-controls'; @@ -295,7 +295,7 @@ export function BlockSettingsDropdown( { ) } - <__unstableCommentIconFill.Slot + diff --git a/packages/block-editor/src/components/block-settings-menu/index.js b/packages/block-editor/src/components/block-settings-menu/index.js index 50e8abe09d018..1b96f30e13038 100644 --- a/packages/block-editor/src/components/block-settings-menu/index.js +++ b/packages/block-editor/src/components/block-settings-menu/index.js @@ -7,12 +7,12 @@ import { ToolbarGroup, ToolbarItem } from '@wordpress/components'; * Internal dependencies */ import BlockSettingsDropdown from './block-settings-dropdown'; -import __unstableCommentIconToolbarFill from '../../components/collab/block-comment-icon-toolbar-slot'; +import CommentIconToolbarSlotFill from '../../components/collab/block-comment-icon-toolbar-slot'; export function BlockSettingsMenu( { clientIds, ...props } ) { return ( - <__unstableCommentIconToolbarFill.Slot /> + { ( toggleProps ) => ( diff --git a/packages/block-editor/src/components/collab/block-comment-icon-slot.js b/packages/block-editor/src/components/collab/block-comment-icon-slot.js index 600db904b2874..bcf8c5f5ff0c5 100644 --- a/packages/block-editor/src/components/collab/block-comment-icon-slot.js +++ b/packages/block-editor/src/components/collab/block-comment-icon-slot.js @@ -3,10 +3,6 @@ */ import { createSlotFill } from '@wordpress/components'; -const { Fill: __unstableCommentIconFill, Slot } = createSlotFill( - '__unstableCommentIconFill' -); +const CommentIconSlotFill = createSlotFill( Symbol( 'CommentIconSlotFill' ) ); -__unstableCommentIconFill.Slot = Slot; - -export default __unstableCommentIconFill; +export default CommentIconSlotFill; diff --git a/packages/block-editor/src/components/collab/block-comment-icon-toolbar-slot.js b/packages/block-editor/src/components/collab/block-comment-icon-toolbar-slot.js index dd84b284f082a..056b9a2623262 100644 --- a/packages/block-editor/src/components/collab/block-comment-icon-toolbar-slot.js +++ b/packages/block-editor/src/components/collab/block-comment-icon-toolbar-slot.js @@ -3,10 +3,8 @@ */ import { createSlotFill } from '@wordpress/components'; -const { Fill: __unstableCommentIconToolbarFill, Slot } = createSlotFill( - '__unstableCommentIconToolbarFill' +const CommentIconToolbarSlotFill = createSlotFill( + Symbol( 'CommentIconToolbarSlotFill' ) ); -__unstableCommentIconToolbarFill.Slot = Slot; - -export default __unstableCommentIconToolbarFill; +export default CommentIconToolbarSlotFill; diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 0faee244e387c..2ca185823f15e 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -49,8 +49,8 @@ import { PrivatePublishDateTimePicker } from './components/publish-date-time-pic import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes'; import useBlockDisplayTitle from './components/block-title/use-block-display-title'; import TabbedSidebar from './components/tabbed-sidebar'; -import __unstableCommentIconFill from './components/collab/block-comment-icon-slot'; -import __unstableCommentIconToolbarFill from './components/collab/block-comment-icon-toolbar-slot'; +import CommentIconSlotFill from './components/collab/block-comment-icon-slot'; +import CommentIconToolbarSlotFill from './components/collab/block-comment-icon-toolbar-slot'; /** * Private @wordpress/block-editor APIs. */ @@ -97,6 +97,6 @@ lock( privateApis, { __unstableBlockStyleVariationOverridesWithConfig, setBackgroundStyleDefaults, sectionRootClientIdKey, - __unstableCommentIconFill, - __unstableCommentIconToolbarFill, + CommentIconSlotFill, + CommentIconToolbarSlotFill, } ); diff --git a/packages/editor/src/components/collab-sidebar/comment-button-toolbar.js b/packages/editor/src/components/collab-sidebar/comment-button-toolbar.js index 2e378a7eaabab..b673ee87fe86c 100644 --- a/packages/editor/src/components/collab-sidebar/comment-button-toolbar.js +++ b/packages/editor/src/components/collab-sidebar/comment-button-toolbar.js @@ -11,18 +11,18 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; */ import { unlock } from '../../lock-unlock'; -const { __unstableCommentIconToolbarFill } = unlock( blockEditorPrivateApis ); +const { CommentIconToolbarSlotFill } = unlock( blockEditorPrivateApis ); const AddCommentToolbarButton = ( { onClick } ) => { return ( - <__unstableCommentIconToolbarFill> + - + ); }; diff --git a/packages/editor/src/components/collab-sidebar/comment-button.js b/packages/editor/src/components/collab-sidebar/comment-button.js index 3b02066181666..373ee0becd92d 100644 --- a/packages/editor/src/components/collab-sidebar/comment-button.js +++ b/packages/editor/src/components/collab-sidebar/comment-button.js @@ -12,11 +12,11 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; */ import { unlock } from '../../lock-unlock'; -const { __unstableCommentIconFill } = unlock( blockEditorPrivateApis ); +const { CommentIconSlotFill } = unlock( blockEditorPrivateApis ); const AddCommentButton = ( { onClick } ) => { return ( - <__unstableCommentIconFill> + { > { _x( 'Comment', 'Add comment button' ) } - + ); }; From 514b670054cac6b84d44b606af3973649ba3354b Mon Sep 17 00:00:00 2001 From: dhruvang21 <105810308+dhruvang21@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:58:03 +0530 Subject: [PATCH 004/444] Query Loop block: remove 'add new post' prompt in the sidebar (#67189) Co-authored-by: dhruvang21 Co-authored-by: ntsekouras Co-authored-by: t-hamano Co-authored-by: aaronrobertshaw Co-authored-by: annezazu --- .../components/block-info-slot-fill/index.js | 25 --------------- .../src/components/block-inspector/index.js | 2 -- packages/block-editor/src/private-apis.js | 2 -- .../create-new-post-link.js | 32 ------------------- .../query/edit/inspector-controls/index.js | 10 ------ packages/block-library/src/query/editor.scss | 4 --- 6 files changed, 75 deletions(-) delete mode 100644 packages/block-editor/src/components/block-info-slot-fill/index.js delete mode 100644 packages/block-library/src/query/edit/inspector-controls/create-new-post-link.js diff --git a/packages/block-editor/src/components/block-info-slot-fill/index.js b/packages/block-editor/src/components/block-info-slot-fill/index.js deleted file mode 100644 index 3592fc0424329..0000000000000 --- a/packages/block-editor/src/components/block-info-slot-fill/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * WordPress dependencies - */ -import { createSlotFill } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { - useBlockEditContext, - mayDisplayControlsKey, -} from '../block-edit/context'; - -const { Fill, Slot } = createSlotFill( Symbol( 'BlockInformation' ) ); - -const BlockInfo = ( props ) => { - const context = useBlockEditContext(); - if ( ! context[ mayDisplayControlsKey ] ) { - return null; - } - return ; -}; -BlockInfo.Slot = ( props ) => ; - -export default BlockInfo; diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 475d4f6a4b8c2..450a370b5c212 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -26,7 +26,6 @@ import useInspectorControlsTabs from '../inspector-controls-tabs/use-inspector-c import AdvancedControls from '../inspector-controls-tabs/advanced-controls-panel'; import PositionControls from '../inspector-controls-tabs/position-controls-panel'; import useBlockInspectorAnimationSettings from './useBlockInspectorAnimationSettings'; -import BlockInfo from '../block-info-slot-fill'; import BlockQuickNavigation from '../block-quick-navigation'; import { useBorderPanelLabel } from '../../hooks/border'; @@ -253,7 +252,6 @@ const BlockInspectorSingleBlock = ( { className={ blockInformation.isSynced && 'is-synced' } /> - { showTabs && ( { - const newPostUrl = addQueryArgs( 'post-new.php', { - post_type: postType, - } ); - - const addNewItemLabel = useSelect( - ( select ) => { - const { getPostType } = select( coreStore ); - return getPostType( postType )?.labels?.add_new_item; - }, - [ postType ] - ); - return ( -
- { createInterpolateElement( - '' + addNewItemLabel + '', - // eslint-disable-next-line jsx-a11y/anchor-has-content - { a: } - ) } -
- ); -}; - -export default CreateNewPostLink; diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 7d5745e190c9a..0ae4b052b5787 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -15,7 +15,6 @@ import { import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { debounce } from '@wordpress/compose'; import { useEffect, useState, useCallback } from '@wordpress/element'; @@ -28,11 +27,9 @@ import ParentControl from './parent-control'; import { TaxonomyControls } from './taxonomy-controls'; import FormatControls from './format-controls'; import StickyControl from './sticky-control'; -import CreateNewPostLink from './create-new-post-link'; import PerPageControl from './per-page-control'; import OffsetControl from './offset-controls'; import PagesControl from './pages-control'; -import { unlock } from '../../../lock-unlock'; import { usePostTypes, useIsPostTypeHierarchical, @@ -42,8 +39,6 @@ import { } from '../../utils'; import { useToolsPanelDropdownMenuProps } from '../../../utils/hooks'; -const { BlockInfo } = unlock( blockEditorPrivateApis ); - export default function QueryInspectorControls( props ) { const { attributes, setQuery, setDisplayLayout, isSingular } = props; const { query, displayLayout } = attributes; @@ -191,11 +186,6 @@ export default function QueryInspectorControls( props ) { return ( <> - { !! postType && ( - - - - ) } { showSettingsPanel && ( { showInheritControl && ( diff --git a/packages/block-library/src/query/editor.scss b/packages/block-library/src/query/editor.scss index b86eae0cbf65a..da863bff0b5da 100644 --- a/packages/block-library/src/query/editor.scss +++ b/packages/block-library/src/query/editor.scss @@ -6,10 +6,6 @@ } } -.wp-block-query__create-new-link { - padding: 0 $grid-unit-20 $grid-unit-20 52px; -} - .block-library-query__pattern-selection-content .block-editor-block-patterns-list { display: grid; grid-template-columns: 1fr 1fr 1fr; From d2c2ed8a8bac9d5b3af1876be471c6d5aaad8d7b Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Tue, 26 Nov 2024 11:53:07 +0100 Subject: [PATCH 005/444] Editor: Fix deprecation version for template selectors Co-authored-by: gigitux Co-authored-by: Mamaduka Co-authored-by: oandregal --- packages/editor/src/store/selectors.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index c17e80f771ea3..a090fee0abd64 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1707,7 +1707,7 @@ export const __experimentalGetDefaultTemplateTypes = createRegistrySelector( deprecated( "select('core/editor').__experimentalGetDefaultTemplateTypes", { - since: '6.7', + since: '6.8', alternative: "select('core/core-data').getEntityRecord( 'root', '__unstableBase' )?.default_template_types", } @@ -1730,7 +1730,7 @@ export const __experimentalGetDefaultTemplatePartAreas = createRegistrySelector( deprecated( "select('core/editor').__experimentalGetDefaultTemplatePartAreas", { - since: '6.7', + since: '6.8', alternative: "select('core/core-data').getEntityRecord( 'root', '__unstableBase' )?.default_template_part_areas", } @@ -1760,7 +1760,7 @@ export const __experimentalGetDefaultTemplateType = createRegistrySelector( deprecated( "select('core/editor').__experimentalGetDefaultTemplateType", { - since: '6.7', + since: '6.8', } ); const templateTypes = select( coreStore ).getEntityRecord( @@ -1792,7 +1792,7 @@ export const __experimentalGetTemplateInfo = createRegistrySelector( ( select ) => createSelector( ( state, template ) => { deprecated( "select('core/editor').__experimentalGetTemplateInfo", { - since: '6.7', + since: '6.8', } ); if ( ! template ) { From 94071077510763cb060de59d8758adef1216fae7 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 26 Nov 2024 17:23:52 +0400 Subject: [PATCH 006/444] Block Editor: Add success notices for image editing (#67312) * Block Editor: Add success notices for image editing * Fix translators comment Co-authored-by: Mamaduka Co-authored-by: ntsekouras Co-authored-by: ajlende --- .../components/image-editor/use-save-image.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/image-editor/use-save-image.js b/packages/block-editor/src/components/image-editor/use-save-image.js index 094ce1600545b..676f74b9ab6a6 100644 --- a/packages/block-editor/src/components/image-editor/use-save-image.js +++ b/packages/block-editor/src/components/image-editor/use-save-image.js @@ -10,6 +10,12 @@ import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +const messages = { + crop: __( 'Image cropped.' ), + rotate: __( 'Image rotated.' ), + cropAndRotate: __( 'Image cropped and rotated.' ), +}; + export default function useSaveImage( { crop, rotation, @@ -18,7 +24,8 @@ export default function useSaveImage( { onSaveImage, onFinishEditing, } ) { - const { createErrorNotice } = useDispatch( noticesStore ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); const [ isInProgress, setIsInProgress ] = useState( false ); const cancel = useCallback( () => { @@ -61,6 +68,9 @@ export default function useSaveImage( { return; } + const modifierType = + modifiers.length === 1 ? modifiers[ 0 ].type : 'cropAndRotate'; + apiFetch( { path: `/wp/v2/media/${ id }/edit`, method: 'POST', @@ -71,11 +81,14 @@ export default function useSaveImage( { id: response.id, url: response.source_url, } ); + createSuccessNotice( messages[ modifierType ], { + type: 'snackbar', + } ); } ) .catch( ( error ) => { createErrorNotice( sprintf( - /* translators: 1. Error message */ + /* translators: %s: Error message. */ __( 'Could not edit image. %s' ), stripHTML( error.message ) ), @@ -96,6 +109,7 @@ export default function useSaveImage( { url, onSaveImage, createErrorNotice, + createSuccessNotice, onFinishEditing, ] ); From 9ee20a821a69271dcada656b3df49a8266c66fe2 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 26 Nov 2024 22:28:30 +0900 Subject: [PATCH 007/444] CustomGradientPicker: Prepare `Button`s for 40px default size (#67286) * CustomGradientPicker: Prepare `Button`s for 40px default size * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + .../src/custom-gradient-picker/gradient-bar/control-points.tsx | 2 ++ packages/components/src/custom-gradient-picker/style.scss | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 23e791645a20c..44351656af3bd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -42,6 +42,7 @@ - `SlotFill`: rewrite the `Slot` component from class component to functional ([#67153](https://github.com/WordPress/gutenberg/pull/67153)). - `Menu.ItemHelpText`: Fix text wrapping to prevent unintended word breaks ([#67011](https://github.com/WordPress/gutenberg/pull/67011)). - `BorderBoxControl`: Suppress redundant warnings for deprecated 36px size ([#67213](https://github.com/WordPress/gutenberg/pull/67213)). +- `CustomGradientPicker`: Prepare `Button`s for 40px default size ([#67286](https://github.com/WordPress/gutenberg/pull/67286)). ## 28.12.0 (2024-11-16) diff --git a/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx b/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx index 3911e21e0f934..d68ee7502e1f6 100644 --- a/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx +++ b/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx @@ -66,6 +66,7 @@ function ControlPointButton( { aria-describedby={ descriptionId } aria-haspopup="true" aria-expanded={ isOpen } + __next40pxDefaultSize className={ clsx( 'components-custom-gradient-picker__control-point-button', { @@ -349,6 +350,7 @@ function InsertPoint( { } } renderToggle={ ( { isOpen, onToggle } ) => ( + ) + } + text={ label } + />
Date: Wed, 27 Nov 2024 20:27:28 +0100 Subject: [PATCH 030/444] Remove fallback for `context.postType` (#67345) Co-authored-by: SantosGuillamot --- packages/editor/src/bindings/post-meta.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index a3602ce7d6207..fcd068ac21d8a 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -108,11 +108,8 @@ export default { return false; } - const postType = - context?.postType || select( editorStore ).getCurrentPostType(); - - // Check that editing is happening in the post editor and not a template. - if ( postType === 'wp_template' ) { + // Lock editing when `postType` is not defined. + if ( ! context?.postType ) { return false; } From d9eb6d9a6d64650f08ee68792b88f914de62cda1 Mon Sep 17 00:00:00 2001 From: Bernie Reiter <96308+ockham@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:35:22 +0100 Subject: [PATCH 031/444] Navigation block: Remove more obsolete Block Hooks helpers (#67193) - `block_core_navigation_remove_serialized_parent_block` was `replaced by remove_serialized_parent_block`. - `apply_block_hooks_to_content` has been part of WP Core since 6.6, so we don't need to check for its existence anymore. Co-authored-by: ockham Co-authored-by: draganescu --- .../block-library/src/navigation/index.php | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index ae3b9620a3358..68b23aceeced6 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -1436,20 +1436,6 @@ function block_core_navigation_get_most_recently_published_navigation() { return null; } -/** - * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks. - * - * @since 6.5.0 - * - * @param string $serialized_block The serialized markup of a block and its inner blocks. - * @return string - */ -function block_core_navigation_remove_serialized_parent_block( $serialized_block ) { - $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); - $end = strrpos( $serialized_block, ' +

Pattern Overrides

+ + +

Post Meta Binding

+ + +

No Overrides or Binding

+ + `; + + const { id } = await requestUtils.createBlock( { + title: 'Pattern', + content, + status: 'publish', + } ); + + await admin.visitSiteEditor( { + postId: 'emptytheme//index', + postType: 'wp_template', + canvas: 'edit', + } ); + + await editor.setContent( '' ); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + + const patternBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Pattern', + } ); + const paragraphs = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + const blockWithOverrides = paragraphs.filter( { + hasText: 'Pattern Overrides', + } ); + const blockWithBindings = paragraphs.filter( { + hasText: 'Post Meta Binding', + } ); + const blockWithoutOverridesOrBindings = paragraphs.filter( { + hasText: 'No Overrides or Binding', + } ); + + await test.step( 'Zoomed in / Design mode', async () => { + await editor.switchEditorTool( 'Design' ); + // In zoomed in and design mode the pattern block and child blocks + // with bindings are editable. + await expect( patternBlock ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithOverrides ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + await test.step( 'Zoomed in / Write mode - pattern as a section', async () => { + await editor.switchEditorTool( 'Write' ); + // The pattern block is still editable as a section. + await expect( patternBlock ).not.toHaveAttribute( + 'inert', + 'true' + ); + // Child blocks of the pattern with bindings are editable. + await expect( blockWithOverrides ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + await test.step( 'Zoomed out / Write mode - pattern as a section', async () => { + await page.getByLabel( 'Zoom Out' ).click(); + // In zoomed out only the pattern block is editable, as in this scenario it's a section. + await expect( patternBlock ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithOverrides ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + await test.step( 'Zoomed out / Design mode - pattern as a section', async () => { + await editor.switchEditorTool( 'Design' ); + // In zoomed out only the pattern block is editable, as in this scenario it's a section. + await expect( patternBlock ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithOverrides ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + // Zoom out and group the pattern. + await page.getByLabel( 'Zoom Out' ).click(); + await editor.selectBlocks( patternBlock ); + await editor.clickBlockOptionsMenuItem( 'Group' ); + + await test.step( 'Zoomed in / Write mode - pattern nested in a section', async () => { + await editor.switchEditorTool( 'Write' ); + // The pattern block is not inert as it has editable content, but it shouldn't be selectable. + // TODO: find a way to test that the block is not selectable. + await expect( patternBlock ).not.toHaveAttribute( + 'inert', + 'true' + ); + // Child blocks of the pattern are editable as normal. + await expect( blockWithOverrides ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).not.toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + await test.step( 'Zoomed out / Write mode - pattern nested in a section', async () => { + await page.getByLabel( 'Zoom Out' ).click(); + // None of the pattern is editable in zoomed out when nested in a section. + await expect( patternBlock ).toHaveAttribute( 'inert', 'true' ); + await expect( blockWithOverrides ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + await test.step( 'Zoomed out / Design mode - pattern nested in a section', async () => { + await editor.switchEditorTool( 'Design' ); + // None of the pattern is editable in zoomed out when nested in a section. + await expect( patternBlock ).toHaveAttribute( 'inert', 'true' ); + await expect( blockWithOverrides ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + } ); + + test( 'disables editing of nested patterns', async ( { + page, + admin, + requestUtils, + editor, + } ) => { + const paragraphName = 'Editable paragraph'; + const headingName = 'Editable heading'; + const innerPattern = await requestUtils.createBlock( { + title: 'Inner Pattern', + content: ` +

Inner paragraph

+ `, + status: 'publish', + } ); + const outerPattern = await requestUtils.createBlock( { + title: 'Outer Pattern', + content: ` +

Outer heading

+ + `, + status: 'publish', + } ); + + await admin.createNewPost(); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: outerPattern.id }, + } ); + + // Make an edit to the outer pattern heading. + await editor.canvas + .getByRole( 'document', { name: 'Block: Heading' } ) + .fill( 'Outer heading (edited)' ); + + const postId = await editor.publishPost(); + + // Check the pattern has the correct attributes. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/block', + attributes: { + ref: outerPattern.id, + content: { + [ headingName ]: { + content: 'Outer heading (edited)', + }, + }, + }, + innerBlocks: [], + }, + ] ); + // Check it renders correctly. + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( headingBlock ).toHaveText( 'Outer heading (edited)' ); + await expect( headingBlock ).not.toHaveAttribute( 'inert', 'true' ); + await expect( paragraphBlock ).toHaveText( + 'Inner paragraph (edited)' + ); + await expect( paragraphBlock ).toHaveAttribute( 'inert', 'true' ); + + // Edit the outer pattern. + await editor.selectBlocks( + editor.canvas + .getByRole( 'document', { name: 'Block: Pattern' } ) + .first() + ); + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Edit original' } ) + .click(); + + // The inner paragraph should be editable in the pattern focus mode. + await editor.selectBlocks( + editor.canvas + .getByRole( 'document', { name: 'Block: Pattern' } ) + .first() + ); + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ), + 'The inner paragraph should be editable' + ).not.toHaveAttribute( 'inert', 'true' ); + + // Visit the post on the frontend. + await page.goto( `/?p=${ postId }` ); + + await expect( + page.getByRole( 'heading', { level: 2 } ) + ).toHaveText( 'Outer heading (edited)' ); + await expect( + page.getByText( 'Inner paragraph (edited)' ) + ).toBeVisible(); + } ); + } ); + test( 'retains override values when converting a pattern block to regular blocks', async ( { page, admin, @@ -425,107 +740,6 @@ test.describe( 'Pattern Overrides', () => { await expect( buttonLink ).toHaveAttribute( 'rel', /^\s*nofollow\s*$/ ); } ); - test( 'disables editing of nested patterns', async ( { - page, - admin, - requestUtils, - editor, - } ) => { - const paragraphName = 'Editable paragraph'; - const headingName = 'Editable heading'; - const innerPattern = await requestUtils.createBlock( { - title: 'Inner Pattern', - content: ` -

Inner paragraph

-`, - status: 'publish', - } ); - const outerPattern = await requestUtils.createBlock( { - title: 'Outer Pattern', - content: ` -

Outer heading

- -`, - status: 'publish', - } ); - - await admin.createNewPost(); - - await editor.insertBlock( { - name: 'core/block', - attributes: { ref: outerPattern.id }, - } ); - - // Make an edit to the outer pattern heading. - await editor.canvas - .getByRole( 'document', { name: 'Block: Heading' } ) - .fill( 'Outer heading (edited)' ); - - const postId = await editor.publishPost(); - - // Check the pattern has the correct attributes. - await expect.poll( editor.getBlocks ).toMatchObject( [ - { - name: 'core/block', - attributes: { - ref: outerPattern.id, - content: { - [ headingName ]: { - content: 'Outer heading (edited)', - }, - }, - }, - innerBlocks: [], - }, - ] ); - // Check it renders correctly. - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( headingBlock ).toHaveText( 'Outer heading (edited)' ); - await expect( headingBlock ).not.toHaveAttribute( 'inert', 'true' ); - await expect( paragraphBlock ).toHaveText( 'Inner paragraph (edited)' ); - await expect( paragraphBlock ).toHaveAttribute( 'inert', 'true' ); - - // Edit the outer pattern. - await editor.selectBlocks( - editor.canvas - .getByRole( 'document', { name: 'Block: Pattern' } ) - .first() - ); - await editor.showBlockToolbar(); - await page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Edit original' } ) - .click(); - - // The inner paragraph should be editable in the pattern focus mode. - await editor.selectBlocks( - editor.canvas - .getByRole( 'document', { name: 'Block: Pattern' } ) - .first() - ); - await expect( - editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ), - 'The inner paragraph should be editable' - ).not.toHaveAttribute( 'inert', 'true' ); - - // Visit the post on the frontend. - await page.goto( `/?p=${ postId }` ); - - await expect( page.getByRole( 'heading', { level: 2 } ) ).toHaveText( - 'Outer heading (edited)' - ); - await expect( - page.getByText( 'Inner paragraph (edited)' ) - ).toBeVisible(); - } ); - test( 'resets overrides after clicking the reset button', async ( { page, admin, From 2f6ef277f3c9f875f8294c387727249ecd0defa0 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 28 Nov 2024 14:23:17 +0800 Subject: [PATCH 037/444] Remove use of `contentOnly` block editing mode for synced patterns (#67364) * Remove use of `contentOnly` block editing mode for synced patterns * Update unit tests ---- Co-authored-by: talldan --- packages/block-editor/src/store/reducer.js | 2 +- packages/block-editor/src/store/test/reducer.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 1e09ec98f005a..edae9c392c37d 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2355,7 +2355,7 @@ function getDerivedBlockEditingModesForTree( return; } - derivedBlockEditingModes.set( clientId, 'contentOnly' ); + // Else do nothing, use the default block editing mode. return; } diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index b539afde9e025..dd1665d6736ad 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -3828,7 +3828,6 @@ describe( 'state', () => { expect( initialState.derivedBlockEditingModes ).toEqual( new Map( Object.entries( { - 'root-pattern': 'contentOnly', 'pattern-paragraph': 'disabled', 'pattern-group': 'disabled', 'pattern-paragraph-with-overrides': 'contentOnly', @@ -3883,7 +3882,6 @@ describe( 'state', () => { expect( derivedBlockEditingModes ).toEqual( new Map( Object.entries( { - 'root-pattern': 'contentOnly', // Pattern and section. 'pattern-paragraph': 'disabled', 'pattern-group': 'disabled', 'pattern-paragraph-with-overrides': 'contentOnly', // Pattern child with bindings. @@ -3903,7 +3901,7 @@ describe( 'state', () => { 'paragraph-1': 'contentOnly', // Content block in section. 'group-2': 'disabled', 'paragraph-2': 'contentOnly', // Content block in section. - 'root-pattern': 'contentOnly', // Pattern and section. + 'root-pattern': 'contentOnly', // Section. 'pattern-paragraph': 'disabled', 'pattern-group': 'disabled', 'pattern-paragraph-with-overrides': 'contentOnly', // Pattern child with bindings. From c9a5cab0e2e9063054ed4f299bde734a242e3794 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 28 Nov 2024 08:51:04 +0100 Subject: [PATCH 038/444] Site Editor: Use path based routing instead of query args and site-editor.php routes (#67199) Co-authored-by: youknowriad Co-authored-by: jsnajdr Co-authored-by: tyxla --- backport-changelog/6.8/7903.md | 3 + lib/compat/wordpress-6.8/site-editor.php | 124 ++++++++++ lib/experimental/posts/load.php | 12 - lib/load.php | 1 + package-lock.json | 10 +- .../src/admin-navigation-commands.js | 6 +- .../src/site-editor-navigation-commands.js | 143 ++++++------ .../src/components/add-new-pattern/index.js | 26 +-- .../src/components/add-new-template/index.js | 8 +- .../edit-site/src/components/app/index.js | 40 +++- .../block-editor/use-editor-iframe-props.js | 9 +- .../use-navigate-to-entity-record.js | 4 +- .../block-editor/use-site-editor-settings.js | 12 +- .../src/components/dataviews-actions/index.js | 6 +- .../edit-site/src/components/editor/index.js | 93 +++++--- .../editor/use-resolve-edited-entity.js | 19 +- .../components/global-styles-sidebar/index.js | 8 +- .../edit-site/src/components/layout/index.js | 7 +- .../edit-site/src/components/layout/router.js | 86 ------- .../delete-category-menu-item.js | 7 +- .../src/components/page-patterns/fields.js | 23 +- .../src/components/page-patterns/index.js | 30 +-- .../src/components/page-templates/fields.js | 19 +- .../src/components/page-templates/index.js | 31 +-- .../src/components/post-list/index.js | 78 ++++--- .../src/components/posts-app-routes/home.js | 36 --- .../src/components/posts-app-routes/index.js | 17 +- .../{posts-edit.js => post-item.js} | 14 +- .../posts-list-view-quick-edit.js | 52 ----- .../posts-app-routes/posts-list-view.js | 40 ---- .../posts-app-routes/posts-view-quick-edit.js | 49 ---- .../components/posts-app-routes/posts-view.js | 35 --- .../src/components/posts-app-routes/posts.js | 66 ++++++ .../src/components/posts-app/index.js | 17 +- .../src/components/resizable-frame/index.js | 13 +- .../edit-site/src/components/routes/link.js | 68 ------ .../src/components/save-panel/index.js | 4 +- .../sidebar-dataviews/add-new-view.js | 18 +- .../sidebar-dataviews/dataview-item.js | 13 +- .../src/components/sidebar-dataviews/index.js | 4 +- .../sidebar-global-styles-wrapper/index.js | 54 ++--- .../sidebar-navigation-item/index.js | 12 +- .../index.js | 26 +-- .../sidebar-navigation-screen-main/index.js | 13 +- .../more-menu.js | 8 +- .../use-navigation-menu-handlers.js | 4 +- .../index.js | 12 +- .../leaf-more-menu.js | 30 +-- .../category-item.js | 25 +- .../index.js | 7 +- .../content.js | 41 ++-- .../index.js | 15 +- .../sidebar-navigation-screen/index.js | 4 +- .../site-editor-routes/home-edit.js | 17 -- .../{home-view.js => home.js} | 9 +- .../components/site-editor-routes/index.js | 56 ++--- .../site-editor-routes/navigation-edit.js | 22 -- .../navigation-item-edit.js | 26 --- .../navigation-item-view.js | 25 -- .../site-editor-routes/navigation-item.js | 39 ++++ .../site-editor-routes/navigation-view.js | 21 -- .../site-editor-routes/navigation.js | 34 +++ .../{pages-edit.js => page-item.js} | 20 +- .../pages-list-view-quick-edit.js | 56 ----- .../site-editor-routes/pages-list-view.js | 44 ---- .../pages-view-quick-edit.js | 53 ----- .../site-editor-routes/pages-view.js | 39 ---- .../components/site-editor-routes/pages.js | 66 ++++++ .../site-editor-routes/pattern-item.js | 15 ++ .../site-editor-routes/patterns-edit.js | 24 -- .../site-editor-routes/patterns-view.js | 22 -- .../components/site-editor-routes/patterns.js | 15 ++ .../{styles-view.js => styles.js} | 14 +- .../site-editor-routes/template-item.js | 15 ++ .../site-editor-routes/template-part-item.js | 15 ++ .../site-editor-routes/templates-edit.js | 22 -- .../site-editor-routes/templates-list-view.js | 28 --- .../site-editor-routes/templates-view.js | 22 -- .../site-editor-routes/templates.js | 45 ++++ .../src/components/site-hub/index.js | 9 +- .../src/hooks/commands/use-common-commands.js | 86 ++----- .../hooks/commands/use-set-command-context.js | 4 +- .../edit-site/src/store/private-actions.js | 7 + packages/edit-site/src/store/reducer.js | 2 + .../src/utils/is-previewing-theme.js | 4 +- .../edit-site/src/utils/use-activate-theme.js | 7 +- packages/router/package.json | 4 +- packages/router/src/history.ts | 99 -------- packages/router/src/link.tsx | 55 +++++ packages/router/src/private-apis.ts | 3 + packages/router/src/router.tsx | 220 +++++++++++++++++- packages/router/tsconfig.json | 5 +- .../editor/various/pattern-overrides.spec.js | 12 +- .../specs/site-editor/browser-history.spec.js | 4 +- .../specs/site-editor/command-center.spec.js | 2 +- .../specs/site-editor/hybrid-theme.spec.js | 2 +- .../site-editor-url-navigation.spec.js | 4 +- 97 files changed, 1249 insertions(+), 1546 deletions(-) create mode 100644 backport-changelog/6.8/7903.md create mode 100644 lib/compat/wordpress-6.8/site-editor.php delete mode 100644 packages/edit-site/src/components/layout/router.js delete mode 100644 packages/edit-site/src/components/posts-app-routes/home.js rename packages/edit-site/src/components/posts-app-routes/{posts-edit.js => post-item.js} (62%) delete mode 100644 packages/edit-site/src/components/posts-app-routes/posts-list-view-quick-edit.js delete mode 100644 packages/edit-site/src/components/posts-app-routes/posts-list-view.js delete mode 100644 packages/edit-site/src/components/posts-app-routes/posts-view-quick-edit.js delete mode 100644 packages/edit-site/src/components/posts-app-routes/posts-view.js create mode 100644 packages/edit-site/src/components/posts-app-routes/posts.js delete mode 100644 packages/edit-site/src/components/routes/link.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/home-edit.js rename packages/edit-site/src/components/site-editor-routes/{home-view.js => home.js} (66%) delete mode 100644 packages/edit-site/src/components/site-editor-routes/navigation-edit.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/navigation-item-edit.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/navigation-item-view.js create mode 100644 packages/edit-site/src/components/site-editor-routes/navigation-item.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/navigation-view.js create mode 100644 packages/edit-site/src/components/site-editor-routes/navigation.js rename packages/edit-site/src/components/site-editor-routes/{pages-edit.js => page-item.js} (54%) delete mode 100644 packages/edit-site/src/components/site-editor-routes/pages-list-view-quick-edit.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/pages-list-view.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/pages-view-quick-edit.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/pages-view.js create mode 100644 packages/edit-site/src/components/site-editor-routes/pages.js create mode 100644 packages/edit-site/src/components/site-editor-routes/pattern-item.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/patterns-edit.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/patterns-view.js create mode 100644 packages/edit-site/src/components/site-editor-routes/patterns.js rename packages/edit-site/src/components/site-editor-routes/{styles-view.js => styles.js} (58%) create mode 100644 packages/edit-site/src/components/site-editor-routes/template-item.js create mode 100644 packages/edit-site/src/components/site-editor-routes/template-part-item.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/templates-edit.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/templates-list-view.js delete mode 100644 packages/edit-site/src/components/site-editor-routes/templates-view.js create mode 100644 packages/edit-site/src/components/site-editor-routes/templates.js delete mode 100644 packages/router/src/history.ts create mode 100644 packages/router/src/link.tsx diff --git a/backport-changelog/6.8/7903.md b/backport-changelog/6.8/7903.md new file mode 100644 index 0000000000000..cb20d8d2dd2b1 --- /dev/null +++ b/backport-changelog/6.8/7903.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7903 + +* https://github.com/WordPress/gutenberg/pull/67199 diff --git a/lib/compat/wordpress-6.8/site-editor.php b/lib/compat/wordpress-6.8/site-editor.php new file mode 100644 index 0000000000000..cde108830b1d2 --- /dev/null +++ b/lib/compat/wordpress-6.8/site-editor.php @@ -0,0 +1,124 @@ + '/wp_navigation/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_navigation' === $_REQUEST['postType'] && empty( $_REQUEST['postId'] ) ) { + return add_query_arg( array( 'p' => '/navigation' ), remove_query_arg( 'postType' ) ); + } + + if ( isset( $_REQUEST['path'] ) && '/wp_global_styles' === $_REQUEST['path'] ) { + return add_query_arg( array( 'p' => '/styles' ), remove_query_arg( 'path' ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'page' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) { + return add_query_arg( array( 'p' => '/page' ), remove_query_arg( 'postType' ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'page' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) { + return add_query_arg( array( 'p' => '/page/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_template' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) { + return add_query_arg( array( 'p' => '/template' ), remove_query_arg( 'postType' ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_template' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) { + return add_query_arg( array( 'p' => '/wp_template/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_block' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) { + return add_query_arg( array( 'p' => '/pattern' ), remove_query_arg( 'postType' ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_block' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) { + return add_query_arg( array( 'p' => '/wp_block/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_template_part' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) { + return add_query_arg( array( 'p' => '/pattern' ) ); + } + + if ( isset( $_REQUEST['postType'] ) && 'wp_template_part' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) { + return add_query_arg( array( 'p' => '/wp_template_part/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) ); + } + + // The following redirects are for backward compatibility with the old site editor URLs. + if ( isset( $_REQUEST['path'] ) && '/wp_template_part/all' === $_REQUEST['path'] ) { + return add_query_arg( + array( + 'p' => '/pattern', + 'postType' => 'wp_template_part', + ), + remove_query_arg( 'path' ) + ); + } + + if ( isset( $_REQUEST['path'] ) && '/page' === $_REQUEST['path'] ) { + return add_query_arg( array( 'p' => '/page' ), remove_query_arg( 'path' ) ); + } + + if ( isset( $_REQUEST['path'] ) && '/wp_template' === $_REQUEST['path'] ) { + return add_query_arg( array( 'p' => '/template' ), remove_query_arg( 'path' ) ); + } + + if ( isset( $_REQUEST['path'] ) && '/patterns' === $_REQUEST['path'] ) { + return add_query_arg( array( 'p' => '/pattern' ), remove_query_arg( 'path' ) ); + } + + if ( isset( $_REQUEST['path'] ) && '/navigation' === $_REQUEST['path'] ) { + return add_query_arg( array( 'p' => '/navigation' ), remove_query_arg( 'path' ) ); + } + + return add_query_arg( array( 'p' => '/' ) ); +} + +function gutenberg_redirect_site_editor_deprecated_urls() { + $redirection = gutenberg_get_site_editor_redirection(); + if ( false !== $redirection ) { + wp_redirect( $redirection, 301 ); + exit; + } +} +add_action( 'admin_init', 'gutenberg_redirect_site_editor_deprecated_urls' ); + +/** + * Filter the `wp_die_handler` to allow access to the Site Editor's new pages page + * for Classic themes. + * + * site-editor.php's access is forbidden for hybrid/classic themes and only allowed with some very special query args (some very special pages like template parts...). + * The only way to disable this protection since we're changing the urls in Gutenberg is to override the wp_die_handler. + * + * @param callable $default_handler The default handler. + * @return callable The default handler or a custom handler. + */ +function gutenberg_styles_wp_die_handler( $default_handler ) { + if ( ! wp_is_block_theme() && str_contains( $_SERVER['REQUEST_URI'], 'site-editor.php' ) && isset( $_GET['p'] ) ) { + return '__return_false'; + } + return $default_handler; +} +add_filter( 'wp_die_handler', 'gutenberg_styles_wp_die_handler' ); diff --git a/lib/experimental/posts/load.php b/lib/experimental/posts/load.php index 7321392b11a25..699534f1886f5 100644 --- a/lib/experimental/posts/load.php +++ b/lib/experimental/posts/load.php @@ -69,18 +69,6 @@ function gutenberg_posts_dashboard() { echo '
'; } -/** - * Redirects to the new posts dashboard page and adds the postType query arg. - */ -function gutenberg_add_post_type_arg() { - global $pagenow; - if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'gutenberg-posts-dashboard' === $_GET['page'] && empty( $_GET['postType'] ) ) { - wp_redirect( admin_url( '/admin.php?page=gutenberg-posts-dashboard&postType=post' ) ); - exit; - } -} -add_action( 'admin_init', 'gutenberg_add_post_type_arg' ); - /** * Replaces the default posts menu item with the new posts dashboard. */ diff --git a/lib/load.php b/lib/load.php index 85d1c7e3292b5..97c5404a3a3ea 100644 --- a/lib/load.php +++ b/lib/load.php @@ -98,6 +98,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/blocks.php'; require __DIR__ . '/compat/wordpress-6.8/functions.php'; require __DIR__ . '/compat/wordpress-6.8/post.php'; +require __DIR__ . '/compat/wordpress-6.8/site-editor.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/package-lock.json b/package-lock.json index ccf779f2d67ea..dc0c18f3b6ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44492,6 +44492,12 @@ "node": ">=10.0.0" } }, + "node_modules/route-recognizer": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/route-recognizer/-/route-recognizer-0.3.4.tgz", + "integrity": "sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==", + "license": "MIT" + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -55563,10 +55569,12 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "7.25.7", + "@wordpress/compose": "*", "@wordpress/element": "*", "@wordpress/private-apis": "*", "@wordpress/url": "*", - "history": "^5.3.0" + "history": "^5.3.0", + "route-recognizer": "^0.3.4" }, "engines": { "node": ">=18.12.0", diff --git a/packages/core-commands/src/admin-navigation-commands.js b/packages/core-commands/src/admin-navigation-commands.js index 8a8167bb29b82..f82faa05baae4 100644 --- a/packages/core-commands/src/admin-navigation-commands.js +++ b/packages/core-commands/src/admin-navigation-commands.js @@ -44,11 +44,7 @@ const getAddNewPageCommand = () => } ); if ( page?.id ) { - history.push( { - postId: page.id, - postType: 'page', - canvas: 'edit', - } ); + history.navigate( `/page/${ page.id }?canvas=edit` ); } } catch ( error ) { const errorMessage = diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index 2785d809d41e0..c1b12a84d4d61 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -136,19 +136,18 @@ const getNavigationCommandLoaderPerPostType = ( postType ) => return { ...command, callback: ( { close } ) => { - const args = { - postType, - postId: record.id, - canvas: 'edit', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( + `/${ postType }/${ record.id }?canvas=edit` + ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: `/${ postType }/${ record.id }`, + canvas: 'edit', + } + ); } close(); }, @@ -220,19 +219,18 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => : __( '(no title)' ), icon: icons[ templateType ], callback: ( { close } ) => { - const args = { - postType: templateType, - postId: record.id, - canvas: 'edit', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( + `/${ templateType }/${ record.id }?canvas=edit` + ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: `/${ templateType }/${ record.id }`, + canvas: 'edit', + } + ); } close(); }, @@ -249,18 +247,19 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => label: __( 'Template parts' ), icon: symbolFilled, callback: ( { close } ) => { - const args = { - postType: 'wp_template_part', - categoryId: 'all-parts', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( + '/pattern?postType=wp_template_part&categoryId=all-parts' + ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: '/pattern', + postType: 'wp_template_part', + categoryId: 'all-parts', + } + ); } close(); }, @@ -303,17 +302,15 @@ const getSiteEditorBasicNavigationCommands = () => label: __( 'Navigation' ), icon: navigation, callback: ( { close } ) => { - const args = { - postType: 'wp_navigation', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( '/navigation' ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: '/navigation', + } + ); } close(); }, @@ -324,17 +321,15 @@ const getSiteEditorBasicNavigationCommands = () => label: __( 'Styles' ), icon: styles, callback: ( { close } ) => { - const args = { - path: '/wp_global_styles', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( '/styles' ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: '/styles', + } + ); } close(); }, @@ -345,17 +340,15 @@ const getSiteEditorBasicNavigationCommands = () => label: __( 'Pages' ), icon: page, callback: ( { close } ) => { - const args = { - postType: 'page', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( '/page' ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: '/page', + } + ); } close(); }, @@ -366,17 +359,15 @@ const getSiteEditorBasicNavigationCommands = () => label: __( 'Templates' ), icon: layout, callback: ( { close } ) => { - const args = { - postType: 'wp_template', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( '/template' ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: '/template', + } + ); } close(); }, @@ -389,17 +380,15 @@ const getSiteEditorBasicNavigationCommands = () => icon: symbol, callback: ( { close } ) => { if ( canCreateTemplate ) { - const args = { - postType: 'wp_block', - }; - const targetUrl = addQueryArgs( - 'site-editor.php', - args - ); if ( isSiteEditor ) { - history.push( args ); + history.navigate( '/pattern' ); } else { - document.location = targetUrl; + document.location = addQueryArgs( + 'site-editor.php', + { + p: '/pattern', + } + ); } close(); } else { diff --git a/packages/edit-site/src/components/add-new-pattern/index.js b/packages/edit-site/src/components/add-new-pattern/index.js index bb9e53da6a566..63452691c1c37 100644 --- a/packages/edit-site/src/components/add-new-pattern/index.js +++ b/packages/edit-site/src/components/add-new-pattern/index.js @@ -69,23 +69,16 @@ export default function AddNewPattern() { function handleCreatePattern( { pattern } ) { setShowPatternModal( false ); - - history.push( { - postId: pattern.id, - postType: PATTERN_TYPES.user, - canvas: 'edit', - } ); + history.navigate( + `/${ PATTERN_TYPES.user }/${ pattern.id }?canvas=edit` + ); } function handleCreateTemplatePart( templatePart ) { setShowTemplatePartModal( false ); - - // Navigate to the created template part editor. - history.push( { - postId: templatePart.id, - postType: TEMPLATE_PART_POST_TYPE, - canvas: 'edit', - } ); + history.navigate( + `/${ TEMPLATE_PART_POST_TYPE }/${ templatePart.id }?canvas=edit` + ); } function handleError() { @@ -203,10 +196,9 @@ export default function AddNewPattern() { ! currentCategoryId && categoryId !== 'my-patterns' ) { - history.push( { - postType: PATTERN_TYPES.user, - categoryId: PATTERN_DEFAULT_CATEGORY, - } ); + history.navigate( + `/pattern?categoryId=${ PATTERN_DEFAULT_CATEGORY }` + ); } createSuccessNotice( diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js index 1a2d9ea727fa8..5f06ecae6824a 100644 --- a/packages/edit-site/src/components/add-new-template/index.js +++ b/packages/edit-site/src/components/add-new-template/index.js @@ -203,11 +203,9 @@ function NewTemplateModal( { onClose } ) { ); // Navigate to the created template editor. - history.push( { - postId: newTemplate.id, - postType: TEMPLATE_POST_TYPE, - canvas: 'edit', - } ); + history.navigate( + `/${ TEMPLATE_POST_TYPE }/${ newTemplate.id }?canvas=edit` + ); createSuccessNotice( sprintf( diff --git a/packages/edit-site/src/components/app/index.js b/packages/edit-site/src/components/app/index.js index 7e4c50d7d00f0..cf13e7baf1b73 100644 --- a/packages/edit-site/src/components/app/index.js +++ b/packages/edit-site/src/components/app/index.js @@ -2,35 +2,41 @@ * WordPress dependencies */ import { store as noticesStore } from '@wordpress/notices'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import { PluginArea } from '@wordpress/plugins'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies */ import Layout from '../layout'; import { unlock } from '../../lock-unlock'; +import { store as editSiteStore } from '../../store'; import { useCommonCommands } from '../../hooks/commands/use-common-commands'; -import useActiveRoute from '../layout/router'; import useSetCommandContext from '../../hooks/commands/use-set-command-context'; import { useRegisterSiteEditorRoutes } from '../site-editor-routes'; +import { + currentlyPreviewingTheme, + isPreviewingTheme, +} from '../../utils/is-previewing-theme'; const { RouterProvider } = unlock( routerPrivateApis ); function AppLayout() { useCommonCommands(); useSetCommandContext(); - useRegisterSiteEditorRoutes(); - const route = useActiveRoute(); - return ; + return ; } export default function App() { + useRegisterSiteEditorRoutes(); const { createErrorNotice } = useDispatch( noticesStore ); - + const routes = useSelect( ( select ) => { + return unlock( select( editSiteStore ) ).getRoutes(); + }, [] ); function onPluginAreaError( name ) { createErrorNotice( sprintf( @@ -42,9 +48,29 @@ export default function App() { ) ); } + const beforeNavigate = useCallback( ( { path, query } ) => { + if ( ! isPreviewingTheme() ) { + return { path, query }; + } + + return { + path, + query: { + ...query, + wp_theme_preview: + 'wp_theme_preview' in query + ? query.wp_theme_preview + : currentlyPreviewingTheme(), + }, + }; + }, [] ); return ( - + diff --git a/packages/edit-site/src/components/block-editor/use-editor-iframe-props.js b/packages/edit-site/src/components/block-editor/use-editor-iframe-props.js index 7c88fee0d5b72..1c70c85aed08d 100644 --- a/packages/edit-site/src/components/block-editor/use-editor-iframe-props.js +++ b/packages/edit-site/src/components/block-editor/use-editor-iframe-props.js @@ -12,6 +12,7 @@ import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { store as editorStore } from '@wordpress/editor'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -21,9 +22,9 @@ import { unlock } from '../../lock-unlock'; const { useLocation, useHistory } = unlock( routerPrivateApis ); export default function useEditorIframeProps() { - const { params } = useLocation(); + const { query, path } = useLocation(); const history = useHistory(); - const { canvas = 'view' } = params; + const { canvas = 'view' } = query; const currentPostIsTrashed = useSelect( ( select ) => { return ( select( editorStore ).getCurrentPostAttribute( 'status' ) === @@ -55,13 +56,13 @@ export default function useEditorIframeProps() { ! currentPostIsTrashed ) { event.preventDefault(); - history.push( { ...params, canvas: 'edit' }, undefined, { + history.navigate( addQueryArgs( path, { canvas: 'edit' } ), { transition: 'canvas-mode-edit-transition', } ); } }, onClick: () => - history.push( { ...params, canvas: 'edit' }, undefined, { + history.navigate( addQueryArgs( path, { canvas: 'edit' } ), { transition: 'canvas-mode-edit-transition', } ), onClickCapture: ( event ) => { diff --git a/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js b/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js index 120b15b8551d3..8cc7fdaefe2d9 100644 --- a/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js +++ b/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js @@ -16,7 +16,9 @@ export default function useNavigateToEntityRecord() { const onNavigateToEntityRecord = useCallback( ( params ) => { - history.push( { ...params, focusMode: true, canvas: 'edit' } ); + history.navigate( + `/${ params.postType }/${ params.id }?canvas=edit&focusMode=true` + ); }, [ history ] ); diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 186f4aacf7923..d37987dc3dc42 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -22,11 +22,11 @@ function useNavigateToPreviousEntityRecord() { const history = useHistory(); const goBack = useMemo( () => { const isFocusMode = - location.params.focusMode || - ( location.params.postId && - FOCUSABLE_ENTITIES.includes( location.params.postType ) ); + location.query.focusMode || + ( location?.params?.postId && + FOCUSABLE_ENTITIES.includes( location?.params?.postType ) ); const didComeFromEditorCanvas = - previousLocation?.params.canvas === 'edit'; + previousLocation?.query.canvas === 'edit'; const showBackButton = isFocusMode && didComeFromEditorCanvas; return showBackButton ? () => history.back() : undefined; // `previousLocation` changes when the component updates for any reason, not @@ -37,8 +37,8 @@ function useNavigateToPreviousEntityRecord() { } export function useSpecificEditorSettings() { - const { params } = useLocation(); - const { canvas = 'view' } = params; + const { query } = useLocation(); + const { canvas = 'view' } = query; const onNavigateToEntityRecord = useNavigateToEntityRecord(); const { settings } = useSelect( ( select ) => { const { getSettings } = select( editSiteStore ); diff --git a/packages/edit-site/src/components/dataviews-actions/index.js b/packages/edit-site/src/components/dataviews-actions/index.js index 09b7597c6cb34..0a7b20c712c82 100644 --- a/packages/edit-site/src/components/dataviews-actions/index.js +++ b/packages/edit-site/src/components/dataviews-actions/index.js @@ -31,11 +31,7 @@ export const useEditPostAction = () => { }, callback( items ) { const post = items[ 0 ]; - history.push( { - postId: post.id, - postType: post.type, - canvas: 'edit', - } ); + history.navigate( `/${ post.type }/${ post.id }?canvas=edit` ); }, } ), [ history ] diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 1d115dca7518d..c045bafd8a683 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -54,6 +54,7 @@ import { useResolveEditedEntity, useSyncDeprecatedEntityIntoState, } from './use-resolve-edited-entity'; +import { addQueryArgs } from '@wordpress/url'; const { Editor, BackButton } = unlock( editorPrivateApis ); const { useHistory, useLocation } = unlock( routerPrivateApis ); @@ -83,10 +84,44 @@ const siteIconVariants = { }, }; +function getListPathForPostType( postType ) { + switch ( postType ) { + case 'navigation': + return '/navigation'; + case 'wp_block': + return '/pattern?postType=wp_block'; + case 'wp_template_part': + return '/pattern?postType=wp_template_part'; + case 'wp_template': + return '/template'; + case 'page': + return '/page'; + case 'post': + return '/'; + } + throw 'Unknown post type'; +} + +function getNavigationPath( location, postType ) { + const { path, name } = location; + if ( + [ + 'pattern-item', + 'template-part-item', + 'page-item', + 'template-item', + 'post-item', + ].includes( name ) + ) { + return getListPathForPostType( postType ); + } + return addQueryArgs( path, { canvas: undefined } ); +} + export default function EditSiteEditor( { isPostsList = false } ) { const disableMotion = useReducedMotion(); - const { params } = useLocation(); - const { canvas = 'view' } = params; + const location = useLocation(); + const { canvas = 'view' } = location.query; const isLoading = useIsSiteEditorLoading(); useAdaptEditorToCanvas( canvas ); const entity = useResolveEditedEntity(); @@ -157,9 +192,11 @@ export default function EditSiteEditor( { isPostsList = false } ) { case 'move-to-trash': case 'delete-post': { - history.push( { - postType: items[ 0 ].type, - } ); + history.navigate( + getListPathForPostType( + postWithTemplate ? context.postType : postType + ) + ); } break; case 'duplicate-post': @@ -182,11 +219,9 @@ export default function EditSiteEditor( { isPostsList = false } ) { { label: __( 'Edit' ), onClick: () => { - history.push( { - postId: newItem.id, - postType: newItem.type, - canvas: 'edit', - } ); + history.navigate( + `/${ newItem.type }/${ newItem.id }?canvas=edit` + ); }, }, ], @@ -196,7 +231,13 @@ export default function EditSiteEditor( { isPostsList = false } ) { break; } }, - [ history, createSuccessNotice ] + [ + postType, + context?.postType, + postWithTemplate, + history, + createSuccessNotice, + ] ); // Replace the title and icon displayed in the DocumentBar when there's an overlay visible. @@ -268,26 +309,20 @@ export default function EditSiteEditor( { isPostsList = false } ) { // come here through `posts list` and are in focus mode editing a template, template part etc.. if ( isPostsList && - params?.focusMode + location.query?.focusMode ) { - history.push( - { - page: 'gutenberg-posts-dashboard', - postType: 'post', - }, - undefined, - { - transition: - 'canvas-mode-view-transition', - } - ); + history.navigate( '/', { + transition: + 'canvas-mode-view-transition', + } ); } else { - history.push( - { - ...params, - canvas: undefined, - }, - undefined, + history.navigate( + getNavigationPath( + location, + postWithTemplate + ? context.postType + : postType + ), { transition: 'canvas-mode-view-transition', diff --git a/packages/edit-site/src/components/editor/use-resolve-edited-entity.js b/packages/edit-site/src/components/editor/use-resolve-edited-entity.js index 4f87373870414..8da076f9f00b7 100644 --- a/packages/edit-site/src/components/editor/use-resolve-edited-entity.js +++ b/packages/edit-site/src/components/editor/use-resolve-edited-entity.js @@ -30,8 +30,23 @@ const postTypesWithoutParentTemplate = [ const authorizedPostTypes = [ 'page', 'post' ]; export function useResolveEditedEntity() { - const { params = {} } = useLocation(); - const { postId, postType } = params; + const { name, params = {}, query } = useLocation(); + const { postId = query?.postId } = params; // Fallback to query param for postId for list view routes. + let postType; + if ( name === 'navigation-item' ) { + postType = NAVIGATION_POST_TYPE; + } else if ( name === 'pattern-item' ) { + postType = PATTERN_TYPES.user; + } else if ( name === 'template-part-item' ) { + postType = TEMPLATE_PART_POST_TYPE; + } else if ( name === 'template-item' || name === 'templates' ) { + postType = TEMPLATE_POST_TYPE; + } else if ( name === 'page-item' || name === 'pages' ) { + postType = 'page'; + } else if ( name === 'post-item' || name === 'posts' ) { + postType = 'post'; + } + const homePage = useSelect( ( select ) => { const { getHomePage } = unlock( select( coreDataStore ) ); return getHomePage(); diff --git a/packages/edit-site/src/components/global-styles-sidebar/index.js b/packages/edit-site/src/components/global-styles-sidebar/index.js index d46346b50dae3..02a29dac5c0b7 100644 --- a/packages/edit-site/src/components/global-styles-sidebar/index.js +++ b/packages/edit-site/src/components/global-styles-sidebar/index.js @@ -28,8 +28,8 @@ const { interfaceStore } = unlock( editorPrivateApis ); const { useLocation } = unlock( routerPrivateApis ); export default function GlobalStylesSidebar() { - const { params } = useLocation(); - const { canvas = 'view', path } = params; + const { query } = useLocation(); + const { canvas = 'view', name } = query; const { shouldClearCanvasContainerView, isStyleBookOpened, @@ -133,14 +133,14 @@ export default function GlobalStylesSidebar() { const previousActiveAreaRef = useRef( null ); useEffect( () => { - if ( path?.startsWith( '/wp_global_styles' ) && canvas === 'edit' ) { + if ( name === 'styles' && canvas === 'edit' ) { previousActiveAreaRef.current = getActiveComplementaryArea( 'core' ); enableComplementaryArea( 'core', 'edit-site/global-styles' ); } else if ( previousActiveAreaRef.current ) { enableComplementaryArea( 'core', previousActiveAreaRef.current ); } - }, [ path, enableComplementaryArea, canvas, getActiveComplementaryArea ] ); + }, [ name, enableComplementaryArea, canvas, getActiveComplementaryArea ] ); return ( { - const { postType, path, categoryType, ...rest } = params; - - if ( path === '/wp_template_part/all' ) { - history.replace( { postType: TEMPLATE_PART_POST_TYPE } ); - } - - if ( path === '/page' ) { - history.replace( { - postType: 'page', - ...rest, - } ); - } - - if ( path === '/wp_template' ) { - history.replace( { - postType: TEMPLATE_POST_TYPE, - ...rest, - } ); - } - - if ( path === '/patterns' ) { - history.replace( { - postType: - categoryType === TEMPLATE_PART_POST_TYPE - ? TEMPLATE_PART_POST_TYPE - : PATTERN_TYPES.user, - ...rest, - } ); - } - - if ( path === '/navigation' ) { - history.replace( { - postType: NAVIGATION_POST_TYPE, - ...rest, - } ); - } - }, [ history, params ] ); -} - -export default function useActiveRoute() { - const { params } = useLocation(); - useRedirectOldPaths(); - const routes = useSelect( ( select ) => { - return unlock( select( editSiteStore ) ).getRoutes(); - }, [] ); - return useMemo( () => { - const matchedRoute = routes.find( ( route ) => route.match( params ) ); - if ( ! matchedRoute ) { - return { - key: 404, - areas: {}, - widths: {}, - }; - } - - return { - name: matchedRoute.name, - areas: matchedRoute.areas, - widths: matchedRoute.widths, - }; - }, [ routes, params ] ); -} diff --git a/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js b/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js index d87737c55326c..ca7bbf2fa7322 100644 --- a/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js +++ b/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js @@ -59,10 +59,9 @@ export default function DeleteCategoryMenuItem( { category, onClose } ) { ); onClose?.(); - history.push( { - postType: PATTERN_TYPES.user, - categoryId: PATTERN_DEFAULT_CATEGORY, - } ); + history.navigate( + `/pattern?categoryId=${ PATTERN_DEFAULT_CATEGORY }` + ); } catch ( error ) { const errorMessage = error.message && error.code !== 'unknown_error' diff --git a/packages/edit-site/src/components/page-patterns/fields.js b/packages/edit-site/src/components/page-patterns/fields.js index e016dca6cd855..60e37844b2edb 100644 --- a/packages/edit-site/src/components/page-patterns/fields.js +++ b/packages/edit-site/src/components/page-patterns/fields.js @@ -21,6 +21,7 @@ import { import { Icon, lockSmall } from '@wordpress/icons'; import { parse } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -32,10 +33,10 @@ import { OPERATOR_IS, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; -import { useLink } from '../routes/link'; import { useAddedBy } from '../page-templates/hooks'; import { defaultGetTitle } from './search-items'; +const { useLink } = unlock( routerPrivateApis ); const { useGlobalStyle } = unlock( blockEditorPrivateApis ); function PreviewWrapper( { item, onClick, ariaDescribedBy, children } ) { @@ -59,11 +60,11 @@ function PreviewField( { item } ) { const isUserPattern = item.type === PATTERN_TYPES.user; const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; const [ backgroundColor ] = useGlobalStyle( 'color.background' ); - const { onClick } = useLink( { - postType: item.type, - postId: isUserPattern || isTemplatePart ? item.id : item.name, - canvas: 'edit', - } ); + const { onClick } = useLink( + `/${ item.type }/${ + isUserPattern || isTemplatePart ? item.id : item.name + }?canvas=edit` + ); const blocks = useMemo( () => { return ( item.blocks ?? @@ -114,11 +115,11 @@ export const previewField = { function TitleField( { item } ) { const isUserPattern = item.type === PATTERN_TYPES.user; const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; - const { onClick } = useLink( { - postType: item.type, - postId: isUserPattern || isTemplatePart ? item.id : item.name, - canvas: 'edit', - } ); + const { onClick } = useLink( + `/${ item.type }/${ + isUserPattern || isTemplatePart ? item.id : item.name + }?canvas=edit` + ); const title = decodeEntities( defaultGetTitle( item ) ); return ( diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 69ebf66093806..b675333488757 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -72,17 +72,16 @@ const DEFAULT_VIEW = { export default function DataviewsPatterns() { const { - params: { postType, categoryId: categoryIdFromURL }, + query: { postType = 'wp_block', categoryId: categoryIdFromURL }, } = useLocation(); - const type = postType || PATTERN_TYPES.user; const categoryId = categoryIdFromURL || PATTERN_DEFAULT_CATEGORY; const [ view, setView ] = useState( DEFAULT_VIEW ); const previousCategoryId = usePrevious( categoryId ); - const previousPostType = usePrevious( type ); + const previousPostType = usePrevious( postType ); const viewSyncStatus = view.filters?.find( ( { field } ) => field === 'sync-status' )?.value; - const { patterns, isResolving } = usePatterns( type, categoryId, { + const { patterns, isResolving } = usePatterns( postType, categoryId, { search: view.search, syncStatus: viewSyncStatus, } ); @@ -108,9 +107,9 @@ export default function DataviewsPatterns() { const fields = useMemo( () => { const _fields = [ previewField, titleField ]; - if ( type === PATTERN_TYPES.user ) { + if ( postType === PATTERN_TYPES.user ) { _fields.push( patternStatusField ); - } else if ( type === TEMPLATE_PART_POST_TYPE ) { + } else if ( postType === TEMPLATE_PART_POST_TYPE ) { _fields.push( { ...templatePartAuthorField, elements: authors, @@ -118,24 +117,27 @@ export default function DataviewsPatterns() { } return _fields; - }, [ type, authors ] ); + }, [ postType, authors ] ); // Reset the page number when the category changes. useEffect( () => { - if ( previousCategoryId !== categoryId || previousPostType !== type ) { + if ( + previousCategoryId !== categoryId || + previousPostType !== postType + ) { setView( ( prevView ) => ( { ...prevView, page: 1 } ) ); } - }, [ categoryId, previousCategoryId, previousPostType, type ] ); + }, [ categoryId, previousCategoryId, previousPostType, postType ] ); const { data, paginationInfo } = useMemo( () => { // Search is managed server-side as well as filters for patterns. // However, the author filter in template parts is done client-side. const viewWithoutFilters = { ...view }; delete viewWithoutFilters.search; - if ( type !== TEMPLATE_PART_POST_TYPE ) { + if ( postType !== TEMPLATE_PART_POST_TYPE ) { viewWithoutFilters.filters = []; } return filterSortAndPaginate( patterns, viewWithoutFilters, fields ); - }, [ patterns, view, fields, type ] ); + }, [ patterns, view, fields, postType ] ); const dataWithPermissions = useAugmentPatternsWithPermissions( data ); @@ -150,11 +152,11 @@ export default function DataviewsPatterns() { const editAction = useEditPostAction(); const actions = useMemo( () => { - if ( type === TEMPLATE_PART_POST_TYPE ) { + if ( postType === TEMPLATE_PART_POST_TYPE ) { return [ editAction, ...templatePartActions ].filter( Boolean ); } return [ editAction, ...patternActions ].filter( Boolean ); - }, [ editAction, type, templatePartActions, patternActions ] ); + }, [ editAction, postType, templatePartActions, patternActions ] ); const id = useId(); const settings = usePatternSettings(); // Wrap everything in a block editor provider. @@ -169,7 +171,7 @@ export default function DataviewsPatterns() { > diff --git a/packages/edit-site/src/components/page-templates/fields.js b/packages/edit-site/src/components/page-templates/fields.js index 69e0596bf49d4..35d7b9714d5be 100644 --- a/packages/edit-site/src/components/page-templates/fields.js +++ b/packages/edit-site/src/components/page-templates/fields.js @@ -16,16 +16,16 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { EditorProvider } from '@wordpress/editor'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import { default as Link, useLink } from '../routes/link'; import { useAddedBy } from './hooks'; - import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; +const { useLink, Link } = unlock( routerPrivateApis ); const { useGlobalStyle } = unlock( blockEditorPrivateApis ); function PreviewField( { item } ) { @@ -34,11 +34,7 @@ function PreviewField( { item } ) { const blocks = useMemo( () => { return parse( item.content.raw ); }, [ item.content.raw ] ); - const { onClick } = useLink( { - postId: item.id, - postType: item.type, - canvas: 'edit', - } ); + const { onClick } = useLink( `/${ item.type }/${ item.id }?canvas=edit` ); const isEmpty = ! blocks?.length; // Wrap everything in a block editor provider to ensure 'styles' that are needed @@ -80,15 +76,8 @@ export const previewField = { }; function TitleField( { item } ) { - const linkProps = { - params: { - postId: item.id, - postType: item.type, - canvas: 'edit', - }, - }; return ( - + { decodeEntities( item.title?.rendered ) || __( '(no title)' ) } ); diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index ea026ca53566e..828867e6f283d 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -7,6 +7,7 @@ import { privateApis as corePrivateApis } from '@wordpress/core-data'; import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -93,8 +94,8 @@ const DEFAULT_VIEW = { }; export default function PageTemplates() { - const { params } = useLocation(); - const { activeView = 'all', layout, postId } = params; + const { path, query } = useLocation(); + const { activeView = 'all', layout, postId } = query; const [ selection, setSelection ] = useState( [ postId ] ); const defaultView = useMemo( () => { @@ -118,8 +119,10 @@ export default function PageTemplates() { }, [ layout, activeView ] ); const [ view, setView ] = useState( defaultView ); useEffect( () => { + const usedType = layout ?? DEFAULT_VIEW.type; setView( ( currentView ) => ( { ...currentView, + type: usedType, filters: activeView !== 'all' ? [ @@ -131,7 +134,7 @@ export default function PageTemplates() { ] : [], } ) ); - }, [ activeView ] ); + }, [ activeView, layout ] ); const { records, isResolving: isLoadingData } = useEntityRecordsWithPermissions( 'postType', TEMPLATE_POST_TYPE, { @@ -142,13 +145,14 @@ export default function PageTemplates() { ( items ) => { setSelection( items ); if ( view?.type === LAYOUT_LIST ) { - history.push( { - ...params, - postId: items.length === 1 ? items[ 0 ] : undefined, - } ); + history.navigate( + addQueryArgs( path, { + postId: items.length === 1 ? items[ 0 ] : undefined, + } ) + ); } }, - [ history, params, view?.type ] + [ history, path, view?.type ] ); const authors = useMemo( () => { @@ -195,15 +199,16 @@ export default function PageTemplates() { const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { - history.push( { - ...params, - layout: newView.type, - } ); + history.navigate( + addQueryArgs( path, { + layout: newView.type, + } ) + ); } setView( newView ); }, - [ view.type, setView, history, params ] + [ view.type, setView, history, path ] ); return ( diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 975809b2ad610..d58ddbe50758c 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -13,6 +13,8 @@ import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; import { drawerRight } from '@wordpress/icons'; +import { usePrevious } from '@wordpress/compose'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -31,7 +33,6 @@ import { import AddNewPostModal from '../add-new-post'; import { unlock } from '../../lock-unlock'; import { useEditPostAction } from '../dataviews-actions'; -import { usePrevious } from '@wordpress/compose'; const { usePostActions, usePostFields } = unlock( editorPrivateApis ); const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -70,7 +71,8 @@ const getCustomView = ( editedEntityRecord ) => { */ function useView( postType ) { const { - params: { activeView = 'all', isCustom = 'false', layout }, + path, + query: { activeView = 'all', isCustom = 'false', layout }, } = useLocation(); const history = useHistory(); @@ -112,16 +114,15 @@ function useView( postType ) { const setViewWithUrlUpdate = useCallback( ( newView ) => { - const { params } = history.getLocationWithParams(); - - if ( newView.type === LAYOUT_LIST && ! params?.layout ) { + if ( newView.type === LAYOUT_LIST && ! layout ) { // Skip updating the layout URL param if // it is not present and the newView.type is LAYOUT_LIST. - } else if ( newView.type !== params?.layout ) { - history.push( { - ...params, - layout: newView.type, - } ); + } else if ( newView.type !== layout ) { + history.navigate( + addQueryArgs( path, { + layout: newView.type, + } ) + ); } setView( newView ); @@ -137,7 +138,14 @@ function useView( postType ) { ); } }, - [ history, isCustom, editEntityRecord, editedEntityRecord?.id ] + [ + history, + isCustom, + editEntityRecord, + editedEntityRecord?.id, + layout, + path, + ] ); // When layout URL param changes, update the view type @@ -186,20 +194,20 @@ export default function PostList( { postType } ) { quickEdit = false, isCustom, activeView = 'all', - } = location.params; + } = location.query; const [ selection, setSelection ] = useState( postId?.split( ',' ) ?? [] ); const onChangeSelection = useCallback( ( items ) => { setSelection( items ); - const { params } = history.getLocationWithParams(); - if ( ( params.isCustom ?? 'false' ) === 'false' ) { - history.push( { - ...params, - postId: items.join( ',' ), - } ); + if ( ( location.query.isCustom ?? 'false' ) === 'false' ) { + history.navigate( + addQueryArgs( location.path, { + postId: items.join( ',' ), + } ) + ); } }, - [ history ] + [ location.path, location.query.isCustom, history ] ); const getActiveViewFilters = ( views, match ) => { @@ -311,12 +319,13 @@ export default function PostList( { postType } ) { useEffect( () => { if ( postIdWasDeleted ) { - history.push( { - ...history.getLocationWithParams().params, - postId: undefined, - } ); + history.navigate( + addQueryArgs( location.path, { + postId: undefined, + } ) + ); } - }, [ postIdWasDeleted, history ] ); + }, [ history, postIdWasDeleted, location.path ] ); const paginationInfo = useMemo( () => ( { @@ -355,11 +364,7 @@ export default function PostList( { postType } ) { const openModal = () => setShowAddPostModal( true ); const closeModal = () => setShowAddPostModal( false ); const handleNewPage = ( { type, id } ) => { - history.push( { - postId: id, - postType: type, - canvas: 'edit', - } ); + history.navigate( `/${ type }/${ id }?canvas=edit` ); closeModal(); }; @@ -401,11 +406,7 @@ export default function PostList( { postType } ) { onChangeSelection={ onChangeSelection } isItemClickable={ ( item ) => item.status !== 'trash' } onClickItem={ ( { id } ) => { - history.push( { - postId: id, - postType, - canvas: 'edit', - } ); + history.navigate( `/${ postType }/${ id }?canvas=edit` ); } } getItemId={ getItemId } defaultLayouts={ defaultLayouts } @@ -419,10 +420,11 @@ export default function PostList( { postType } ) { icon={ drawerRight } label={ __( 'Details' ) } onClick={ () => { - history.push( { - ...location.params, - quickEdit: quickEdit ? undefined : true, - } ); + history.navigate( + addQueryArgs( location.path, { + quickEdit: quickEdit ? undefined : true, + } ) + ); } } /> ) diff --git a/packages/edit-site/src/components/posts-app-routes/home.js b/packages/edit-site/src/components/posts-app-routes/home.js deleted file mode 100644 index ec99cbd8899f1..0000000000000 --- a/packages/edit-site/src/components/posts-app-routes/home.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * WordPress dependencies - */ -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import Editor from '../editor'; -import SidebarNavigationScreenMain from '../sidebar-navigation-screen-main'; -import { unlock } from '../../lock-unlock'; - -const { useLocation } = unlock( routerPrivateApis ); - -function HomeMobileView() { - const { params = {} } = useLocation(); - const { canvas = 'view' } = params; - - return canvas === 'edit' ? ( - - ) : ( - - ); -} - -export const homeRoute = { - name: 'home', - match: () => { - return true; - }, - areas: { - sidebar: , - preview: , - mobile: HomeMobileView, - }, -}; diff --git a/packages/edit-site/src/components/posts-app-routes/index.js b/packages/edit-site/src/components/posts-app-routes/index.js index e850bbd382200..3919ea3930d07 100644 --- a/packages/edit-site/src/components/posts-app-routes/index.js +++ b/packages/edit-site/src/components/posts-app-routes/index.js @@ -9,21 +9,10 @@ import { useEffect } from '@wordpress/element'; */ import { unlock } from '../../lock-unlock'; import { store as siteEditorStore } from '../../store'; -import { homeRoute } from './home'; -import { postsListViewQuickEditRoute } from './posts-list-view-quick-edit'; -import { postsListViewRoute } from './posts-list-view'; -import { postsViewQuickEditRoute } from './posts-view-quick-edit'; -import { postsViewRoute } from './posts-view'; -import { postsEditRoute } from './posts-edit'; +import { postsRoute } from './posts'; +import { postItemRoute } from './post-item'; -const routes = [ - postsListViewQuickEditRoute, - postsListViewRoute, - postsViewQuickEditRoute, - postsViewRoute, - postsEditRoute, - homeRoute, -]; +const routes = [ postItemRoute, postsRoute ]; export function useRegisterPostsAppRoutes() { const registry = useRegistry(); diff --git a/packages/edit-site/src/components/posts-app-routes/posts-edit.js b/packages/edit-site/src/components/posts-app-routes/post-item.js similarity index 62% rename from packages/edit-site/src/components/posts-app-routes/posts-edit.js rename to packages/edit-site/src/components/posts-app-routes/post-item.js index d395824559541..54131814f1ae2 100644 --- a/packages/edit-site/src/components/posts-app-routes/posts-edit.js +++ b/packages/edit-site/src/components/posts-app-routes/post-item.js @@ -6,25 +6,21 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import PostList from '../post-list'; +import Editor from '../editor'; import DataViewsSidebarContent from '../sidebar-dataviews'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import Editor from '../editor'; -export const postsEditRoute = { - name: 'posts-edit', - match: ( params ) => { - return params.postType === 'post' && params.canvas === 'edit'; - }, +export const postItemRoute = { + name: 'post-item', + path: '/post/:postId', areas: { sidebar: ( } + content={ } /> ), - content: , mobile: , preview: , }, diff --git a/packages/edit-site/src/components/posts-app-routes/posts-list-view-quick-edit.js b/packages/edit-site/src/components/posts-app-routes/posts-list-view-quick-edit.js deleted file mode 100644 index d2434b390ffd9..0000000000000 --- a/packages/edit-site/src/components/posts-app-routes/posts-list-view-quick-edit.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import { unlock } from '../../lock-unlock'; -import { PostEdit } from '../post-edit'; -import Editor from '../editor'; - -const { useLocation } = unlock( routerPrivateApis ); - -function PostQuickEdit() { - const { params } = useLocation(); - return ; -} - -export const postsListViewQuickEditRoute = { - name: 'posts-list-view-quick-edit', - match: ( params ) => { - return ( - params.isCustom !== 'true' && - ( params.layout ?? 'list' ) === 'list' && - !! params.quickEdit && - params.postType === 'post' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - mobile: , - preview: , - edit: , - }, - widths: { - content: 380, - edit: 380, - }, -}; diff --git a/packages/edit-site/src/components/posts-app-routes/posts-list-view.js b/packages/edit-site/src/components/posts-app-routes/posts-list-view.js deleted file mode 100644 index 68aa86c7fb239..0000000000000 --- a/packages/edit-site/src/components/posts-app-routes/posts-list-view.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import Editor from '../editor'; - -export const postsListViewRoute = { - name: 'posts-list-view', - match: ( params ) => { - return ( - params.isCustom !== 'true' && - ( params.layout ?? 'list' ) === 'list' && - ! params.quickEdit && - params.postType === 'post' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - preview: , - mobile: , - }, - widths: { - content: 380, - }, -}; diff --git a/packages/edit-site/src/components/posts-app-routes/posts-view-quick-edit.js b/packages/edit-site/src/components/posts-app-routes/posts-view-quick-edit.js deleted file mode 100644 index 52e6f9a2d26ef..0000000000000 --- a/packages/edit-site/src/components/posts-app-routes/posts-view-quick-edit.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import { unlock } from '../../lock-unlock'; -import { PostEdit } from '../post-edit'; - -const { useLocation } = unlock( routerPrivateApis ); - -function PostQuickEdit() { - const { params } = useLocation(); - return ; -} - -export const postsViewQuickEditRoute = { - name: 'posts-view-quick-edit', - match: ( params ) => { - return ( - ( params.isCustom === 'true' || - ( params.layout ?? 'list' ) !== 'list' ) && - !! params.quickEdit && - params.postType === 'post' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - mobile: , - edit: , - }, - widths: { - edit: 380, - }, -}; diff --git a/packages/edit-site/src/components/posts-app-routes/posts-view.js b/packages/edit-site/src/components/posts-app-routes/posts-view.js deleted file mode 100644 index 6559991475d27..0000000000000 --- a/packages/edit-site/src/components/posts-app-routes/posts-view.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; - -export const postsViewRoute = { - name: 'posts-view', - match: ( params ) => { - return ( - ( params.isCustom === 'true' || - ( params.layout ?? 'list' ) !== 'list' ) && - ! params.quickEdit && - params.postType === 'post' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/posts-app-routes/posts.js b/packages/edit-site/src/components/posts-app-routes/posts.js new file mode 100644 index 0000000000000..80af8a75fbc80 --- /dev/null +++ b/packages/edit-site/src/components/posts-app-routes/posts.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreen from '../sidebar-navigation-screen'; +import DataViewsSidebarContent from '../sidebar-dataviews'; +import PostList from '../post-list'; +import { unlock } from '../../lock-unlock'; +import { PostEdit } from '../post-edit'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobilePostsView() { + const { query = {} } = useLocation(); + const { canvas = 'view' } = query; + + return canvas === 'edit' ? : ; +} + +export const postsRoute = { + name: 'posts', + path: '/', + areas: { + sidebar: ( + } + /> + ), + content: , + preview( { query } ) { + const isListView = + ( query.layout === 'list' || ! query.layout ) && + query.isCustom !== 'true'; + return isListView ? : undefined; + }, + mobile: , + edit( { query } ) { + const hasQuickEdit = + ( query.layout ?? 'list' ) === 'list' && !! query.quickEdit; + return hasQuickEdit ? ( + + ) : undefined; + }, + }, + widths: { + content( { query } ) { + const isListView = + ( query.layout === 'list' || ! query.layout ) && + query.isCustom !== 'true'; + return isListView ? 380 : undefined; + }, + edit( { query } ) { + const hasQuickEdit = + ( query.layout ?? 'list' ) === 'list' && !! query.quickEdit; + return hasQuickEdit ? 380 : undefined; + }, + }, +}; diff --git a/packages/edit-site/src/components/posts-app/index.js b/packages/edit-site/src/components/posts-app/index.js index e6eb90c168001..ab8cfab99f762 100644 --- a/packages/edit-site/src/components/posts-app/index.js +++ b/packages/edit-site/src/components/posts-app/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -9,20 +10,18 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import Layout from '../layout'; import { useRegisterPostsAppRoutes } from '../posts-app-routes'; import { unlock } from '../../lock-unlock'; -import useActiveRoute from '../layout/router'; +import { store as editSiteStore } from '../../store'; const { RouterProvider } = unlock( routerPrivateApis ); -function PostsLayout() { - useRegisterPostsAppRoutes(); - const route = useActiveRoute(); - return ; -} - export default function PostsApp() { + useRegisterPostsAppRoutes(); + const routes = useSelect( ( select ) => { + return unlock( select( editSiteStore ) ).getRoutes(); + }, [] ); return ( - - + + ); } diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js index 95ccfe4fdd966..99f650971112f 100644 --- a/packages/edit-site/src/components/resizable-frame/index.js +++ b/packages/edit-site/src/components/resizable-frame/index.js @@ -20,6 +20,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ import { unlock } from '../../lock-unlock'; +import { addQueryArgs } from '@wordpress/url'; const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -88,8 +89,8 @@ function ResizableFrame( { innerContentStyle, } ) { const history = useHistory(); - const { params } = useLocation(); - const { canvas = 'view' } = params; + const { path, query } = useLocation(); + const { canvas = 'view' } = query; const disableMotion = useReducedMotion(); const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE ); // The width of the resizable frame when a new resize gesture starts. @@ -158,12 +159,10 @@ function ResizableFrame( { setFrameSize( INITIAL_FRAME_SIZE ); } else { // Trigger full screen if the frame is resized far enough to the left. - history.push( - { - ...params, + history.navigate( + addQueryArgs( path, { canvas: 'edit', - }, - undefined, + } ), { transition: 'canvas-mode-edit-transition', } diff --git a/packages/edit-site/src/components/routes/link.js b/packages/edit-site/src/components/routes/link.js deleted file mode 100644 index a34b37943a079..0000000000000 --- a/packages/edit-site/src/components/routes/link.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * WordPress dependencies - */ -import { addQueryArgs, getQueryArgs, removeQueryArgs } from '@wordpress/url'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; -import { - isPreviewingTheme, - currentlyPreviewingTheme, -} from '../../utils/is-previewing-theme'; - -const { useHistory } = unlock( routerPrivateApis ); - -export function useLink( params, state, shouldReplace = false ) { - const history = useHistory(); - function onClick( event ) { - event?.preventDefault(); - - if ( shouldReplace ) { - history.replace( params, state ); - } else { - history.push( params, state ); - } - } - - const currentArgs = getQueryArgs( window.location.href ); - const currentUrlWithoutArgs = removeQueryArgs( - window.location.href, - ...Object.keys( currentArgs ) - ); - - let extraParams = {}; - if ( isPreviewingTheme() ) { - extraParams = { - wp_theme_preview: currentlyPreviewingTheme(), - }; - } - - const newUrl = addQueryArgs( currentUrlWithoutArgs, { - ...params, - ...extraParams, - } ); - - return { - href: newUrl, - onClick, - }; -} - -export default function Link( { - params = {}, - state, - replace: shouldReplace = false, - children, - ...props -} ) { - const { href, onClick } = useLink( params, state, shouldReplace ); - - return ( - - { children } - - ); -} diff --git a/packages/edit-site/src/components/save-panel/index.js b/packages/edit-site/src/components/save-panel/index.js index b77e5a9a1a10b..81a0f99557df0 100644 --- a/packages/edit-site/src/components/save-panel/index.js +++ b/packages/edit-site/src/components/save-panel/index.js @@ -90,8 +90,8 @@ const _EntitiesSavedStates = ( { onClose, renderDialog = undefined } ) => { }; export default function SavePanel() { - const { params } = useLocation(); - const { canvas = 'view' } = params; + const { query } = useLocation(); + const { canvas = 'view' } = query; const { isSaveViewOpen, isDirty, isSaving } = useSelect( ( select ) => { const { __experimentalGetDirtyEntityRecords, diff --git a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js index 62956ccd18960..815de181a9dde 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js +++ b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js @@ -14,6 +14,7 @@ import { store as coreStore } from '@wordpress/core-data'; import { useState } from '@wordpress/element'; import { plus } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -22,10 +23,11 @@ import SidebarNavigationItem from '../sidebar-navigation-item'; import { useDefaultViews } from './default-views'; import { unlock } from '../../lock-unlock'; -const { useHistory } = unlock( routerPrivateApis ); +const { useLocation, useHistory } = unlock( routerPrivateApis ); function AddNewItemModalContent( { type, setIsAdding } ) { const history = useHistory(); + const { path } = useLocation(); const { saveEntityRecord } = useDispatch( coreStore ); const [ title, setTitle ] = useState( '' ); const [ isSaving, setIsSaving ] = useState( false ); @@ -64,14 +66,12 @@ function AddNewItemModalContent( { type, setIsAdding } ) { content: JSON.stringify( defaultViews[ 0 ].view ), } ); - const { - params: { postType }, - } = history.getLocationWithParams(); - history.push( { - postType, - activeView: savedRecord.id, - isCustom: 'true', - } ); + history.navigate( + addQueryArgs( path, { + activeView: savedRecord.id, + isCustom: 'true', + } ) + ); setIsSaving( false ); setIsAdding( false ); } } diff --git a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js index 1e12d6706d81b..b98f8b80938d6 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js +++ b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js @@ -9,11 +9,11 @@ import clsx from 'clsx'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { __experimentalHStack as HStack } from '@wordpress/components'; import { VIEW_LAYOUTS } from '@wordpress/dataviews'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { useLink } from '../routes/link'; import SidebarNavigationItem from '../sidebar-navigation-item'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); @@ -28,9 +28,7 @@ export default function DataViewItem( { isCustom, suffix, } ) { - const { - params: { postType }, - } = useLocation(); + const { path } = useLocation(); const iconToUse = icon || VIEW_LAYOUTS.find( ( v ) => v.type === type ).icon; @@ -39,12 +37,11 @@ export default function DataViewItem( { if ( activeView === 'all' ) { activeView = undefined; } - const linkInfo = useLink( { - postType, + const query = { layout: type, activeView, isCustom: isCustom ? 'true' : undefined, - } ); + }; return ( { title } diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js index 86420c4eec1d1..410767650c6f3 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -14,9 +14,9 @@ import CustomDataViewsList from './custom-dataviews-list'; const { useLocation } = unlock( routerPrivateApis ); -export default function DataViewsSidebarContent() { +export default function DataViewsSidebarContent( { postType } ) { const { - params: { postType, activeView = 'all', isCustom = 'false' }, + query: { activeView = 'all', isCustom = 'false' }, } = useLocation(); const defaultViews = useDefaultViews( { postType } ); if ( ! postType ) { diff --git a/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js b/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js index 342fb1b5db52d..980f20c49821b 100644 --- a/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js +++ b/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js @@ -9,6 +9,7 @@ import { Button, privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -21,7 +22,6 @@ import { STYLE_BOOK_COLOR_GROUPS } from '../style-book/constants'; const { useLocation, useHistory } = unlock( routerPrivateApis ); const { Menu } = unlock( componentsPrivateApis ); -const GLOBAL_STYLES_PATH_PREFIX = '/wp_global_styles'; const GlobalStylesPageActions = ( { isStyleBookOpened, @@ -63,28 +63,23 @@ const GlobalStylesPageActions = ( { }; export default function GlobalStylesUIWrapper() { - const { params } = useLocation(); + const { path, query } = useLocation(); const history = useHistory(); - const { canvas = 'view' } = params; + const { canvas = 'view' } = query; const [ isStyleBookOpened, setIsStyleBookOpened ] = useState( false ); const isMobileViewport = useViewportMatch( 'medium', '<' ); - const pathWithPrefix = params.path; - const [ path, onPathChange ] = useMemo( () => { - const processedPath = pathWithPrefix.substring( - GLOBAL_STYLES_PATH_PREFIX.length - ); + const [ section, onChangeSection ] = useMemo( () => { return [ - processedPath ? processedPath : '/', - ( newPath ) => { - history.push( { - path: - ! newPath || newPath === '/' - ? GLOBAL_STYLES_PATH_PREFIX - : `${ GLOBAL_STYLES_PATH_PREFIX }${ newPath }`, - } ); + query.section ?? '/', + ( updatedSection ) => { + history.navigate( + addQueryArgs( path, { + section: updatedSection, + } ) + ); }, ]; - }, [ pathWithPrefix, history ] ); + }, [ path, query.section, history ] ); return ( <> @@ -100,7 +95,10 @@ export default function GlobalStylesUIWrapper() { className="edit-site-styles" title={ __( 'Styles' ) } > - + { canvas === 'view' && isStyleBookOpened && ( { if ( STYLE_BOOK_COLOR_GROUPS.find( @@ -129,17 +123,17 @@ export default function GlobalStylesUIWrapper() { ) ) { // Go to color palettes Global Styles. - onPathChange( '/colors/palette' ); + onChangeSection( '/colors/palette' ); return; } if ( blockName === 'typography' ) { // Go to typography Global Styles. - onPathChange( '/typography' ); + onChangeSection( '/typography' ); return; } // Now go to the selected block. - onPathChange( + onChangeSection( `/blocks/${ encodeURIComponent( blockName ) }` ); } } diff --git a/packages/edit-site/src/components/sidebar-navigation-item/index.js b/packages/edit-site/src/components/sidebar-navigation-item/index.js index 80f06d7e93133..4bde94dcbbeb4 100644 --- a/packages/edit-site/src/components/sidebar-navigation-item/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-item/index.js @@ -22,7 +22,7 @@ import { useContext } from '@wordpress/element'; import { unlock } from '../../lock-unlock'; import { SidebarNavigationContext } from '../sidebar'; -const { useHistory } = unlock( routerPrivateApis ); +const { useHistory, useLink } = unlock( routerPrivateApis ); export default function SidebarNavigationItem( { className, @@ -30,7 +30,7 @@ export default function SidebarNavigationItem( { withChevron = false, suffix, uid, - params, + to, onClick, children, ...props @@ -42,12 +42,13 @@ export default function SidebarNavigationItem( { if ( onClick ) { onClick( e ); navigate( 'forward' ); - } else if ( params ) { + } else if ( to ) { e.preventDefault(); - history.push( params ); + history.navigate( to ); navigate( 'forward', `[id="${ uid }"]` ); } } + const linkProps = useLink( to ); return ( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js index 72671714479ac..4023ba436b865 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js @@ -7,6 +7,7 @@ import { store as coreStore } from '@wordpress/core-data'; import { useCallback } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -22,7 +23,7 @@ import { MainSidebarNavigationContent } from '../sidebar-navigation-screen-main' const { useLocation, useHistory } = unlock( routerPrivateApis ); export function SidebarNavigationItemGlobalStyles( props ) { - const { params } = useLocation(); + const { name } = useLocation(); const hasGlobalStyleVariations = useSelect( ( select ) => !! select( @@ -34,11 +35,9 @@ export function SidebarNavigationItemGlobalStyles( props ) { return ( ); } @@ -47,7 +46,7 @@ export function SidebarNavigationItemGlobalStyles( props ) { export default function SidebarNavigationScreenGlobalStyles() { const history = useHistory(); - const { params } = useLocation(); + const { path } = useLocation(); const { revisions, isLoading: isLoadingRevisions, @@ -60,21 +59,14 @@ export default function SidebarNavigationScreenGlobalStyles() { const { set: setPreference } = useDispatch( preferencesStore ); const openGlobalStyles = useCallback( async () => { - history.push( - { - ...params, - canvas: 'edit', - }, - undefined, - { - transition: 'canvas-mode-edit-transition', - } - ); + history.navigate( addQueryArgs( path, { canvas: 'edit' } ), { + transition: 'canvas-mode-edit-transition', + } ); return Promise.all( [ setPreference( 'core', 'distractionFree', false ), openGeneralSidebar( 'edit-site/global-styles' ), ] ); - }, [ history, params, openGeneralSidebar, setPreference ] ); + }, [ path, history, openGeneralSidebar, setPreference ] ); const openRevisions = useCallback( async () => { await openGlobalStyles(); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js index 49e60d4404732..1db651631c53d 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js @@ -15,18 +15,13 @@ import SidebarNavigationItem from '../sidebar-navigation-item'; import { SidebarNavigationItemGlobalStyles } from '../sidebar-navigation-screen-global-styles'; import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; -import { - NAVIGATION_POST_TYPE, - TEMPLATE_POST_TYPE, - PATTERN_TYPES, -} from '../../utils/constants'; export function MainSidebarNavigationContent() { return ( @@ -40,7 +35,7 @@ export function MainSidebarNavigationContent() { @@ -48,7 +43,7 @@ export function MainSidebarNavigationContent() { @@ -56,7 +51,7 @@ export function MainSidebarNavigationContent() { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js index 6b85e088817ed..a07167413ae11 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js @@ -58,11 +58,9 @@ export default function ScreenNavigationMoreMenu( props ) { { - history.push( { - postId: menuId, - postType: 'wp_navigation', - canvas: 'edit', - } ); + history.navigate( + `/wp_navigation/${ menuId }?canvas=edit` + ); } } > { __( 'Edit' ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/use-navigation-menu-handlers.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/use-navigation-menu-handlers.js index 4a7e1deddc6d9..11635c6c6abb1 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/use-navigation-menu-handlers.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/use-navigation-menu-handlers.js @@ -42,7 +42,7 @@ function useDeleteNavigationMenu() { type: 'snackbar', } ); - history.push( { postType: 'wp_navigation' } ); + history.navivate( '/navigation' ); } catch ( error ) { createErrorNotice( sprintf( @@ -165,7 +165,7 @@ function useDuplicateNavigationMenu() { createSuccessNotice( __( 'Duplicated Navigation Menu' ), { type: 'snackbar', } ); - history.push( { postType, postId: savedRecord.id } ); + history.navigate( `/wp_navigation/${ savedRecord.id }` ); } } catch ( error ) { createErrorNotice( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js index ece549f57378b..dc3dd87961184 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js @@ -18,7 +18,6 @@ import { navigation } from '@wordpress/icons'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; import SidebarNavigationItem from '../sidebar-navigation-item'; import { PRELOADED_NAVIGATION_MENUS_QUERY } from './constants'; -import { useLink } from '../routes/link'; import SingleNavigationMenu from '../sidebar-navigation-screen-navigation-menu/single-navigation-menu'; import useNavigationMenuHandlers from '../sidebar-navigation-screen-navigation-menu/use-navigation-menu-handlers'; import { unlock } from '../../lock-unlock'; @@ -152,9 +151,10 @@ export function SidebarNavigationScreenWrapper( { } const NavMenuItem = ( { postId, ...props } ) => { - const linkInfo = useLink( { - postId, - postType: 'wp_navigation', - } ); - return ; + return ( + + ); }; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js index 568ec291f9ed1..ba01faab0291c 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js @@ -20,10 +20,11 @@ const POPOVER_PROPS = { */ import { unlock } from '../../lock-unlock'; -const { useHistory } = unlock( routerPrivateApis ); +const { useHistory, useLocation } = unlock( routerPrivateApis ); export default function LeafMoreMenu( props ) { const history = useHistory(); + const { path } = useLocation(); const { block } = props; const { clientId } = block; const { moveBlocksDown, moveBlocksUp, removeBlocks } = @@ -59,33 +60,20 @@ export default function LeafMoreMenu( props ) { attributes.type && history ) { - const { params } = history.getLocationWithParams(); - history.push( + history.navigate( + `/${ attributes.type }/${ attributes.id }?canvas=edit`, { - postType: attributes.type, - postId: attributes.id, - canvas: 'edit', - }, - { - backPath: params, + state: { backPath: path }, } ); } if ( name === 'core/page-list-item' && attributes.id && history ) { - const { params } = history.getLocationWithParams(); - history.push( - { - postType: 'page', - postId: attributes.id, - canvas: 'edit', - }, - { - backPath: params, - } - ); + history.navigate( `/page/${ attributes.id }?canvas=edit`, { + state: { backPath: path }, + } ); } }, - [ history ] + [ path, history ] ); return ( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js index 9c193304b99fc..4e92af1d84f50 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js @@ -2,13 +2,6 @@ * Internal dependencies */ import SidebarNavigationItem from '../sidebar-navigation-item'; -import { useLink } from '../routes/link'; -import { - TEMPLATE_PART_POST_TYPE, - TEMPLATE_PART_ALL_AREAS_CATEGORY, - PATTERN_DEFAULT_CATEGORY, - PATTERN_TYPES, -} from '../../utils/constants'; export default function CategoryItem( { count, @@ -18,28 +11,20 @@ export default function CategoryItem( { label, type, } ) { - const linkInfo = useLink( { - categoryId: - id !== TEMPLATE_PART_ALL_AREAS_CATEGORY && - id !== PATTERN_DEFAULT_CATEGORY - ? id - : undefined, - postType: - type === TEMPLATE_PART_POST_TYPE - ? TEMPLATE_PART_POST_TYPE - : PATTERN_TYPES.user, - } ); - if ( ! count ) { return; } + const queryArgs = [ `postType=${ type }` ]; + if ( id ) { + queryArgs.push( `categoryId=${ id }` ); + } return ( { count } } aria-current={ isActive ? 'true' : undefined } + to={ `/pattern?${ queryArgs.join( '&' ) }` } > { label } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js index eeec513cb99af..d63389ad3be31 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js @@ -104,12 +104,11 @@ function CategoriesGroup( { export default function SidebarNavigationScreenPatterns( { backPath } ) { const { - params: { postType, categoryId }, + query: { postType = 'wp_block', categoryId }, } = useLocation(); - const currentType = postType || PATTERN_TYPES.user; const currentCategory = categoryId || - ( currentType === PATTERN_TYPES.user + ( postType === PATTERN_TYPES.user ? PATTERN_DEFAULT_CATEGORY : TEMPLATE_PART_ALL_AREAS_CATEGORY ); @@ -143,7 +142,7 @@ export default function SidebarNavigationScreenPatterns( { backPath } ) { templatePartAreas={ templatePartAreas } patternCategories={ patternCategories } currentCategory={ currentCategory } - currentType={ currentType } + currentType={ postType } /> ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js index 5a07adf62d9b3..5d3819eac0ee3 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js @@ -4,35 +4,40 @@ import { useEntityRecords } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import DataViewItem from '../sidebar-dataviews/dataview-item'; +import SidebarNavigationItem from '../sidebar-navigation-item'; import { useAddedBy } from '../page-templates/hooks'; import { layout } from '@wordpress/icons'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import { unlock } from '../../lock-unlock'; + +const { useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; function TemplateDataviewItem( { template, isActive } ) { const { text, icon } = useAddedBy( template.type, template.id ); + return ( - + aria-current={ isActive } + > + { text } + ); } -export default function DataviewsTemplatesSidebarContent( { - activeView, - title, -} ) { +export default function DataviewsTemplatesSidebarContent() { + const { + query: { activeView = 'all' }, + } = useLocation(); const { records } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, } ); @@ -52,13 +57,13 @@ export default function DataviewsTemplatesSidebarContent( { return ( - + aria-current={ activeView === 'all' } + > + { __( 'All templates' ) } + { firstItemPerAuthorText.map( ( template ) => { return ( - } + content={ } /> ); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js index 0080964310525..c6b3742a3fd8b 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js @@ -83,7 +83,7 @@ export default function SidebarNavigationScreen( { { ! isRoot && ( { - history.push( backPath ); + history.navigate( backPath ); navigate( 'back' ); } } icon={ icon } @@ -97,7 +97,7 @@ export default function SidebarNavigationScreen( { label={ dashboardLinkText || __( 'Go to the Dashboard' ) } - href={ dashboardLink || 'index.php' } + href={ dashboardLink } /> ) } { - return params.canvas === 'edit'; - }, - areas: { - sidebar: , - preview: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/home-view.js b/packages/edit-site/src/components/site-editor-routes/home.js similarity index 66% rename from packages/edit-site/src/components/site-editor-routes/home-view.js rename to packages/edit-site/src/components/site-editor-routes/home.js index 63d3d021e8208..3b6230e6b6c38 100644 --- a/packages/edit-site/src/components/site-editor-routes/home-view.js +++ b/packages/edit-site/src/components/site-editor-routes/home.js @@ -4,13 +4,12 @@ import Editor from '../editor'; import SidebarNavigationScreenMain from '../sidebar-navigation-screen-main'; -export const homeViewRoute = { - name: 'home-view', - match: ( params ) => { - return params.canvas !== 'edit'; - }, +export const homeRoute = { + name: 'home', + path: '/', areas: { sidebar: , preview: , + mobile: , }, }; diff --git a/packages/edit-site/src/components/site-editor-routes/index.js b/packages/edit-site/src/components/site-editor-routes/index.js index 4887436dc2ed1..bac2fa302b5cf 100644 --- a/packages/edit-site/src/components/site-editor-routes/index.js +++ b/packages/edit-site/src/components/site-editor-routes/index.js @@ -9,42 +9,30 @@ import { useEffect } from '@wordpress/element'; */ import { unlock } from '../../lock-unlock'; import { store as siteEditorStore } from '../../store'; -import { homeViewRoute } from './home-view'; -import { homeEditRoute } from './home-edit'; -import { navigationViewRoute } from './navigation-view'; -import { navigationEditRoute } from './navigation-edit'; -import { navigationItemEditRoute } from './navigation-item-edit'; -import { navigationItemViewRoute } from './navigation-item-view'; -import { stylesViewRoute } from './styles-view'; -import { patternsEditRoute } from './patterns-edit'; -import { patternsViewRoute } from './patterns-view'; -import { templatesEditRoute } from './templates-edit'; -import { templatesListViewRoute } from './templates-list-view'; -import { templatesViewRoute } from './templates-view'; -import { pagesViewRoute } from './pages-view'; -import { pagesEditRoute } from './pages-edit'; -import { pagesListViewRoute } from './pages-list-view'; -import { pagesListViewQuickEditRoute } from './pages-list-view-quick-edit'; -import { pagesViewQuickEditRoute } from './pages-view-quick-edit'; +import { homeRoute } from './home'; +import { stylesRoute } from './styles'; +import { navigationRoute } from './navigation'; +import { navigationItemRoute } from './navigation-item'; +import { patternsRoute } from './patterns'; +import { patternItemRoute } from './pattern-item'; +import { templatePartItemRoute } from './template-part-item'; +import { templatesRoute } from './templates'; +import { templateItemRoute } from './template-item'; +import { pagesRoute } from './pages'; +import { pageItemRoute } from './page-item'; const routes = [ - pagesListViewQuickEditRoute, - pagesListViewRoute, - pagesViewQuickEditRoute, - pagesViewRoute, - pagesEditRoute, - templatesEditRoute, - templatesListViewRoute, - templatesViewRoute, - patternsViewRoute, - patternsEditRoute, - stylesViewRoute, - navigationItemViewRoute, - navigationItemEditRoute, - navigationViewRoute, - navigationEditRoute, - homeViewRoute, - homeEditRoute, + pageItemRoute, + pagesRoute, + templateItemRoute, + templatesRoute, + templatePartItemRoute, + patternItemRoute, + patternsRoute, + navigationItemRoute, + navigationRoute, + stylesRoute, + homeRoute, ]; export function useRegisterSiteEditorRoutes() { diff --git a/packages/edit-site/src/components/site-editor-routes/navigation-edit.js b/packages/edit-site/src/components/site-editor-routes/navigation-edit.js deleted file mode 100644 index fdba963c41d0c..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/navigation-edit.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Internal dependencies - */ -import { NAVIGATION_POST_TYPE } from '../../utils/constants'; -import Editor from '../editor'; -import SidebarNavigationScreenNavigationMenus from '../sidebar-navigation-screen-navigation-menus'; - -export const navigationEditRoute = { - name: 'navigation-edit', - match: ( params ) => { - return ( - params.postType === NAVIGATION_POST_TYPE && - ! params.postId && - params.canvas === 'edit' - ); - }, - areas: { - sidebar: , - preview: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/navigation-item-edit.js b/packages/edit-site/src/components/site-editor-routes/navigation-item-edit.js deleted file mode 100644 index b03cdbd995ac7..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/navigation-item-edit.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Internal dependencies - */ -import { NAVIGATION_POST_TYPE } from '../../utils/constants'; -import Editor from '../editor'; -import SidebarNavigationScreenNavigationMenu from '../sidebar-navigation-screen-navigation-menu'; - -export const navigationItemEditRoute = { - name: 'navigation-item-edit', - match: ( params ) => { - return ( - params.postType === NAVIGATION_POST_TYPE && - !! params.postId && - params.canvas === 'edit' - ); - }, - areas: { - sidebar: ( - - ), - preview: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/navigation-item-view.js b/packages/edit-site/src/components/site-editor-routes/navigation-item-view.js deleted file mode 100644 index d04a03a8f9df3..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/navigation-item-view.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Internal dependencies - */ -import { NAVIGATION_POST_TYPE } from '../../utils/constants'; -import Editor from '../editor'; -import SidebarNavigationScreenNavigationMenu from '../sidebar-navigation-screen-navigation-menu'; - -export const navigationItemViewRoute = { - name: 'navigation-item-view', - match: ( params ) => { - return ( - params.postType === NAVIGATION_POST_TYPE && - !! params.postId && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - - ), - preview: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/navigation-item.js b/packages/edit-site/src/components/site-editor-routes/navigation-item.js new file mode 100644 index 0000000000000..76983d8ff8daa --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/navigation-item.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { NAVIGATION_POST_TYPE } from '../../utils/constants'; +import Editor from '../editor'; +import SidebarNavigationScreenNavigationMenu from '../sidebar-navigation-screen-navigation-menu'; +import { unlock } from '../../lock-unlock'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobileNavigationItemView() { + const { query = {} } = useLocation(); + const { canvas = 'view' } = query; + + return canvas === 'edit' ? ( + + ) : ( + + ); +} + +export const navigationItemRoute = { + name: 'navigation-item', + path: '/wp_navigation/:postId', + areas: { + sidebar: ( + + ), + preview: , + mobile: , + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/navigation-view.js b/packages/edit-site/src/components/site-editor-routes/navigation-view.js deleted file mode 100644 index 59c38a2f1d099..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/navigation-view.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Internal dependencies - */ -import { NAVIGATION_POST_TYPE } from '../../utils/constants'; -import Editor from '../editor'; -import SidebarNavigationScreenNavigationMenus from '../sidebar-navigation-screen-navigation-menus'; - -export const navigationViewRoute = { - name: 'navigation-view', - match: ( params ) => { - return ( - params.postType === NAVIGATION_POST_TYPE && - ! params.postId && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: , - preview: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/navigation.js b/packages/edit-site/src/components/site-editor-routes/navigation.js new file mode 100644 index 0000000000000..4c435e78a495f --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/navigation.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreenNavigationMenus from '../sidebar-navigation-screen-navigation-menus'; +import { unlock } from '../../lock-unlock'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobileNavigationView() { + const { query = {} } = useLocation(); + const { canvas = 'view' } = query; + + return canvas === 'edit' ? ( + + ) : ( + + ); +} + +export const navigationRoute = { + name: 'navigation', + path: '/navigation', + areas: { + sidebar: , + preview: , + mobile: , + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/pages-edit.js b/packages/edit-site/src/components/site-editor-routes/page-item.js similarity index 54% rename from packages/edit-site/src/components/site-editor-routes/pages-edit.js rename to packages/edit-site/src/components/site-editor-routes/page-item.js index ef4c7efbfb09c..c20720316b10e 100644 --- a/packages/edit-site/src/components/site-editor-routes/pages-edit.js +++ b/packages/edit-site/src/components/site-editor-routes/page-item.js @@ -6,29 +6,21 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import PostList from '../post-list'; +import Editor from '../editor'; import DataViewsSidebarContent from '../sidebar-dataviews'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import Editor from '../editor'; -function PageList() { - return ; -} - -export const pagesEditRoute = { - name: 'pages-edit', - match: ( params ) => { - return params.postType === 'page' && params.canvas === 'edit'; - }, +export const pageItemRoute = { + name: 'page-item', + path: '/page/:postId', areas: { sidebar: ( } + backPath="/" + content={ } /> ), - content: , mobile: , preview: , }, diff --git a/packages/edit-site/src/components/site-editor-routes/pages-list-view-quick-edit.js b/packages/edit-site/src/components/site-editor-routes/pages-list-view-quick-edit.js deleted file mode 100644 index 9eb33e05a99bb..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/pages-list-view-quick-edit.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import { unlock } from '../../lock-unlock'; -import { PostEdit } from '../post-edit'; -import Editor from '../editor'; - -const { useLocation } = unlock( routerPrivateApis ); - -function PageList() { - return ; -} - -function PageQuickEdit() { - const { params } = useLocation(); - return ; -} - -export const pagesListViewQuickEditRoute = { - name: 'pages-list-view-quick-edit', - match: ( params ) => { - return ( - params.isCustom !== 'true' && - ( params.layout ?? 'list' ) === 'list' && - !! params.quickEdit && - params.postType === 'page' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - mobile: , - preview: , - edit: , - }, - widths: { - content: 380, - edit: 380, - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/pages-list-view.js b/packages/edit-site/src/components/site-editor-routes/pages-list-view.js deleted file mode 100644 index 74b39848e83f2..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/pages-list-view.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import Editor from '../editor'; - -function PageList() { - return ; -} - -export const pagesListViewRoute = { - name: 'pages-list-view', - match: ( params ) => { - return ( - params.isCustom !== 'true' && - ( params.layout ?? 'list' ) === 'list' && - ! params.quickEdit && - params.postType === 'page' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - preview: , - mobile: , - }, - widths: { - content: 380, - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/pages-view-quick-edit.js b/packages/edit-site/src/components/site-editor-routes/pages-view-quick-edit.js deleted file mode 100644 index 907054364c8a9..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/pages-view-quick-edit.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import { unlock } from '../../lock-unlock'; -import { PostEdit } from '../post-edit'; - -const { useLocation } = unlock( routerPrivateApis ); - -function PageList() { - return ; -} - -function PageQuickEdit() { - const { params } = useLocation(); - return ; -} - -export const pagesViewQuickEditRoute = { - name: 'pages-view-quick-edit', - match: ( params ) => { - return ( - ( params.isCustom === 'true' || - ( params.layout ?? 'list' ) !== 'list' ) && - !! params.quickEdit && - params.postType === 'page' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - mobile: , - edit: , - }, - widths: { - edit: 380, - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/pages-view.js b/packages/edit-site/src/components/site-editor-routes/pages-view.js deleted file mode 100644 index df7e211022cac..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/pages-view.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import PostList from '../post-list'; -import DataViewsSidebarContent from '../sidebar-dataviews'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; - -function PageList() { - return ; -} - -export const pagesViewRoute = { - name: 'pages-view', - match: ( params ) => { - return ( - ( params.isCustom === 'true' || - ( params.layout ?? 'list' ) !== 'list' ) && - ! params.quickEdit && - params.postType === 'page' && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: ( - } - /> - ), - content: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/pages.js b/packages/edit-site/src/components/site-editor-routes/pages.js new file mode 100644 index 0000000000000..e8c55cd10307e --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/pages.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreen from '../sidebar-navigation-screen'; +import DataViewsSidebarContent from '../sidebar-dataviews'; +import PostList from '../post-list'; +import { unlock } from '../../lock-unlock'; +import { PostEdit } from '../post-edit'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobilePagesView() { + const { query = {} } = useLocation(); + const { canvas = 'view' } = query; + + return canvas === 'edit' ? : ; +} + +export const pagesRoute = { + name: 'pages', + path: '/page', + areas: { + sidebar: ( + } + /> + ), + content: , + preview( { query } ) { + const isListView = + ( query.layout === 'list' || ! query.layout ) && + query.isCustom !== 'true'; + return isListView ? : undefined; + }, + mobile: , + edit( { query } ) { + const hasQuickEdit = + ( query.layout ?? 'list' ) === 'list' && !! query.quickEdit; + return hasQuickEdit ? ( + + ) : undefined; + }, + }, + widths: { + content( { query } ) { + const isListView = + ( query.layout === 'list' || ! query.layout ) && + query.isCustom !== 'true'; + return isListView ? 380 : undefined; + }, + edit( { query } ) { + const hasQuickEdit = + ( query.layout ?? 'list' ) === 'list' && !! query.quickEdit; + return hasQuickEdit ? 380 : undefined; + }, + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/pattern-item.js b/packages/edit-site/src/components/site-editor-routes/pattern-item.js new file mode 100644 index 0000000000000..c4cbcf871f368 --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/pattern-item.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; + +export const patternItemRoute = { + name: 'pattern-item', + path: '/wp_block/:postId', + areas: { + sidebar: , + mobile: , + preview: , + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/patterns-edit.js b/packages/edit-site/src/components/site-editor-routes/patterns-edit.js deleted file mode 100644 index eaf1fd6802018..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/patterns-edit.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Internal dependencies - */ -import Editor from '../editor'; -import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; -import PagePatterns from '../page-patterns'; -import { PATTERN_TYPES, TEMPLATE_PART_POST_TYPE } from '../../utils/constants'; - -export const patternsEditRoute = { - name: 'patterns-edit', - match: ( params ) => { - return ( - [ TEMPLATE_PART_POST_TYPE, PATTERN_TYPES.user ].includes( - params.postType - ) && params.canvas === 'edit' - ); - }, - areas: { - sidebar: , - content: , - mobile: , - preview: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/patterns-view.js b/packages/edit-site/src/components/site-editor-routes/patterns-view.js deleted file mode 100644 index 468f7f14abc13..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/patterns-view.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Internal dependencies - */ -import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; -import PagePatterns from '../page-patterns'; -import { PATTERN_TYPES, TEMPLATE_PART_POST_TYPE } from '../../utils/constants'; - -export const patternsViewRoute = { - name: 'patterns-view', - match: ( params ) => { - return ( - [ TEMPLATE_PART_POST_TYPE, PATTERN_TYPES.user ].includes( - params.postType - ) && params.canvas !== 'edit' - ); - }, - areas: { - sidebar: , - content: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/patterns.js b/packages/edit-site/src/components/site-editor-routes/patterns.js new file mode 100644 index 0000000000000..48207cfe1c1d2 --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/patterns.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; +import PagePatterns from '../page-patterns'; + +export const patternsRoute = { + name: 'patterns', + path: '/pattern', + areas: { + sidebar: , + content: , + mobile: , + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/styles-view.js b/packages/edit-site/src/components/site-editor-routes/styles.js similarity index 58% rename from packages/edit-site/src/components/site-editor-routes/styles-view.js rename to packages/edit-site/src/components/site-editor-routes/styles.js index cc9411eb8144c..17e4a3c064d02 100644 --- a/packages/edit-site/src/components/site-editor-routes/styles-view.js +++ b/packages/edit-site/src/components/site-editor-routes/styles.js @@ -5,18 +5,12 @@ import Editor from '../editor'; import SidebarNavigationScreenGlobalStyles from '../sidebar-navigation-screen-global-styles'; import GlobalStylesUIWrapper from '../sidebar-global-styles-wrapper'; -export const stylesViewRoute = { - name: 'styles-view', - match: ( params ) => { - return ( - params.path && - params.path.startsWith( '/wp_global_styles' ) && - params.canvas !== 'edit' - ); - }, +export const stylesRoute = { + name: 'styles', + path: '/styles', areas: { content: , - sidebar: , + sidebar: , preview: , mobile: , }, diff --git a/packages/edit-site/src/components/site-editor-routes/template-item.js b/packages/edit-site/src/components/site-editor-routes/template-item.js new file mode 100644 index 0000000000000..8ad3ab2b69990 --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/template-item.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen-templates-browse'; + +export const templateItemRoute = { + name: 'template-item', + path: '/wp_template/*postId', + areas: { + sidebar: , + mobile: , + preview: , + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/template-part-item.js b/packages/edit-site/src/components/site-editor-routes/template-part-item.js new file mode 100644 index 0000000000000..a2b21cf23f808 --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/template-part-item.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; + +export const templatePartItemRoute = { + name: 'template-part-item', + path: '/wp_template_part/*postId', + areas: { + sidebar: , + mobile: , + preview: , + }, +}; diff --git a/packages/edit-site/src/components/site-editor-routes/templates-edit.js b/packages/edit-site/src/components/site-editor-routes/templates-edit.js deleted file mode 100644 index 488e9decc1888..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/templates-edit.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Internal dependencies - */ -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; -import PageTemplates from '../page-templates'; -import Editor from '../editor'; -import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen-templates-browse'; - -export const templatesEditRoute = { - name: 'templates-edit', - match: ( params ) => { - return ( - params.postType === TEMPLATE_POST_TYPE && params.canvas === 'edit' - ); - }, - areas: { - sidebar: , - content: , - mobile: , - preview: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/templates-list-view.js b/packages/edit-site/src/components/site-editor-routes/templates-list-view.js deleted file mode 100644 index 7cdda1b13c0b4..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/templates-list-view.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Internal dependencies - */ -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; -import PageTemplates from '../page-templates'; -import Editor from '../editor'; -import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen-templates-browse'; - -export const templatesListViewRoute = { - name: 'templates-list-view', - match: ( params ) => { - return ( - params.isCustom !== 'true' && - params.layout === 'list' && - params.postType === TEMPLATE_POST_TYPE && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: , - content: , - mobile: , - preview: , - }, - widths: { - content: 380, - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/templates-view.js b/packages/edit-site/src/components/site-editor-routes/templates-view.js deleted file mode 100644 index 40fd88c0e60a6..0000000000000 --- a/packages/edit-site/src/components/site-editor-routes/templates-view.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Internal dependencies - */ -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; -import PageTemplates from '../page-templates'; -import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen-templates-browse'; - -export const templatesViewRoute = { - name: 'templates-view', - match: ( params ) => { - return ( - ( params.isCustom === 'true' || params.layout !== 'list' ) && - params.postType === TEMPLATE_POST_TYPE && - params.canvas !== 'edit' - ); - }, - areas: { - sidebar: , - content: , - mobile: , - }, -}; diff --git a/packages/edit-site/src/components/site-editor-routes/templates.js b/packages/edit-site/src/components/site-editor-routes/templates.js new file mode 100644 index 0000000000000..06ba07fcd0659 --- /dev/null +++ b/packages/edit-site/src/components/site-editor-routes/templates.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import Editor from '../editor'; +import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen-templates-browse'; +import { unlock } from '../../lock-unlock'; +import PageTemplates from '../page-templates'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobileTemplatesView() { + const { query = {} } = useLocation(); + const { canvas = 'view' } = query; + + return canvas === 'edit' ? ( + + ) : ( + + ); +} + +export const templatesRoute = { + name: 'templates', + path: '/template', + areas: { + sidebar: , + content: , + preview( { query } ) { + const isListView = query.layout === 'list'; + return isListView ? : undefined; + }, + mobile: , + }, + widths: { + content( { query } ) { + const isListView = query.layout === 'list'; + return isListView ? 380 : undefined; + }, + }, +}; diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index 9e57034bfe73a..91324356d0197 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -39,8 +39,7 @@ const SiteHub = memo( const { getEntityRecord } = select( coreStore ); const _site = getEntityRecord( 'root', 'site' ); return { - dashboardLink: - getSettings().__experimentalDashboardLink || 'index.php', + dashboardLink: getSettings().__experimentalDashboardLink, homeUrl: getEntityRecord( 'root', '__unstableBase' )?.home, siteTitle: ! _site?.title && !! _site?.url @@ -129,9 +128,7 @@ export const SiteHubMobile = memo( select( coreStore ); const _site = getEntityRecord( 'root', 'site' ); return { - dashboardLink: - getSettings().__experimentalDashboardLink || - 'index.php', + dashboardLink: getSettings().__experimentalDashboardLink, isBlockTheme: getCurrentTheme()?.is_block_theme, homeUrl: getEntityRecord( 'root', '__unstableBase' )?.home, siteTitle: @@ -170,7 +167,7 @@ export const SiteHubMobile = memo( } : { onClick: () => { - history.push( {} ); + history.navigate( '/' ); navigate( 'back' ); }, label: __( 'Go to Site Editor' ), diff --git a/packages/edit-site/src/hooks/commands/use-common-commands.js b/packages/edit-site/src/hooks/commands/use-common-commands.js index 3e87f8721e116..34ddae3e1af7a 100644 --- a/packages/edit-site/src/hooks/commands/use-common-commands.js +++ b/packages/edit-site/src/hooks/commands/use-common-commands.js @@ -49,27 +49,17 @@ const getGlobalStylesOpenStylesCommands = () => label: __( 'Open styles' ), callback: ( { close } ) => { close(); - if ( ! params.postId ) { - history.push( { - path: '/wp_global_styles', - canvas: 'edit', + if ( canvas !== 'edit' ) { + history.navigate( '/styles?canvas=edit', { + transition: 'canvas-mode-edit-transition', } ); } - if ( params.postId && canvas !== 'edit' ) { - history.push( - { ...params, canvas: 'edit' }, - undefined, - { - transition: 'canvas-mode-edit-transition', - } - ); - } openGeneralSidebar( 'edit-site/global-styles' ); }, icon: styles, }, ]; - }, [ history, openGeneralSidebar, params, canvas, isBlockBasedTheme ] ); + }, [ history, openGeneralSidebar, canvas, isBlockBasedTheme ] ); return { isLoading: false, @@ -100,24 +90,11 @@ const getGlobalStylesToggleWelcomeGuideCommands = () => label: __( 'Learn about styles' ), callback: ( { close } ) => { close(); - if ( ! params.postId ) { - history.push( { - path: '/wp_global_styles', - canvas: 'edit', + if ( canvas !== 'edit' ) { + history.navigate( '/styles?canvas=edit', { + transition: 'canvas-mode-edit-transition', } ); } - if ( params.postId && canvas !== 'edit' ) { - history.push( - { - ...params, - canvas: 'edit', - }, - undefined, - { - transition: 'canvas-mode-edit-transition', - } - ); - } openGeneralSidebar( 'edit-site/global-styles' ); set( 'core/edit-site', 'welcomeGuideStyles', true ); // sometimes there's a focus loss that happens after some time @@ -129,14 +106,7 @@ const getGlobalStylesToggleWelcomeGuideCommands = () => icon: help, }, ]; - }, [ - history, - openGeneralSidebar, - canvas, - isBlockBasedTheme, - set, - params, - ] ); + }, [ history, openGeneralSidebar, canvas, isBlockBasedTheme, set ] ); return { isLoading: false, @@ -205,24 +175,11 @@ const getGlobalStylesOpenCssCommands = () => icon: brush, callback: ( { close } ) => { close(); - if ( ! params.postId ) { - history.push( { - path: '/wp_global_styles', - canvas: 'edit', + if ( canvas !== 'edit' ) { + history.navigate( '/styles?canvas=edit', { + transition: 'canvas-mode-edit-transition', } ); } - if ( params.postId && canvas !== 'edit' ) { - history.push( - { - ...params, - canvas: 'edit', - }, - undefined, - { - transition: 'canvas-mode-edit-transition', - } - ); - } openGeneralSidebar( 'edit-site/global-styles' ); setEditorCanvasContainerView( 'global-styles-css' ); }, @@ -234,7 +191,6 @@ const getGlobalStylesOpenCssCommands = () => setEditorCanvasContainerView, canEditCSS, canvas, - params, ] ); return { isLoading: false, @@ -272,24 +228,11 @@ const getGlobalStylesOpenRevisionsCommands = () => icon: backup, callback: ( { close } ) => { close(); - if ( ! params.postId ) { - history.push( { - path: '/wp_global_styles', - canvas: 'edit', + if ( canvas !== 'edit' ) { + history.navigate( '/styles?canvas=edit', { + transition: 'canvas-mode-edit-transition', } ); } - if ( params.postId && canvas !== 'edit' ) { - history.push( - { - ...params, - canvas: 'edit', - }, - undefined, - { - transition: 'canvas-mode-edit-transition', - } - ); - } openGeneralSidebar( 'edit-site/global-styles' ); setEditorCanvasContainerView( 'global-styles-revisions' @@ -303,7 +246,6 @@ const getGlobalStylesOpenRevisionsCommands = () => openGeneralSidebar, setEditorCanvasContainerView, canvas, - params, ] ); return { diff --git a/packages/edit-site/src/hooks/commands/use-set-command-context.js b/packages/edit-site/src/hooks/commands/use-set-command-context.js index e27c4ca91582f..6ecdf04989609 100644 --- a/packages/edit-site/src/hooks/commands/use-set-command-context.js +++ b/packages/edit-site/src/hooks/commands/use-set-command-context.js @@ -19,8 +19,8 @@ const { useLocation } = unlock( routerPrivateApis ); * React hook used to set the correct command context based on the current state. */ export default function useSetCommandContext() { - const { params } = useLocation(); - const { canvas = 'view' } = params; + const { query = {} } = useLocation(); + const { canvas = 'view' } = query; const hasBlockSelected = useSelect( ( select ) => { return select( blockEditorStore ).getBlockSelectionStart(); }, [] ); diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index 1db3873acedda..9b16748049cd0 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -18,3 +18,10 @@ export function registerRoute( route ) { route, }; } + +export function unregisterRoute( name ) { + return { + type: 'UNREGISTER_ROUTE', + name, + }; +} diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index 3ce067c25c195..7ffb276a35da1 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -85,6 +85,8 @@ function routes( state = [], action ) { switch ( action.type ) { case 'REGISTER_ROUTE': return [ ...state, action.route ]; + case 'UNREGISTER_ROUTE': + return state.filter( ( route ) => route.name !== action.name ); } return state; diff --git a/packages/edit-site/src/utils/is-previewing-theme.js b/packages/edit-site/src/utils/is-previewing-theme.js index 1a71c441f9925..a4c830b4b60ad 100644 --- a/packages/edit-site/src/utils/is-previewing-theme.js +++ b/packages/edit-site/src/utils/is-previewing-theme.js @@ -4,9 +4,7 @@ import { getQueryArg } from '@wordpress/url'; export function isPreviewingTheme() { - return ( - getQueryArg( window.location.href, 'wp_theme_preview' ) !== undefined - ); + return !! getQueryArg( window.location.href, 'wp_theme_preview' ); } export function currentlyPreviewingTheme() { diff --git a/packages/edit-site/src/utils/use-activate-theme.js b/packages/edit-site/src/utils/use-activate-theme.js index 0dafd88340ba7..447ea07305349 100644 --- a/packages/edit-site/src/utils/use-activate-theme.js +++ b/packages/edit-site/src/utils/use-activate-theme.js @@ -4,6 +4,7 @@ import { store as coreStore } from '@wordpress/core-data'; import { useDispatch } from '@wordpress/data'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -14,7 +15,7 @@ import { currentlyPreviewingTheme, } from './is-previewing-theme'; -const { useHistory } = unlock( routerPrivateApis ); +const { useHistory, useLocation } = unlock( routerPrivateApis ); /** * This should be refactored to use the REST API, once the REST API can activate themes. @@ -23,6 +24,7 @@ const { useHistory } = unlock( routerPrivateApis ); */ export function useActivateTheme() { const history = useHistory(); + const { path } = useLocation(); const { startResolution, finishResolution } = useDispatch( coreStore ); return async () => { @@ -37,8 +39,7 @@ export function useActivateTheme() { finishResolution( 'activateTheme' ); // Remove the wp_theme_preview query param: we've finished activating // the queue and are switching to normal Site Editor. - const { params } = history.getLocationWithParams(); - history.replace( { ...params, wp_theme_preview: undefined } ); + history.navigate( addQueryArgs( path, { wp_theme_preview: '' } ) ); } }; } diff --git a/packages/router/package.json b/packages/router/package.json index 26b4f29df04f6..66a64f4ddc5ba 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -29,10 +29,12 @@ "types": "build-types", "dependencies": { "@babel/runtime": "7.25.7", + "@wordpress/compose": "*", "@wordpress/element": "*", "@wordpress/private-apis": "*", "@wordpress/url": "*", - "history": "^5.3.0" + "history": "^5.3.0", + "route-recognizer": "^0.3.4" }, "peerDependencies": { "react": "^18.0.0" diff --git a/packages/router/src/history.ts b/packages/router/src/history.ts deleted file mode 100644 index 6cbef108eec20..0000000000000 --- a/packages/router/src/history.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * External dependencies - */ -import { createBrowserHistory, type BrowserHistory } from 'history'; - -/** - * WordPress dependencies - */ -import { buildQueryString } from '@wordpress/url'; - -export interface EnhancedHistory extends BrowserHistory { - getLocationWithParams: () => Location; -} - -interface PushOptions { - transition?: string; -} - -const history = createBrowserHistory(); - -const originalHistoryPush = history.push; -const originalHistoryReplace = history.replace; - -// Preserve the `wp_theme_preview` query parameter when navigating -// around the Site Editor. -// TODO: move this hack out of the router into Site Editor code. -function preserveThemePreview( params: Record< string, any > ) { - if ( params.hasOwnProperty( 'wp_theme_preview' ) ) { - return params; - } - const currentSearch = new URLSearchParams( history.location.search ); - const currentThemePreview = currentSearch.get( 'wp_theme_preview' ); - if ( currentThemePreview === null ) { - return params; - } - return { ...params, wp_theme_preview: currentThemePreview }; -} - -function push( - params: Record< string, any >, - state: Record< string, any >, - options: PushOptions = {} -) { - const performPush = () => { - const search = buildQueryString( preserveThemePreview( params ) ); - return originalHistoryPush.call( history, { search }, state ); - }; - - /* - * Skip transition in mobile, otherwise it crashes the browser. - * See: https://github.com/WordPress/gutenberg/pull/63002. - */ - const isMediumOrBigger = window.matchMedia( '(min-width: 782px)' ).matches; - if ( - ! isMediumOrBigger || - // @ts-expect-error - ! document.startViewTransition || - ! options.transition - ) { - return performPush(); - } - document.documentElement.classList.add( options.transition ); - // @ts-expect-error - const transition = document.startViewTransition( () => performPush() ); - transition.finished.finally( () => { - document.documentElement.classList.remove( options.transition ?? '' ); - } ); -} - -function replace( - params: Record< string, any >, - state: Record< string, any > -) { - const search = buildQueryString( preserveThemePreview( params ) ); - return originalHistoryReplace.call( history, { search }, state ); -} - -const locationMemo = new WeakMap(); -function getLocationWithParams() { - const location = history.location; - let locationWithParams = locationMemo.get( location ); - if ( ! locationWithParams ) { - locationWithParams = { - ...location, - params: Object.fromEntries( - new URLSearchParams( location.search ) - ), - }; - locationMemo.set( location, locationWithParams ); - } - return locationWithParams; -} - -export default { - ...history, - push, - replace, - getLocationWithParams, -}; diff --git a/packages/router/src/link.tsx b/packages/router/src/link.tsx new file mode 100644 index 0000000000000..d312a9da14460 --- /dev/null +++ b/packages/router/src/link.tsx @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +import { useContext, useMemo } from '@wordpress/element'; +import { getQueryArgs, getPath, buildQueryString } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { ConfigContext, type NavigationOptions, useHistory } from './router'; + +export function useLink( to: string, options: NavigationOptions = {} ) { + const history = useHistory(); + const { pathArg, beforeNavigate } = useContext( ConfigContext ); + function onClick( event: React.SyntheticEvent< HTMLAnchorElement > ) { + event?.preventDefault(); + history.navigate( to, options ); + } + const query = getQueryArgs( to ); + const path = getPath( 'http://domain.com/' + to ) ?? ''; + const link = useMemo( () => { + return beforeNavigate + ? beforeNavigate( { path, query } ) + : { path, query }; + }, [ path, query, beforeNavigate ] ); + + const [ before ] = window.location.href.split( '?' ); + + return { + href: `${ before }?${ buildQueryString( { + [ pathArg ]: link.path, + ...link.query, + } ) }`, + onClick, + }; +} + +export function Link( { + to, + options, + children, + ...props +}: { + to: string; + options?: NavigationOptions; + children: React.ReactNode; +} ) { + const { href, onClick } = useLink( to, options ); + + return ( + + { children } + + ); +} diff --git a/packages/router/src/private-apis.ts b/packages/router/src/private-apis.ts index 7b2945a24ab1a..9ef316ed716cf 100644 --- a/packages/router/src/private-apis.ts +++ b/packages/router/src/private-apis.ts @@ -2,6 +2,7 @@ * Internal dependencies */ import { useHistory, useLocation, RouterProvider } from './router'; +import { useLink, Link } from './link'; import { lock } from './lock-unlock'; export const privateApis = {}; @@ -9,4 +10,6 @@ lock( privateApis, { useHistory, useLocation, RouterProvider, + useLink, + Link, } ); diff --git a/packages/router/src/router.tsx b/packages/router/src/router.tsx index 9a1d01aa5f8d8..ea0b218fa6a40 100644 --- a/packages/router/src/router.tsx +++ b/packages/router/src/router.tsx @@ -1,3 +1,9 @@ +/** + * External dependencies + */ +import RouteRecognizer from 'route-recognizer'; +import { createBrowserHistory } from 'history'; + /** * WordPress dependencies */ @@ -5,37 +11,227 @@ import { createContext, useContext, useSyncExternalStore, + useMemo, } from '@wordpress/element'; +import { + addQueryArgs, + getQueryArgs, + getPath, + buildQueryString, +} from '@wordpress/url'; +import { useEvent } from '@wordpress/compose'; /** * Internal dependencies */ -import history from './history'; -import type { EnhancedHistory } from './history'; +import type { ReactNode } from 'react'; + +const history = createBrowserHistory(); +interface Route { + name: string; + path: string; + areas: Record< string, ReactNode >; + widths: Record< string, number >; +} + +type LocationWithQuery = Location & { + query?: Record< string, any >; +}; + +interface Match { + name: string; + path: string; + areas: Record< string, ReactNode >; + widths: Record< string, number >; + query?: Record< string, any >; + params?: Record< string, any >; +} + +export type BeforeNavigate = ( arg: { + path: string; + query: Record< string, any >; +} ) => { + path: string; + query: Record< string, any >; +}; + +interface Config { + pathArg: string; + beforeNavigate?: BeforeNavigate; +} + +export interface NavigationOptions { + transition?: string; + state?: Record< string, any >; +} -const RoutesContext = createContext< Location | null >( null ); -const HistoryContext = createContext< EnhancedHistory >( history ); +const RoutesContext = createContext< Match | null >( null ); +export const ConfigContext = createContext< Config >( { pathArg: 'p' } ); + +const locationMemo = new WeakMap(); +function getLocationWithQuery() { + const location = history.location; + let locationWithQuery = locationMemo.get( location ); + if ( ! locationWithQuery ) { + locationWithQuery = { + ...location, + query: Object.fromEntries( new URLSearchParams( location.search ) ), + }; + locationMemo.set( location, locationWithQuery ); + } + return locationWithQuery; +} export function useLocation() { - return useContext( RoutesContext ); + const context = useContext( RoutesContext ); + if ( ! context ) { + throw new Error( 'useLocation must be used within a RouterProvider' ); + } + return context; } export function useHistory() { - return useContext( HistoryContext ); + const { pathArg, beforeNavigate } = useContext( ConfigContext ); + + const navigate = useEvent( + async ( rawPath: string, options: NavigationOptions = {} ) => { + const query = getQueryArgs( rawPath ); + const path = getPath( 'http://domain.com/' + rawPath ) ?? ''; + const performPush = () => { + const result = beforeNavigate + ? beforeNavigate( { path, query } ) + : { path, query }; + return history.push( + { + search: buildQueryString( { + [ pathArg ]: result.path, + ...result.query, + } ), + }, + options.state + ); + }; + + /* + * Skip transition in mobile, otherwise it crashes the browser. + * See: https://github.com/WordPress/gutenberg/pull/63002. + */ + const isMediumOrBigger = + window.matchMedia( '(min-width: 782px)' ).matches; + if ( + ! isMediumOrBigger || + // @ts-expect-error + ! document.startViewTransition || + ! options.transition + ) { + performPush(); + } + + await new Promise< void >( ( resolve ) => { + const classname = options.transition ?? ''; + document.documentElement.classList.add( classname ); + // @ts-expect-error + const transition = document.startViewTransition( () => + performPush() + ); + transition.finished.finally( () => { + document.documentElement.classList.remove( classname ); + resolve(); + } ); + } ); + } + ); + + return useMemo( + () => ( { + navigate, + } ), + [ navigate ] + ); +} + +export default function useMatch( + location: LocationWithQuery, + matcher: RouteRecognizer, + pathArg: string +): Match { + const { query: rawQuery = {} } = location; + + return useMemo( () => { + const { [ pathArg ]: path = '/', ...query } = rawQuery; + const result = matcher.recognize( path )?.[ 0 ]; + if ( ! result ) { + return { + name: '404', + path: addQueryArgs( path, query ), + areas: {}, + widths: {}, + query, + params: {}, + }; + } + + const matchedRoute = result.handler as Route; + const resolveFunctions = ( record: Record< string, any > = {} ) => { + return Object.fromEntries( + Object.entries( record ).map( ( [ key, value ] ) => { + if ( typeof value === 'function' ) { + return [ + key, + value( { query, params: result.params } ), + ]; + } + return [ key, value ]; + } ) + ); + }; + return { + name: matchedRoute.name, + areas: resolveFunctions( matchedRoute.areas ), + widths: resolveFunctions( matchedRoute.widths ), + params: result.params, + query, + path: addQueryArgs( path, query ), + }; + }, [ matcher, rawQuery, pathArg ] ); } -export function RouterProvider( { children }: { children: React.ReactNode } ) { +export function RouterProvider( { + routes, + pathArg, + beforeNavigate, + children, +}: { + routes: Route[]; + pathArg: string; + beforeNavigate?: BeforeNavigate; + children: React.ReactNode; +} ) { const location = useSyncExternalStore( history.listen, - history.getLocationWithParams, - history.getLocationWithParams + getLocationWithQuery, + getLocationWithQuery + ); + const matcher = useMemo( () => { + const ret = new RouteRecognizer(); + routes.forEach( ( route ) => { + ret.add( [ { path: route.path, handler: route } ], { + as: route.name, + } ); + } ); + return ret; + }, [ routes ] ); + const match = useMatch( location, matcher, pathArg ); + const config = useMemo( + () => ( { beforeNavigate, pathArg } ), + [ beforeNavigate, pathArg ] ); return ( - - + + { children } - + ); } diff --git a/packages/router/tsconfig.json b/packages/router/tsconfig.json index e4945eef8bac0..8706b546ff304 100644 --- a/packages/router/tsconfig.json +++ b/packages/router/tsconfig.json @@ -4,11 +4,10 @@ "compilerOptions": { "rootDir": "src", "declarationDir": "build-types", - "types": [ "gutenberg-env" ], - "allowJs": false, - "checkJs": false + "types": [ "gutenberg-env" ] }, "references": [ + { "path": "../compose" }, { "path": "../element" }, { "path": "../private-apis" }, { "path": "../url" } diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 20eff4096cb1c..7069b4cec258a 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -128,7 +128,11 @@ test.describe( 'Pattern Overrides', () => { page.getByRole( 'button', { name: 'Dismiss this notice' } ) ).toBeVisible(); - patternId = new URL( page.url() ).searchParams.get( 'postId' ); + patternId = await page.evaluate( () => { + return window.wp.data + .select( 'core/editor' ) + .getCurrentPostId(); + } ); } ); await test.step( 'Create a post and insert the pattern with overrides', async () => { @@ -1207,7 +1211,11 @@ test.describe( 'Pattern Overrides', () => { page.getByRole( 'button', { name: 'Dismiss this notice' } ) ).toBeVisible(); - patternId = new URL( page.url() ).searchParams.get( 'postId' ); + patternId = await page.evaluate( () => { + return window.wp.data + .select( 'core/editor' ) + .getCurrentPostId(); + } ); } ); await test.step( 'create a post and insert the pattern with synced values', async () => { diff --git a/test/e2e/specs/site-editor/browser-history.spec.js b/test/e2e/specs/site-editor/browser-history.spec.js index eaafb3aad1b3f..a2326d10e3cc5 100644 --- a/test/e2e/specs/site-editor/browser-history.spec.js +++ b/test/e2e/specs/site-editor/browser-history.spec.js @@ -21,13 +21,13 @@ test.describe( 'Site editor browser history', () => { await page.click( 'role=button[name="Templates"]' ); await page.getByRole( 'link', { name: 'Index' } ).click(); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?postId=emptytheme%2F%2Findex&postType=wp_template&canvas=edit' + '/wp-admin/site-editor.php?p=%2Fwp_template%2Femptytheme%2F%2Findex&canvas=edit' ); // Navigate back to the template list await page.goBack(); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?postType=wp_template' + '/wp-admin/site-editor.php?p=%2Ftemplate' ); // Navigate back to the dashboard diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 19318081aa171..197a01c43c8b4 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -28,7 +28,7 @@ test.describe( 'Site editor command palette', () => { await page.keyboard.type( 'new page' ); await page.getByRole( 'option', { name: 'Add new page' } ).click(); await expect( page ).toHaveURL( - /\/wp-admin\/site-editor.php\?postId=(\d+)&postType=page&canvas=edit/ + /\/wp-admin\/site-editor.php\?p=%2Fpage%2F(\d+)&canvas=edit/ ); await expect( editor.canvas diff --git a/test/e2e/specs/site-editor/hybrid-theme.spec.js b/test/e2e/specs/site-editor/hybrid-theme.spec.js index b568aaf4445b5..042cb1042cac2 100644 --- a/test/e2e/specs/site-editor/hybrid-theme.spec.js +++ b/test/e2e/specs/site-editor/hybrid-theme.spec.js @@ -33,7 +33,7 @@ test.describe( 'Hybrid theme', () => { ); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?postType=wp_template_part' + '/wp-admin/site-editor.php?p=%2Fpattern&postType=wp_template_part' ); await expect( diff --git a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js index f26fb8e13b8c3..a0cc0af5463ae 100644 --- a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js +++ b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js @@ -44,7 +44,7 @@ test.describe( 'Site editor url navigation', () => { .click(); await page.getByRole( 'option', { name: 'Demo' } ).click(); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?postId=emptytheme%2F%2Fsingle-post-demo&postType=wp_template&canvas=edit' + '/wp-admin/site-editor.php?p=%2Fwp_template%2Femptytheme%2F%2Fsingle-post-demo&canvas=edit' ); } ); @@ -63,7 +63,7 @@ test.describe( 'Site editor url navigation', () => { await page.type( 'role=dialog >> role=textbox[name="Name"i]', 'Demo' ); await page.keyboard.press( 'Enter' ); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?postId=emptytheme%2F%2Fdemo&postType=wp_template_part&canvas=edit' + '/wp-admin/site-editor.php?p=%2Fwp_template_part%2Femptytheme%2F%2Fdemo&canvas=edit' ); } ); From b54d1fe5fe96b00d7f6455b4aaf40bb21ec43967 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Thu, 28 Nov 2024 08:55:30 +0100 Subject: [PATCH 039/444] Fix Site editor navigation menu items alignment visual regression. (#67321) Co-authored-by: afercia Co-authored-by: ramonjd Co-authored-by: t-hamano Co-authored-by: jameskoster --- .../src/components/sidebar-navigation-screen/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss index 959115e0fac8c..1486e6b725844 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss @@ -18,7 +18,7 @@ .edit-site-sidebar-navigation-screen__content { padding: 0 $grid-unit-20; - .edit-site-sidebar-navigation-screen-details-footer { + .components-item-group { margin-left: -$grid-unit-20; margin-right: -$grid-unit-20; } @@ -131,7 +131,7 @@ margin: $grid-unit-20 0 0; border-top: 1px solid $gray-800; - .components-item-group { + .edit-site-sidebar-navigation-screen-details-footer { margin-left: -$grid-unit-20; margin-right: -$grid-unit-20; } From d64cdabc6a1025ff579c90f4bb5b11aa54965e23 Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Thu, 28 Nov 2024 03:35:18 -0600 Subject: [PATCH 040/444] Data Views: Add action for pages to set site homepage (#65426) * Adds basic action and modal to set page as homepage * Adds permissions and settings checks to set as homepage action * Adds proper description and handles unpublished pages * Adds action to set homepage to show latest posts * Doesn't show action if there's a front-page template * Creates page for posts, when specified * Refactors modal component * Fixes issues from rebase * Only show option on published pages * Update snackbar wording * Check item exists before running getItemTitle logic * Make key optional on GetEntityRecord * Remove some ts-ignore comments * Add support for page_for_posts to Settings * Remove some more ts-ignores * Allow recordId to be optional * Increase size of action modal * Implement choose existing page option * Fix number/string comparison * Add initial e2e test * Set post actions modal to medium * Tweak ToggleGroupControl help text * Fix initial test * Remove extra useSiteSettings hook * Allow setting draft pages to homepage * Fix merge conflict * Remove item check from getItemTitle * Remove posts page options * Don't show homepage option if selected page is the page for posts * Reload actions list when site settings change * Update tests * Remove call to __experimentalGetTemplateForLink * Update tests * Remove item check in getItemTitle * Use useSelect instead of select * Remove PAGE_POST_TYPE constant * Use saveEntityRecord instead of editEntityRecord * Remove onSetLatestPostsHomepage option * Remove select for site settings from isEligible * Update post actions with site settings info * Remove select for templates from isEligible * Skip last test for now * Restore whitespace * Rename _select * Remove sub-objects from additionalContext selectors * Remove duplicate page_for_posts definition * Fix page/post type error * Remove additional groups within additionalContext * Fix siteSettings in TitleView * Move hasFrontPageTemplate check to private-actions * Add JSDoc to setAsHomepage * Refactor siteSettings in post-list * Move homepage action to edit-site * Revert unnecessary changes * Move getItemTitle to edit-site utils * Allow undefined on GetEntityRecord key * Make it more clear that draft page will be published * Update draft page wording * Add set homepage action to post editor * Attempt to fix build error * Remove homepage action from edit-site * Remove extra line * Fix getting current homepage title * Make key in getEntityRecord optional * Use getHomePage selector * Move canManageOptions and hasFrontPageTemplate to actions.js * Make key optional in EntityRecordKey * Remove undefined from getEntityRecord calls * Update packages/editor/src/components/post-actions/actions.js Co-authored-by: Dave Smith * Update getEntityRecord key docs * Refactor fetching currentHomePage * Disable modal buttons if saving * Store isPageDraft in useRef * Fix lint error * Remove onActionPerformed * Fix current homepage test * Remove duplicate getItemTitle function * Update logic for shouldShowSetAsHomepageAction * Swap order of list of actions * Add comment about manual saveEntityRecord call * Remove unnecessary space * Remove temporary modalButtonLabel variable * Combine draft and publish status tests * Only allow action on pages with draft or publish status * Remove handling of draft pages * Move closeModal into finally block * Refactor and remove renderModalBody --------- Co-authored-by: Sarah Norris Co-authored-by: Sarah Norris <1645628+mikachan@users.noreply.github.com> Co-authored-by: Dave Smith Co-authored-by: creativecoder Co-authored-by: mikachan Co-authored-by: ntsekouras Co-authored-by: youknowriad Co-authored-by: jsnajdr Co-authored-by: ellatrix Co-authored-by: oandregal Co-authored-by: getdave Co-authored-by: jameskoster Co-authored-by: richtabor Co-authored-by: ramonjd Co-authored-by: jasmussen Co-authored-by: mtias --- docs/reference-guides/data/data-core.md | 2 +- packages/core-data/README.md | 2 +- packages/core-data/src/private-selectors.ts | 1 - packages/core-data/src/selectors.ts | 8 +- .../dataviews-item-actions/index.tsx | 2 +- .../src/components/post-actions/actions.js | 39 +++- .../src/components/post-actions/index.js | 2 +- .../post-actions/set-as-homepage.js | 174 ++++++++++++++++++ packages/editor/src/dataviews/types.ts | 2 +- packages/fields/src/actions/utils.ts | 2 +- .../fields/src/fields/title/title-view.tsx | 7 +- .../site-editor/homepage-settings.spec.js | 72 ++++++++ 12 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 packages/editor/src/components/post-actions/set-as-homepage.js create mode 100644 test/e2e/specs/site-editor/homepage-settings.spec.js diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 474207aa20460..199c29cd67dd2 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -359,7 +359,7 @@ _Parameters_ - _state_ `State`: State tree - _kind_ `string`: Entity kind. - _name_ `string`: Entity name. -- _key_ `EntityRecordKey`: Record's key +- _key_ `EntityRecordKey`: Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included. - _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]". _Returns_ diff --git a/packages/core-data/README.md b/packages/core-data/README.md index eb6980cdd4eea..9549e6742d8cd 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -581,7 +581,7 @@ _Parameters_ - _state_ `State`: State tree - _kind_ `string`: Entity kind. - _name_ `string`: Entity name. -- _key_ `EntityRecordKey`: Record's key +- _key_ `EntityRecordKey`: Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included. - _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]". _Returns_ diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 02fe152ed0abb..7779051265306 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -151,7 +151,6 @@ export const getHomePage = createRegistrySelector( ( select ) => return { postType: 'wp_template', postId: frontPageTemplateId }; }, ( state ) => [ - // @ts-expect-error getEntityRecord( state, 'root', 'site' ), getDefaultTemplateId( state, { slug: 'front-page', diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 7ea8c2f7f26d5..7f4b0d3884646 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -310,7 +310,7 @@ export interface GetEntityRecord { state: State, kind: string, name: string, - key: EntityRecordKey, + key?: EntityRecordKey, query?: GetRecordsHttpQuery ): EntityRecord | undefined; @@ -321,7 +321,7 @@ export interface GetEntityRecord { >( kind: string, name: string, - key: EntityRecordKey, + key?: EntityRecordKey, query?: GetRecordsHttpQuery ) => EntityRecord | undefined; __unstableNormalizeArgs?: ( args: EntityRecordArgs ) => EntityRecordArgs; @@ -335,7 +335,7 @@ export interface GetEntityRecord { * @param state State tree * @param kind Entity kind. * @param name Entity name. - * @param key Record's key + * @param key Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included. * @param query Optional query. If requesting specific * fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]". * @@ -350,7 +350,7 @@ export const getEntityRecord = createSelector( state: State, kind: string, name: string, - key: EntityRecordKey, + key?: EntityRecordKey, query?: GetRecordsHttpQuery ): EntityRecord | undefined => { const queriedState = diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index 787cef4420acc..b5eaac11bcd8d 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -114,7 +114,7 @@ export function ActionModal< Item >( { __experimentalHideHeader={ !! action.hideModalHeader } onRequestClose={ closeModal ?? ( () => {} ) } focusOnMount="firstContentElement" - size="small" + size="medium" overlayClassName={ `dataviews-action-modal dataviews-action-modal__${ kebabCase( action.id ) }` } diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 8dbe5b9dfcd5a..1b6ff4fbe384b 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -3,12 +3,14 @@ */ import { useDispatch, useSelect } from '@wordpress/data'; import { useMemo, useEffect } from '@wordpress/element'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import { useSetAsHomepageAction } from './set-as-homepage'; export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions } = useSelect( @@ -21,19 +23,46 @@ export function usePostActions( { postType, onActionPerformed, context } ) { [ postType ] ); + const { canManageOptions, hasFrontPageTemplate } = useSelect( + ( select ) => { + const { getEntityRecords } = select( coreStore ); + const templates = getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + } ); + + return { + canManageOptions: select( coreStore ).canUser( 'update', { + kind: 'root', + name: 'site', + } ), + hasFrontPageTemplate: !! templates?.find( + ( template ) => template?.slug === 'front-page' + ), + }; + } + ); + + const setAsHomepageAction = useSetAsHomepageAction(); + const shouldShowSetAsHomepageAction = + canManageOptions && ! hasFrontPageTemplate; + const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); useEffect( () => { registerPostTypeSchema( postType ); }, [ registerPostTypeSchema, postType ] ); return useMemo( () => { + let actions = [ + ...defaultActions, + shouldShowSetAsHomepageAction ? setAsHomepageAction : [], + ]; // Filter actions based on provided context. If not provided // all actions are returned. We'll have a single entry for getting the actions // and the consumer should provide the context to filter the actions, if needed. // Actions should also provide the `context` they support, if it's specific, to // compare with the provided context to get all the actions. // Right now the only supported context is `list`. - const actions = defaultActions.filter( ( action ) => { + actions = actions.filter( ( action ) => { if ( ! action.context ) { return true; } @@ -88,5 +117,11 @@ export function usePostActions( { postType, onActionPerformed, context } ) { } return actions; - }, [ defaultActions, onActionPerformed, context ] ); + }, [ + context, + defaultActions, + onActionPerformed, + setAsHomepageAction, + shouldShowSetAsHomepageAction, + ] ); } diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index 9f39b1f3305ae..ab11b5e318b5a 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -123,7 +123,7 @@ function ActionWithModal( { action, item, ActionTrigger, onClose } ) { action.id ) }` } focusOnMount="firstContentElement" - size="small" + size="medium" > { + const [ item ] = items; + const pageTitle = getItemTitle( item ); + const { showOnFront, currentHomePage, isSaving } = useSelect( + ( select ) => { + const { getEntityRecord, isSavingEntityRecord } = + select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + const currentHomePageItem = getEntityRecord( + 'postType', + 'page', + siteSettings?.page_on_front + ); + return { + showOnFront: siteSettings?.show_on_front, + currentHomePage: currentHomePageItem, + isSaving: isSavingEntityRecord( 'root', 'site' ), + }; + } + ); + const currentHomePageTitle = currentHomePage + ? getItemTitle( currentHomePage ) + : ''; + + const { saveEditedEntityRecord, saveEntityRecord } = + useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function onSetPageAsHomepage( event ) { + event.preventDefault(); + + try { + // Save new home page settings. + await saveEditedEntityRecord( 'root', 'site', undefined, { + page_on_front: item.id, + show_on_front: 'page', + } ); + + // This second call to a save function is a workaround for a bug in + // `saveEditedEntityRecord`. This forces the root site settings to be updated. + // See https://github.com/WordPress/gutenberg/issues/67161. + await saveEntityRecord( 'root', 'site', { + page_on_front: item.id, + show_on_front: 'page', + } ); + + createSuccessNotice( __( 'Homepage updated' ), { + type: 'snackbar', + } ); + } catch ( error ) { + const typedError = error; + const errorMessage = + typedError.message && typedError.code !== 'unknown_error' + ? typedError.message + : __( 'An error occurred while setting the homepage' ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } finally { + closeModal?.(); + } + } + + const modalWarning = + 'posts' === showOnFront + ? __( + 'This will replace the current homepage which is set to display latest posts.' + ) + : sprintf( + // translators: %s: title of the current home page. + __( 'This will replace the current homepage: "%s"' ), + currentHomePageTitle + ); + + const modalText = sprintf( + // translators: %1$s: title of the page to be set as the homepage, %2$s: homepage replacement warning message. + __( 'Set "%1$s" as the site homepage? %2$s' ), + pageTitle, + modalWarning + ); + + // translators: Button label to confirm setting the specified page as the homepage. + const modalButtonLabel = __( 'Set homepage' ); + + return ( +
+ + { modalText } + + + + + +
+ ); +}; + +export const useSetAsHomepageAction = () => { + const { pageOnFront, pageForPosts } = useSelect( ( select ) => { + const { getEntityRecord } = select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + return { + pageOnFront: siteSettings?.page_on_front, + pageForPosts: siteSettings?.page_for_posts, + }; + } ); + + return useMemo( + () => ( { + id: 'set-as-homepage', + label: __( 'Set as homepage' ), + isEligible( post ) { + if ( post.status !== 'publish' ) { + return false; + } + + if ( post.type !== 'page' ) { + return false; + } + + // Don't show the action if the page is already set as the homepage. + if ( pageOnFront === post.id ) { + return false; + } + + // Don't show the action if the page is already set as the page for posts. + if ( pageForPosts === post.id ) { + return false; + } + + return true; + }, + RenderModal: SetAsHomepageModal, + } ), + [ pageForPosts, pageOnFront ] + ); +}; diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 664c2dd417201..4d27fc7dc4139 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -1,5 +1,5 @@ type PostStatus = - | 'published' + | 'publish' | 'draft' | 'pending' | 'private' diff --git a/packages/fields/src/actions/utils.ts b/packages/fields/src/actions/utils.ts index 60d3d00e82766..8f990fb1168fc 100644 --- a/packages/fields/src/actions/utils.ts +++ b/packages/fields/src/actions/utils.ts @@ -30,7 +30,7 @@ export function isTemplateOrTemplatePart( return p.type === TEMPLATE_POST_TYPE || p.type === TEMPLATE_PART_POST_TYPE; } -export function getItemTitle( item: Post ) { +export function getItemTitle( item: Post ): string { if ( typeof item.title === 'string' ) { return decodeEntities( item.title ); } diff --git a/packages/fields/src/fields/title/title-view.tsx b/packages/fields/src/fields/title/title-view.tsx index c15ed96b89b73..f6bf5fb1817d9 100644 --- a/packages/fields/src/fields/title/title-view.tsx +++ b/packages/fields/src/fields/title/title-view.tsx @@ -17,11 +17,10 @@ import { getItemTitle } from '../../actions/utils'; const TitleView = ( { item }: { item: BasePost } ) => { const { frontPageId, postsPageId } = useSelect( ( select ) => { const { getEntityRecord } = select( coreStore ); - const siteSettings: Settings | undefined = getEntityRecord( + const siteSettings = getEntityRecord( 'root', - 'site', - '' - ); + 'site' + ) as Partial< Settings >; return { frontPageId: siteSettings?.page_on_front, postsPageId: siteSettings?.page_for_posts, diff --git a/test/e2e/specs/site-editor/homepage-settings.spec.js b/test/e2e/specs/site-editor/homepage-settings.spec.js new file mode 100644 index 0000000000000..d53130af23ac8 --- /dev/null +++ b/test/e2e/specs/site-editor/homepage-settings.spec.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Homepage Settings via Editor', () => { + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ requestUtils.activateTheme( 'emptytheme' ) ] ); + await requestUtils.createPage( { + title: 'Homepage', + status: 'publish', + } ); + } ); + + test.beforeEach( async ( { admin, page } ) => { + await admin.visitSiteEditor(); + await page.getByRole( 'button', { name: 'Pages' } ).click(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.deleteAllPages(), + requestUtils.updateSiteSettings( { + show_on_front: 'posts', + page_on_front: 0, + page_for_posts: 0, + } ), + ] ); + } ); + + test( 'should show "Set as homepage" action on pages with `publish` status', async ( { + page, + } ) => { + const samplePage = page + .getByRole( 'gridcell' ) + .getByLabel( 'Homepage' ); + const samplePageRow = page + .getByRole( 'row' ) + .filter( { has: samplePage } ); + await samplePageRow.hover(); + await samplePageRow + .getByRole( 'button', { + name: 'Actions', + } ) + .click(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as homepage' } ) + ).toBeVisible(); + } ); + + test( 'should not show "Set as homepage" action on current homepage', async ( { + page, + } ) => { + const samplePage = page + .getByRole( 'gridcell' ) + .getByLabel( 'Homepage' ); + const samplePageRow = page + .getByRole( 'row' ) + .filter( { has: samplePage } ); + await samplePageRow.click(); + await samplePageRow + .getByRole( 'button', { + name: 'Actions', + } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Set as homepage' } ).click(); + await page.getByRole( 'button', { name: 'Set homepage' } ).click(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as homepage' } ) + ).toBeHidden(); + } ); +} ); From 2efb3a9f7c87dae155b71ad520ac5aa00f7490b4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 28 Nov 2024 10:39:35 +0100 Subject: [PATCH 041/444] REST API: Support search_columns argument in the user endpoint (#67330) Co-authored-by: youknowriad Co-authored-by: Mamaduka Co-authored-by: ntsekouras Co-authored-by: mreishus Co-authored-by claudiulodro < claudiulodro@git.wordpress.org> --- backport-changelog/6.8/7909.md | 3 + .../class-gutenberg-rest-user-controller.php | 62 +++++++++++++++++++ lib/load.php | 1 + .../src/components/post-author/constants.js | 2 +- .../editor/src/components/post-author/hook.js | 1 + 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 backport-changelog/6.8/7909.md create mode 100644 lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php diff --git a/backport-changelog/6.8/7909.md b/backport-changelog/6.8/7909.md new file mode 100644 index 0000000000000..32a441ef296a2 --- /dev/null +++ b/backport-changelog/6.8/7909.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7909 + +* https://github.com/WordPress/gutenberg/pull/67330 diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php new file mode 100644 index 0000000000000..c1ecb8c86660c --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php @@ -0,0 +1,62 @@ + array(), + 'description' => __( 'Array of column names to be searched.' ), + 'type' => 'array', + 'items' => array( + 'enum' => array( 'email', 'name', 'id', 'username', 'slug' ), + 'type' => 'string', + ), + ); + + return $query_params; +} + +add_filter( 'rest_user_collection_params', 'gutenberg_add_search_columns_param', 10, 1 ); + +/** + * Modify user query based on search_columns parameter + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The REST API request. + * @return array Modified arguments + */ +function gutenberg_modify_user_query_args( $prepared_args, $request ) { + if ( $request->get_param( 'search' ) && $request->get_param( 'search_columns' ) ) { + $search_columns = $request->get_param( 'search_columns' ); + + // Validate search columns + $valid_columns = isset( $prepared_args['search_columns'] ) + ? $prepared_args['search_columns'] + : array( 'ID', 'user_login', 'user_nicename', 'user_email', 'user_url', 'display_name' ); + $search_columns_mapping = array( + 'id' => 'ID', + 'username' => 'user_login', + 'slug' => 'user_nicename', + 'email' => 'user_email', + 'name' => 'display_name', + ); + $search_columns = array_map( + static function ( $column ) use ( $search_columns_mapping ) { + return $search_columns_mapping[ $column ]; + }, + $search_columns + ); + $search_columns = array_intersect( $search_columns, $valid_columns ); + + if ( ! empty( $search_columns ) ) { + $prepared_args['search_columns'] = $search_columns; + } + } + + return $prepared_args; +} +add_filter( 'rest_user_query', 'gutenberg_modify_user_query_args', 10, 2 ); diff --git a/lib/load.php b/lib/load.php index 97c5404a3a3ea..26af78f3173c5 100644 --- a/lib/load.php +++ b/lib/load.php @@ -99,6 +99,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/functions.php'; require __DIR__ . '/compat/wordpress-6.8/post.php'; require __DIR__ . '/compat/wordpress-6.8/site-editor.php'; +require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/packages/editor/src/components/post-author/constants.js b/packages/editor/src/components/post-author/constants.js index 46bc2d46d1b08..517cbd9ea1dc8 100644 --- a/packages/editor/src/components/post-author/constants.js +++ b/packages/editor/src/components/post-author/constants.js @@ -5,6 +5,6 @@ export const BASE_QUERY = { export const AUTHORS_QUERY = { who: 'authors', - per_page: 50, + per_page: 100, ...BASE_QUERY, }; diff --git a/packages/editor/src/components/post-author/hook.js b/packages/editor/src/components/post-author/hook.js index 62830cf6ea60e..f251eba79e180 100644 --- a/packages/editor/src/components/post-author/hook.js +++ b/packages/editor/src/components/post-author/hook.js @@ -23,6 +23,7 @@ export function useAuthorsQuery( search ) { if ( search ) { query.search = search; + query.search_columns = [ 'name' ]; } return { From a48a414853b7f66606393dd311556e6bdda1aa62 Mon Sep 17 00:00:00 2001 From: Yogesh Bhutkar Date: Thu, 28 Nov 2024 15:43:06 +0530 Subject: [PATCH 042/444] CommentsPagination: Set font-size to inherit for pagination items (#67296) * CommentsPagination: Set font-size to inherit for pagination items * Remove extra spaces --------- Co-authored-by: yogeshbhutkar Co-authored-by: cbravobernal --- packages/block-library/src/comments-pagination/editor.scss | 1 + packages/block-library/src/comments-pagination/style.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/block-library/src/comments-pagination/editor.scss b/packages/block-library/src/comments-pagination/editor.scss index a875c9e0ee21c..3cd99c632ee83 100644 --- a/packages/block-library/src/comments-pagination/editor.scss +++ b/packages/block-library/src/comments-pagination/editor.scss @@ -26,6 +26,7 @@ $pagination-margin: 0.5em; margin-right: $pagination-margin; margin-bottom: $pagination-margin; + font-size: inherit; &:last-child { /*rtl:ignore*/ margin-right: 0; diff --git a/packages/block-library/src/comments-pagination/style.scss b/packages/block-library/src/comments-pagination/style.scss index c6b5d9a0a29e9..2fb6e3dd2d48f 100644 --- a/packages/block-library/src/comments-pagination/style.scss +++ b/packages/block-library/src/comments-pagination/style.scss @@ -8,6 +8,7 @@ $pagination-margin: 0.5em; margin-right: $pagination-margin; margin-bottom: $pagination-margin; + font-size: inherit; &:last-child { /*rtl:ignore*/ margin-right: 0; From f414562bee98285ed512ac4f9dfd6ca0609fa6a6 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:54:14 +0100 Subject: [PATCH 043/444] Block Bindings: Remove client core sources registration in widgets (#67349) * Remove client core sources registration in widgets * Remove dependencies Co-authored-by: SantosGuillamot Co-authored-by: ramonjd Co-authored-by: talldan Co-authored-by: gziolo Co-authored-by: youknowriad --- package-lock.json | 2 -- packages/customize-widgets/package.json | 1 - packages/customize-widgets/src/index.js | 5 ----- packages/edit-widgets/package.json | 1 - packages/edit-widgets/src/index.js | 5 ----- 5 files changed, 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc0c18f3b6ac5..80a64c6f7a04b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53963,7 +53963,6 @@ "@wordpress/core-data": "*", "@wordpress/data": "*", "@wordpress/dom": "*", - "@wordpress/editor": "*", "@wordpress/element": "*", "@wordpress/hooks": "*", "@wordpress/i18n": "*", @@ -54392,7 +54391,6 @@ "@wordpress/data": "*", "@wordpress/deprecated": "*", "@wordpress/dom": "*", - "@wordpress/editor": "*", "@wordpress/element": "*", "@wordpress/hooks": "*", "@wordpress/i18n": "*", diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 4a14ac743b724..10c3b2dfb510d 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -34,7 +34,6 @@ "@wordpress/core-data": "*", "@wordpress/data": "*", "@wordpress/dom": "*", - "@wordpress/editor": "*", "@wordpress/element": "*", "@wordpress/hooks": "*", "@wordpress/i18n": "*", diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index df96d645ee700..5de010fa8bd37 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -17,7 +17,6 @@ import { store as blocksStore, } from '@wordpress/blocks'; import { dispatch } from '@wordpress/data'; -import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { store as preferencesStore } from '@wordpress/preferences'; /** @@ -27,7 +26,6 @@ import CustomizeWidgets from './components/customize-widgets'; import getSidebarSection from './controls/sidebar-section'; import getSidebarControl from './controls/sidebar-control'; import './filters'; -import { unlock } from './lock-unlock'; const { wp } = window; @@ -39,8 +37,6 @@ const DISABLED_BLOCKS = [ ]; const ENABLE_EXPERIMENTAL_FSE_BLOCKS = false; -const { registerCoreBlockBindingsSources } = unlock( editorPrivateApis ); - /** * Initializes the widgets block editor in the customizer. * @@ -64,7 +60,6 @@ export function initialize( editorName, blockEditorSettings ) { ); } ); registerCoreBlocks( coreBlocks ); - registerCoreBlockBindingsSources(); registerLegacyWidgetBlock(); if ( globalThis.IS_GUTENBERG_PLUGIN ) { __experimentalRegisterExperimentalCoreBlocks( { diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index ed375f7430a1a..0528348291481 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -39,7 +39,6 @@ "@wordpress/data": "*", "@wordpress/deprecated": "*", "@wordpress/dom": "*", - "@wordpress/editor": "*", "@wordpress/element": "*", "@wordpress/hooks": "*", "@wordpress/i18n": "*", diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 8788ee2b99ea1..2f0ced0c09bd7 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -9,7 +9,6 @@ import { } from '@wordpress/blocks'; import { dispatch } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; -import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { StrictMode, createRoot } from '@wordpress/element'; import { registerCoreBlocks, @@ -30,7 +29,6 @@ import { store as preferencesStore } from '@wordpress/preferences'; import './store'; import './filters'; import * as widgetArea from './blocks/widget-area'; -import { unlock } from './lock-unlock'; import Layout from './components/layout'; import { ALLOW_REUSABLE_BLOCKS, @@ -44,8 +42,6 @@ const disabledBlocks = [ ...( ALLOW_REUSABLE_BLOCKS ? [] : [ 'core/block' ] ), ]; -const { registerCoreBlockBindingsSources } = unlock( editorPrivateApis ); - /** * Initializes the block editor in the widgets screen. * @@ -75,7 +71,6 @@ export function initializeEditor( id, settings ) { dispatch( blocksStore ).reapplyBlockTypeFilters(); registerCoreBlocks( coreBlocks ); - registerCoreBlockBindingsSources(); registerLegacyWidgetBlock(); if ( globalThis.IS_GUTENBERG_PLUGIN ) { __experimentalRegisterExperimentalCoreBlocks( { From c845cdad4b98b531d8e9924b36aeea2b01709b4f Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:17:30 +0200 Subject: [PATCH 044/444] Router: Fix addition and removal of empty classnames (#67378) * Router: Fix addition and removal of empty classnames * Actually prevent transition Co-authored-by: tyxla Co-authored-by: youknowriad Co-authored-by: jsnajdr --- packages/router/src/router.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/router/src/router.tsx b/packages/router/src/router.tsx index ea0b218fa6a40..34cc542c7b573 100644 --- a/packages/router/src/router.tsx +++ b/packages/router/src/router.tsx @@ -125,6 +125,7 @@ export function useHistory() { ! options.transition ) { performPush(); + return; } await new Promise< void >( ( resolve ) => { From 45d9528de2da7edb3a162d5df82fb646c067e052 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Thu, 28 Nov 2024 15:16:57 +0200 Subject: [PATCH 045/444] DataViews: Update `usePostFields` to accept postType (#67380) Co-authored-by: ntsekouras Co-authored-by: youknowriad Co-authored-by: oandregal --- .../src/components/post-edit/index.js | 2 +- .../src/components/post-list/index.js | 4 +++- .../src/components/post-fields/index.ts | 8 ++++--- .../src/dataviews/store/private-actions.ts | 21 +++++++++---------- packages/editor/src/dataviews/types.ts | 3 +++ 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index a7842f0feb3c2..3e75ef71d1ac9 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -49,7 +49,7 @@ function PostEditForm( { postType, postId } ) { ); const [ multiEdits, setMultiEdits ] = useState( {} ); const { editEntityRecord } = useDispatch( coreDataStore ); - const { fields: _fields } = usePostFields(); + const { fields: _fields } = usePostFields( { postType } ); const fields = useMemo( () => _fields?.map( ( field ) => { diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index d58ddbe50758c..200aa60ee1797 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -215,7 +215,9 @@ export default function PostList( { postType } ) { return found?.filters ?? []; }; - const { isLoading: isLoadingFields, fields: _fields } = usePostFields(); + const { isLoading: isLoadingFields, fields: _fields } = usePostFields( { + postType, + } ); const fields = useMemo( () => { const activeViewFilters = getActiveViewFilters( defaultViews, diff --git a/packages/editor/src/components/post-fields/index.ts b/packages/editor/src/components/post-fields/index.ts index 41b61fe103a70..d701bdef2284e 100644 --- a/packages/editor/src/components/post-fields/index.ts +++ b/packages/editor/src/components/post-fields/index.ts @@ -23,9 +23,11 @@ interface Author { name: string; } -function usePostFields(): UsePostFieldsReturn { - const postType = 'page'; // TODO: this could be page or post (experimental). - +function usePostFields( { + postType, +}: { + postType: string; +} ): UsePostFieldsReturn { const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); useEffect( () => { registerPostTypeSchema( postType ); diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 77ac131a8e230..9e8d184e34d3a 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -128,7 +128,7 @@ export const registerPostTypeSchema = const actions = [ postTypeConfig.viewable ? viewPost : undefined, - !! postTypeConfig?.supports?.revisions + !! postTypeConfig.supports?.revisions ? viewPostRevisions : undefined, // @ts-ignore @@ -148,7 +148,7 @@ export const registerPostTypeSchema = ? duplicatePattern : undefined, postTypeConfig.supports?.title ? renamePost : undefined, - postTypeConfig?.supports?.[ 'page-attributes' ] + postTypeConfig.supports?.[ 'page-attributes' ] ? reorderPage : undefined, postTypeConfig.slug === 'wp_block' ? exportPattern : undefined, @@ -157,25 +157,24 @@ export const registerPostTypeSchema = deletePost, trashPost, permanentlyDeletePost, - ]; + ].filter( Boolean ); const fields = [ - featuredImageField, + postTypeConfig.supports?.thumbnail && + currentTheme?.[ 'theme-supports' ]?.[ 'post-thumbnails' ] && + featuredImageField, titleField, - authorField, + postTypeConfig.supports?.author && authorField, statusField, dateField, slugField, - parentField, - commentStatusField, + postTypeConfig.supports?.[ 'page-attributes' ] && parentField, + postTypeConfig.supports?.comments && commentStatusField, passwordField, - ]; + ].filter( Boolean ); registry.batch( () => { actions.forEach( ( action ) => { - if ( ! action ) { - return; - } unlock( registry.dispatch( editorStore ) ).registerEntityAction( 'postType', postType, diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 4d27fc7dc4139..9549e6c4aa374 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -78,6 +78,9 @@ export interface PostType { 'page-attributes'?: boolean; title?: boolean; revisions?: boolean; + thumbnail?: boolean; + comments?: boolean; + author?: boolean; }; } From daaa785b27c4124c7d39a7ee12bc2b4ce0359aa4 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:05:14 +0200 Subject: [PATCH 046/444] Edit Site: Fix sidebar template author navigation (#67382) Co-authored-by: tyxla Co-authored-by: youknowriad --- .../sidebar-navigation-screen-templates-browse/content.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js index 5d3819eac0ee3..aad38959c73dc 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js @@ -6,6 +6,7 @@ import { useMemo } from '@wordpress/element'; import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -25,7 +26,7 @@ function TemplateDataviewItem( { template, isActive } ) { return ( From 007daf07967134b15a09eb7b9f0e59f7ffa0584e Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:29:40 +0100 Subject: [PATCH 047/444] Only pass `aria-label` when it is not empty (#67381) Co-authored-by: SantosGuillamot Co-authored-by: afercia --- packages/block-library/src/navigation/index.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 68b23aceeced6..9a56e399fcfec 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -567,13 +567,14 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) $is_responsive_menu = static::is_responsive( $attributes ); $style = static::get_styles( $attributes ); $class = static::get_classes( $attributes ); - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => $class, - 'style' => $style, - 'aria-label' => $nav_menu_name, - ) + $extra_attributes = array( + 'class' => $class, + 'style' => $style, ); + if ( ! empty( $nav_menu_name ) ) { + $extra_attributes['aria-label'] = $nav_menu_name; + } + $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes ); if ( $is_responsive_menu ) { $nav_element_directives = static::get_nav_element_directives( $is_interactive ); From 81327d1e18a724758eedbb461bde29dfd8b41b7f Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 28 Nov 2024 15:54:46 +0100 Subject: [PATCH 048/444] Update @ariakit/react to 0.4.13 (#65907) * Remove all ariakit dependencies * Re-add ariakit dependencies targeting latest version * Remove focus-visible DropdownMenuV2 workaround * Remove composite tabbable workaround * CHANGELOG * Remove Tabs workaround --- Co-authored-by: ciampo Co-authored-by: oandregal Co-authored-by: tyxla Co-authored-by: diegohaz Co-authored-by: jsnajdr Co-authored-by: t-hamano --- package-lock.json | 82 ++++++++++++------- package.json | 2 +- packages/components/CHANGELOG.md | 6 +- packages/components/package.json | 2 +- packages/components/src/composite/item.tsx | 20 +---- .../components/src/menu/checkbox-item.tsx | 6 +- packages/components/src/menu/item.tsx | 6 +- packages/components/src/menu/radio-item.tsx | 6 +- .../menu/use-temporary-focus-visible-fix.ts | 22 ----- packages/components/src/tabs/tab.tsx | 18 ---- packages/dataviews/package.json | 2 +- 11 files changed, 63 insertions(+), 109 deletions(-) delete mode 100644 packages/components/src/menu/use-temporary-focus-visible-fix.ts diff --git a/package-lock.json b/package-lock.json index 80a64c6f7a04b..58479ecfa2ed9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@actions/core": "1.9.1", "@actions/github": "5.0.0", "@apidevtools/json-schema-ref-parser": "11.6.4", - "@ariakit/test": "^0.4.2", + "@ariakit/test": "^0.4.5", "@babel/core": "7.25.7", "@babel/plugin-syntax-jsx": "7.25.7", "@babel/runtime-corejs3": "7.25.7", @@ -1432,18 +1432,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ariakit/core": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.9.tgz", - "integrity": "sha512-nV0B/OTK/0iB+P9RC7fudznYZ8eR6rR1F912Zc54e3+wSW5RrRvNOiRxyMrgENidd4R7cCMDw77XJLSBLKgEPQ==" - }, "node_modules/@ariakit/test": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.2.tgz", - "integrity": "sha512-WXAAiAyTaHV9klntOB81Y+YHyA5iGxy9wXCmjQOfYK5InsuIour+7TVXICUxn2NF0XD6j6OoEJbCVDJ2Y46xEA==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.5.tgz", + "integrity": "sha512-dK9OtI8MeKfdtOiW1auDITnyaelq0O0aUTnolIqJj+RJd8LFai0gi7fQUgrun9CZHJ2wWsEad4vlviGfhfIIhQ==", "dev": true, + "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.9", + "@ariakit/core": "0.4.12", "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { @@ -1463,6 +1459,13 @@ } } }, + "node_modules/@ariakit/test/node_modules/@ariakit/core": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.12.tgz", + "integrity": "sha512-+NNpy88tdP/w9mOBPuDrMTbtapPbo/8yVIzpQB7TAmN0sPh/Cq3nU1f2KCTCIujPmwRvAcMSW9UHOlFmbKEPOA==", + "dev": true, + "license": "MIT" + }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -49340,9 +49343,10 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -53677,7 +53681,7 @@ "version": "28.13.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.4.10", + "@ariakit/react": "^0.4.13", "@babel/runtime": "7.25.7", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -53732,12 +53736,19 @@ "react-dom": "^18.0.0" } }, + "packages/components/node_modules/@ariakit/core": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.12.tgz", + "integrity": "sha512-+NNpy88tdP/w9mOBPuDrMTbtapPbo/8yVIzpQB7TAmN0sPh/Cq3nU1f2KCTCIujPmwRvAcMSW9UHOlFmbKEPOA==", + "license": "MIT" + }, "packages/components/node_modules/@ariakit/react": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.10.tgz", - "integrity": "sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.13.tgz", + "integrity": "sha512-pTGYgoqCojfyt2xNJ5VQhejxXwwtcP7VDDqcnnVChv7TA2TWWyYerJ5m4oxViI1pgeNqnHZwKlQ79ZipF7W2kQ==", + "license": "MIT", "dependencies": { - "@ariakit/react-core": "0.4.10" + "@ariakit/react-core": "0.4.13" }, "funding": { "type": "opencollective", @@ -53749,11 +53760,12 @@ } }, "packages/components/node_modules/@ariakit/react-core": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.10.tgz", - "integrity": "sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.13.tgz", + "integrity": "sha512-iIjQeupP9d0pOubOzX4a0UPXbhXbp0ZCduDpkv7+u/pYP/utk/YRECD0M/QpZr6YSeltmDiNxKjdyK8r9Yhv4Q==", + "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.9", + "@ariakit/core": "0.4.12", "@floating-ui/dom": "^1.0.0", "use-sync-external-store": "^1.2.0" }, @@ -54039,7 +54051,7 @@ "version": "4.9.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.4.10", + "@ariakit/react": "^0.4.13", "@babel/runtime": "7.25.7", "@wordpress/components": "*", "@wordpress/compose": "*", @@ -54061,12 +54073,19 @@ "react": "^18.0.0" } }, + "packages/dataviews/node_modules/@ariakit/core": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.12.tgz", + "integrity": "sha512-+NNpy88tdP/w9mOBPuDrMTbtapPbo/8yVIzpQB7TAmN0sPh/Cq3nU1f2KCTCIujPmwRvAcMSW9UHOlFmbKEPOA==", + "license": "MIT" + }, "packages/dataviews/node_modules/@ariakit/react": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.10.tgz", - "integrity": "sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.13.tgz", + "integrity": "sha512-pTGYgoqCojfyt2xNJ5VQhejxXwwtcP7VDDqcnnVChv7TA2TWWyYerJ5m4oxViI1pgeNqnHZwKlQ79ZipF7W2kQ==", + "license": "MIT", "dependencies": { - "@ariakit/react-core": "0.4.10" + "@ariakit/react-core": "0.4.13" }, "funding": { "type": "opencollective", @@ -54078,11 +54097,12 @@ } }, "packages/dataviews/node_modules/@ariakit/react-core": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.10.tgz", - "integrity": "sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.13.tgz", + "integrity": "sha512-iIjQeupP9d0pOubOzX4a0UPXbhXbp0ZCduDpkv7+u/pYP/utk/YRECD0M/QpZr6YSeltmDiNxKjdyK8r9Yhv4Q==", + "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.9", + "@ariakit/core": "0.4.12", "@floating-ui/dom": "^1.0.0", "use-sync-external-store": "^1.2.0" }, diff --git a/package.json b/package.json index 3ddcb981f6f6f..84425dbd1cff2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@actions/core": "1.9.1", "@actions/github": "5.0.0", "@apidevtools/json-schema-ref-parser": "11.6.4", - "@ariakit/test": "^0.4.2", + "@ariakit/test": "^0.4.5", "@babel/core": "7.25.7", "@babel/plugin-syntax-jsx": "7.25.7", "@babel/runtime-corejs3": "7.25.7", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index be930515f1665..937027ecdd1ea 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). + ## 28.13.0 (2024-11-27) ### Deprecations @@ -11,7 +15,7 @@ - `FontSizePicker`: Deprecate 36px default size ([#66920](https://github.com/WordPress/gutenberg/pull/66920)). - `ComboboxControl`: Deprecate 36px default size ([#66900](https://github.com/WordPress/gutenberg/pull/66900)). - `ToggleGroupControl`: Deprecate 36px default size ([#66747](https://github.com/WordPress/gutenberg/pull/66747)). -- `RangeControl`: Deprecate 36px default size ([#66721](https://github.com/WordPress/gutenberg/pull/66721)). +- `RangeControl`: Deprecate 36px default size ([#66721](https://github.com/WordPress/gutenberg/pull/66721)). ### Bug Fixes diff --git a/packages/components/package.json b/packages/components/package.json index dc62f992c3bb2..75f0d1eb1f233 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -32,7 +32,7 @@ "src/**/*.scss" ], "dependencies": { - "@ariakit/react": "^0.4.10", + "@ariakit/react": "^0.4.13", "@babel/runtime": "7.25.7", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", diff --git a/packages/components/src/composite/item.tsx b/packages/components/src/composite/item.tsx index edbf0b92e039a..4a02f76039a5c 100644 --- a/packages/components/src/composite/item.tsx +++ b/packages/components/src/composite/item.tsx @@ -26,23 +26,5 @@ export const CompositeItem = forwardRef< // obfuscated to discourage its use outside of the component's internals. const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; - // If the active item is not connected, Composite may end up in a state - // where none of the items are tabbable. In this case, we force all items to - // be tabbable, so that as soon as an item received focus, it becomes active - // and Composite goes back to working as expected. - const tabbable = Ariakit.useStoreState( store, ( state ) => { - return ( - state?.activeId !== null && - ! store?.item( state?.activeId )?.element?.isConnected - ); - } ); - - return ( - - ); + return ; } ); diff --git a/packages/components/src/menu/checkbox-item.tsx b/packages/components/src/menu/checkbox-item.tsx index b9a9b8105e517..182c27dfdee30 100644 --- a/packages/components/src/menu/checkbox-item.tsx +++ b/packages/components/src/menu/checkbox-item.tsx @@ -16,24 +16,20 @@ import type { WordPressComponentProps } from '../context'; import { MenuContext } from './context'; import type { MenuCheckboxItemProps } from './types'; import * as Styled from './styles'; -import { useTemporaryFocusVisibleFix } from './use-temporary-focus-visible-fix'; export const MenuCheckboxItem = forwardRef< HTMLDivElement, WordPressComponentProps< MenuCheckboxItemProps, 'div', false > >( function MenuCheckboxItem( - { suffix, children, onBlur, hideOnClick = false, ...props }, + { suffix, children, hideOnClick = false, ...props }, ref ) { - // TODO: Remove when https://github.com/ariakit/ariakit/issues/4083 is fixed - const focusVisibleFixProps = useTemporaryFocusVisibleFix( { onBlur } ); const menuContext = useContext( MenuContext ); return ( >( function MenuItem( - { prefix, suffix, children, onBlur, hideOnClick = true, ...props }, + { prefix, suffix, children, hideOnClick = true, ...props }, ref ) { - // TODO: Remove when https://github.com/ariakit/ariakit/issues/4083 is fixed - const focusVisibleFixProps = useTemporaryFocusVisibleFix( { onBlur } ); const menuContext = useContext( MenuContext ); return ( @@ -29,18 +28,15 @@ export const MenuRadioItem = forwardRef< HTMLDivElement, WordPressComponentProps< MenuRadioItemProps, 'div', false > >( function MenuRadioItem( - { suffix, children, onBlur, hideOnClick = false, ...props }, + { suffix, children, hideOnClick = false, ...props }, ref ) { - // TODO: Remove when https://github.com/ariakit/ariakit/issues/4083 is fixed - const focusVisibleFixProps = useTemporaryFocusVisibleFix( { onBlur } ); const menuContext = useContext( MenuContext ); return ( ; -} ) { - const [ focusVisible, setFocusVisible ] = useState( false ); - return { - 'data-focus-visible': focusVisible || undefined, - onFocusVisible: () => { - flushSync( () => setFocusVisible( true ) ); - }, - onBlur: ( ( event ) => { - onBlurProp?.( event ); - setFocusVisible( false ); - } ) as React.FocusEventHandler< HTMLDivElement >, - }; -} diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index 70f56e52ad262..8226d0589f08c 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import * as Ariakit from '@ariakit/react'; - /** * WordPress dependencies */ @@ -29,18 +24,6 @@ export const Tab = forwardRef< >( function Tab( { children, tabId, disabled, render, ...otherProps }, ref ) { const { store, instanceId } = useTabsContext() ?? {}; - // If the active item is not connected, the tablist may end up in a state - // where none of the tabs are tabbable. In this case, we force all tabs to - // be tabbable, so that as soon as an item received focus, it becomes active - // and Tablist goes back to working as expected. - // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const tabbable = Ariakit.useStoreState( store, ( state ) => { - return ( - state?.activeId !== null && - ! store?.item( state?.activeId )?.element?.isConnected - ); - } ); - if ( ! store ) { warning( '`Tabs.Tab` must be wrapped in a `Tabs` component.' ); return null; @@ -55,7 +38,6 @@ export const Tab = forwardRef< id={ instancedTabId } disabled={ disabled } render={ render } - tabbable={ tabbable } { ...otherProps } > { children } diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index f4d42102731eb..8fe2e04236725 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -43,7 +43,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@ariakit/react": "^0.4.10", + "@ariakit/react": "^0.4.13", "@babel/runtime": "7.25.7", "@wordpress/components": "*", "@wordpress/compose": "*", From a387fbbdf9fa468217944aa5b78fb6113c2ed63b Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 28 Nov 2024 19:17:37 +0400 Subject: [PATCH 049/444] Block Editor: Fix JS error in the 'useTabNav' hook (#67102) * Block Editor: Fix JS error in the 'useTabNav' hook * Focus on canvas when there's no section root Co-authored-by: Mamaduka Co-authored-by: jeryj Co-authored-by: getdave --- .../src/components/writing-flow/use-tab-nav.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index 16a18358fb2ed..46c40d56fe96d 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -35,6 +35,11 @@ export default function useTabNav() { const noCaptureRef = useRef(); function onFocusCapture( event ) { + const canvasElement = + container.current.ownerDocument === event.target.ownerDocument + ? container.current + : container.current.ownerDocument.defaultView.frameElement; + // Do not capture incoming focus if set by us in WritingFlow. if ( noCaptureRef.current ) { noCaptureRef.current = null; @@ -64,17 +69,15 @@ export default function useTabNav() { .focus(); } // If we don't have any section blocks, focus the section root. - else { + else if ( sectionRootClientId ) { container.current .querySelector( `[data-block="${ sectionRootClientId }"]` ) .focus(); + } else { + // If we don't have any section root, focus the canvas. + canvasElement.focus(); } } else { - const canvasElement = - container.current.ownerDocument === event.target.ownerDocument - ? container.current - : container.current.ownerDocument.defaultView.frameElement; - const isBefore = // eslint-disable-next-line no-bitwise event.target.compareDocumentPosition( canvasElement ) & From 5efeef9ae740d2aa51fa2493782773083b5363ee Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Thu, 28 Nov 2024 07:45:13 -0800 Subject: [PATCH 050/444] Convert lock unlock to generics (#66682) * Convert lock unlock to generics * Set object type from generic for unlock * Fix types affected by updated signature of lock/unlock * Improve signature * Remove expected errors no longer needed * Restore the type for component private APIs Co-authored-by: manzoorwanijk Co-authored-by: youknowriad Co-authored-by: jsnajdr --- packages/private-apis/src/implementation.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts index bae53bae8d158..5a5fb3f39fa18 100644 --- a/packages/private-apis/src/implementation.ts +++ b/packages/private-apis/src/implementation.ts @@ -137,14 +137,16 @@ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( * @param object The object to bind the private data to. * @param privateData The private data to bind to the object. */ -function lock( object: Record< symbol, WeakKey >, privateData: unknown ) { +function lock( object: unknown, privateData: unknown ) { if ( ! object ) { throw new Error( 'Cannot lock an undefined object.' ); } - if ( ! ( __private in object ) ) { - object[ __private ] = {}; + const _object = object as Record< symbol, WeakKey >; + + if ( ! ( __private in _object ) ) { + _object[ __private ] = {}; } - lockedData.set( object[ __private ], privateData ); + lockedData.set( _object[ __private ], privateData ); } /** @@ -170,17 +172,19 @@ function lock( object: Record< symbol, WeakKey >, privateData: unknown ) { * @param object The object to unlock the private data from. * @return The private data bound to the object. */ -function unlock( object: Record< symbol, WeakKey > ) { +function unlock< T = any >( object: unknown ): T { if ( ! object ) { throw new Error( 'Cannot unlock an undefined object.' ); } - if ( ! ( __private in object ) ) { + const _object = object as Record< symbol, WeakKey >; + + if ( ! ( __private in _object ) ) { throw new Error( 'Cannot unlock an object that was not locked before. ' ); } - return lockedData.get( object[ __private ] ); + return lockedData.get( _object[ __private ] ); } const lockedData = new WeakMap(); From db263fb38a7e6bd8bbf7d27cb189c13063979c90 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 28 Nov 2024 20:24:44 +0400 Subject: [PATCH 051/444] Components: Fix the 'ClipboardButton' effect cleanup (#67399) Co-authored-by: Mamaduka Co-authored-by: tyxla --- packages/components/src/clipboard-button/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/components/src/clipboard-button/index.tsx b/packages/components/src/clipboard-button/index.tsx index 0bf7d177e251e..492ab64b7290e 100644 --- a/packages/components/src/clipboard-button/index.tsx +++ b/packages/components/src/clipboard-button/index.tsx @@ -45,9 +45,11 @@ export default function ClipboardButton( { } ); useEffect( () => { - if ( timeoutIdRef.current ) { - clearTimeout( timeoutIdRef.current ); - } + return () => { + if ( timeoutIdRef.current ) { + clearTimeout( timeoutIdRef.current ); + } + }; }, [] ); const classes = clsx( 'components-clipboard-button', className ); From 6a989b420b2dc809076f79985b976b0b400749e3 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 28 Nov 2024 18:08:39 +0100 Subject: [PATCH 052/444] DataViews: Avoid double click handler on primary fields (#67393) Co-authored-by: youknowriad Co-authored-by: gigitux --- .../dataviews-layouts/utils/get-clickable-item-props.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts b/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts index e2a6081a68fa3..efb4a8f598c7b 100644 --- a/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts +++ b/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts @@ -12,9 +12,15 @@ export default function getClickableItemProps< Item >( className: `${ className } ${ className }--clickable`, role: 'button', tabIndex: 0, - onClick: () => onClickItem( item ), + onClick: ( event: React.MouseEvent ) => { + // Prevents onChangeSelection from triggering. + event.stopPropagation(); + onClickItem( item ); + }, onKeyDown: ( event: React.KeyboardEvent ) => { if ( event.key === 'Enter' || event.key === '' ) { + // Prevents onChangeSelection from triggering. + event.stopPropagation(); onClickItem( item ); } }, From 9c1446de351e94f3b186050e9c4396c2747d38e5 Mon Sep 17 00:00:00 2001 From: Yogesh Bhutkar Date: Fri, 29 Nov 2024 00:05:45 +0530 Subject: [PATCH 053/444] FontCollection: Update pagination controls (#67143) Co-authored-by: yogeshbhutkar Co-authored-by: matiasbenedetto Co-authored-by: juanfra --- .../font-library-modal/font-collection.js | 67 +++++++++++-------- .../font-library-modal/style.scss | 17 +++-- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index caf339091de75..3aef0171ec358 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -27,7 +27,13 @@ import { } from '@wordpress/components'; import { debounce } from '@wordpress/compose'; import { sprintf, __, _x, isRTL } from '@wordpress/i18n'; -import { moreVertical, chevronLeft, chevronRight } from '@wordpress/icons'; +import { + moreVertical, + next, + previous, + chevronLeft, + chevronRight, +} from '@wordpress/icons'; /** * Internal dependencies @@ -486,37 +492,30 @@ function FontCollection( { slug } ) { { ! selectedFont && ( - ); } -class PostPublishPanelPostpublish extends Component { - constructor() { - super( ...arguments ); - this.state = { - showCopyConfirmation: false, +export default function PostPublishPanelPostpublish( { + focusOnMount, + children, +} ) { + const { post, postType, isScheduled } = useSelect( ( select ) => { + const { + getEditedPostAttribute, + getCurrentPost, + isCurrentPostScheduled, + } = select( editorStore ); + const { getPostType } = select( coreStore ); + + return { + post: getCurrentPost(), + postType: getPostType( getEditedPostAttribute( 'type' ) ), + isScheduled: isCurrentPostScheduled(), }; - this.onCopy = this.onCopy.bind( this ); - this.onSelectInput = this.onSelectInput.bind( this ); - this.postLink = createRef(); - } - - componentDidMount() { - if ( this.props.focusOnMount ) { - this.postLink.current.focus(); - } - } - - componentWillUnmount() { - clearTimeout( this.dismissCopyConfirmation ); - } - - onCopy() { - this.setState( { - showCopyConfirmation: true, - } ); + }, [] ); + + const postLabel = postType?.labels?.singular_name; + const viewPostLabel = postType?.labels?.view_item; + const addNewPostLabel = postType?.labels?.add_new_item; + const link = + post.status === 'future' ? getFuturePostUrl( post ) : post.link; + const addLink = addQueryArgs( 'post-new.php', { + post_type: post.type, + } ); + + const postLinkRef = useCallback( + ( node ) => { + if ( focusOnMount && node ) { + node.focus(); + } + }, + [ focusOnMount ] + ); - clearTimeout( this.dismissCopyConfirmation ); - this.dismissCopyConfirmation = setTimeout( () => { - this.setState( { - showCopyConfirmation: false, - } ); - }, 4000 ); - } + const postPublishNonLinkHeader = isScheduled ? ( + <> + { __( 'is now scheduled. It will go live on' ) }{ ' ' } + . + + ) : ( + __( 'is now live.' ) + ); - onSelectInput( event ) { - event.target.select(); - } + return ( +
+ + + { decodeEntities( post.title ) || __( '(no title)' ) } + { ' ' } + { postPublishNonLinkHeader } + + +

+ { __( 'What’s next?' ) } +

+
+ event.target.select() } + /> - render() { - const { children, isScheduled, post, postType } = this.props; - const postLabel = postType?.labels?.singular_name; - const viewPostLabel = postType?.labels?.view_item; - const addNewPostLabel = postType?.labels?.add_new_item; - const link = - post.status === 'future' ? getFuturePostUrl( post ) : post.link; - const addLink = addQueryArgs( 'post-new.php', { - post_type: post.type, - } ); - - const postPublishNonLinkHeader = isScheduled ? ( - <> - { __( 'is now scheduled. It will go live on' ) }{ ' ' } - . - - ) : ( - __( 'is now live.' ) - ); - - return ( -
- - - { decodeEntities( post.title ) || __( '(no title)' ) } - { ' ' } - { postPublishNonLinkHeader } - - -

- { __( 'What’s next?' ) } -

-
- - -
- - { this.state.showCopyConfirmation - ? __( 'Copied!' ) - : __( 'Copy' ) } - -
+
+
+
-
- { ! isScheduled && ( - - ) } +
+ { ! isScheduled && ( -
- - { children } -
- ); - } + ) } + +
+ + { children } +
+ ); } - -export default withSelect( ( select ) => { - const { getEditedPostAttribute, getCurrentPost, isCurrentPostScheduled } = - select( editorStore ); - const { getPostType } = select( coreStore ); - - return { - post: getCurrentPost(), - postType: getPostType( getEditedPostAttribute( 'type' ) ), - isScheduled: isCurrentPostScheduled(), - }; -} )( PostPublishPanelPostpublish ); From a572238c5d90b83e3d8d55c738a0cc5f474c2c7b Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 29 Nov 2024 08:39:23 +0100 Subject: [PATCH 058/444] [mini] drag and drop: fix scroll disorientation after drop (#67405) Co-authored-by: ellatrix Co-authored-by: talldan --- .../src/components/use-moving-animation/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index 602b683150d0c..ef367c0f33210 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -52,6 +52,7 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { isFirstMultiSelectedBlock, isBlockMultiSelected, isAncestorMultiSelected, + isDraggingBlocks, } = useSelect( blockEditorStore ); // Whenever the trigger changes, we need to take a snapshot of the current @@ -85,6 +86,11 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { } } + // Neither animate nor scroll. + if ( isDraggingBlocks() ) { + return; + } + // We disable the animation if the user has a preference for reduced // motion, if the user is typing (insertion by Enter), or if the block // count exceeds the threshold (insertion caused all the blocks that @@ -153,6 +159,7 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { isFirstMultiSelectedBlock, isBlockMultiSelected, isAncestorMultiSelected, + isDraggingBlocks, ] ); return ref; From 8acc11dec8cbb3e335002c1119da1d5b6b90eabc Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 29 Nov 2024 10:19:48 +0100 Subject: [PATCH 059/444] Menu: throw when subcomponents are not rendered inside top level Menu (#67411) * Menu: throw when subcomponents are not rendered inside top level Menu * CHANGELOG * Remove unnecessary optional chaining * Rename changelog section --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 6 +++++- packages/components/src/menu/checkbox-item.tsx | 10 ++++++++-- packages/components/src/menu/group-label.tsx | 9 ++++++++- packages/components/src/menu/group.tsx | 9 ++++++++- packages/components/src/menu/item-help-text.tsx | 11 ++++++++++- packages/components/src/menu/item-label.tsx | 11 ++++++++++- packages/components/src/menu/item.tsx | 8 +++++++- packages/components/src/menu/radio-item.tsx | 10 ++++++++-- packages/components/src/menu/separator.tsx | 11 +++++++++-- 9 files changed, 73 insertions(+), 12 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index fe326e847970b..37da311b0547a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,6 +7,10 @@ - `BoxControl`: Passive deprecate `onMouseOver`/`onMouseOut`. Pass to the `inputProps` prop instead ([#67332](https://github.com/WordPress/gutenberg/pull/67332)). - `BoxControl`: Deprecate 36px default size ([#66704](https://github.com/WordPress/gutenberg/pull/66704)). +### Experimental + +- `Menu`: throw when subcomponents are not rendered inside top level `Menu` ([#67411](https://github.com/WordPress/gutenberg/pull/67411)). + ### Internal - Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). @@ -20,7 +24,7 @@ - `FontSizePicker`: Deprecate 36px default size ([#66920](https://github.com/WordPress/gutenberg/pull/66920)). - `ComboboxControl`: Deprecate 36px default size ([#66900](https://github.com/WordPress/gutenberg/pull/66900)). - `ToggleGroupControl`: Deprecate 36px default size ([#66747](https://github.com/WordPress/gutenberg/pull/66747)). -- `RangeControl`: Deprecate 36px default size ([#66721](https://github.com/WordPress/gutenberg/pull/66721)). +- `RangeControl`: Deprecate 36px default size ([#66721](https://github.com/WordPress/gutenberg/pull/66721)). ### Bug Fixes diff --git a/packages/components/src/menu/checkbox-item.tsx b/packages/components/src/menu/checkbox-item.tsx index 182c27dfdee30..ddb700b43324a 100644 --- a/packages/components/src/menu/checkbox-item.tsx +++ b/packages/components/src/menu/checkbox-item.tsx @@ -26,16 +26,22 @@ export const MenuCheckboxItem = forwardRef< ) { const menuContext = useContext( MenuContext ); + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.CheckboxItem can only be rendered inside a Menu component' + ); + } + return ( } // Override some ariakit inline styles style={ { width: 'auto', height: 'auto' } } diff --git a/packages/components/src/menu/group-label.tsx b/packages/components/src/menu/group-label.tsx index 71c5c7de69941..5bf081880cb1d 100644 --- a/packages/components/src/menu/group-label.tsx +++ b/packages/components/src/menu/group-label.tsx @@ -17,6 +17,13 @@ export const MenuGroupLabel = forwardRef< WordPressComponentProps< MenuGroupLabelProps, 'div', false > >( function MenuGroup( props, ref ) { const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.GroupLabel can only be rendered inside a Menu component' + ); + } + return ( } { ...props } - store={ menuContext?.store } + store={ menuContext.store } /> ); } ); diff --git a/packages/components/src/menu/group.tsx b/packages/components/src/menu/group.tsx index f9a4138fe4358..834350955f3c5 100644 --- a/packages/components/src/menu/group.tsx +++ b/packages/components/src/menu/group.tsx @@ -16,11 +16,18 @@ export const MenuGroup = forwardRef< WordPressComponentProps< MenuGroupProps, 'div', false > >( function MenuGroup( props, ref ) { const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.Group can only be rendered inside a Menu component' + ); + } + return ( ); } ); diff --git a/packages/components/src/menu/item-help-text.tsx b/packages/components/src/menu/item-help-text.tsx index 0ccc8f7461a8f..13d14c294125b 100644 --- a/packages/components/src/menu/item-help-text.tsx +++ b/packages/components/src/menu/item-help-text.tsx @@ -1,18 +1,27 @@ /** * WordPress dependencies */ -import { forwardRef } from '@wordpress/element'; +import { forwardRef, useContext } from '@wordpress/element'; /** * Internal dependencies */ import type { WordPressComponentProps } from '../context'; +import { MenuContext } from './context'; import * as Styled from './styles'; export const MenuItemHelpText = forwardRef< HTMLSpanElement, WordPressComponentProps< { children: React.ReactNode }, 'span', true > >( function MenuItemHelpText( props, ref ) { + const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.ItemHelpText can only be rendered inside a Menu component' + ); + } + return ( ); diff --git a/packages/components/src/menu/item-label.tsx b/packages/components/src/menu/item-label.tsx index 458f69558eafb..4f5f80e547861 100644 --- a/packages/components/src/menu/item-label.tsx +++ b/packages/components/src/menu/item-label.tsx @@ -1,18 +1,27 @@ /** * WordPress dependencies */ -import { forwardRef } from '@wordpress/element'; +import { forwardRef, useContext } from '@wordpress/element'; /** * Internal dependencies */ import type { WordPressComponentProps } from '../context'; +import { MenuContext } from './context'; import * as Styled from './styles'; export const MenuItemLabel = forwardRef< HTMLSpanElement, WordPressComponentProps< { children: React.ReactNode }, 'span', true > >( function MenuItemLabel( props, ref ) { + const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.ItemLabel can only be rendered inside a Menu component' + ); + } + return ( ); diff --git a/packages/components/src/menu/item.tsx b/packages/components/src/menu/item.tsx index 6e8c510e4c9c2..6d09bdf3d0f59 100644 --- a/packages/components/src/menu/item.tsx +++ b/packages/components/src/menu/item.tsx @@ -20,13 +20,19 @@ export const MenuItem = forwardRef< ) { const menuContext = useContext( MenuContext ); + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.Item can only be rendered inside a Menu component' + ); + } + return ( { prefix } diff --git a/packages/components/src/menu/radio-item.tsx b/packages/components/src/menu/radio-item.tsx index 45b0039f900e2..5534a6b7f3e10 100644 --- a/packages/components/src/menu/radio-item.tsx +++ b/packages/components/src/menu/radio-item.tsx @@ -33,16 +33,22 @@ export const MenuRadioItem = forwardRef< ) { const menuContext = useContext( MenuContext ); + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.RadioItem can only be rendered inside a Menu component' + ); + } + return ( } // Override some ariakit inline styles style={ { width: 'auto', height: 'auto' } } diff --git a/packages/components/src/menu/separator.tsx b/packages/components/src/menu/separator.tsx index 5d0110016d9c4..57cff572c287a 100644 --- a/packages/components/src/menu/separator.tsx +++ b/packages/components/src/menu/separator.tsx @@ -16,12 +16,19 @@ export const MenuSeparator = forwardRef< WordPressComponentProps< MenuSeparatorProps, 'hr', false > >( function MenuSeparator( props, ref ) { const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.Separator can only be rendered inside a Menu component' + ); + } + return ( ); } ); From 68c7aba5277eb1fb2d956f88ba9729325eec2e12 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 29 Nov 2024 10:42:52 +0100 Subject: [PATCH 060/444] Tabs: overhaul unit tests (#66140) * Rewrite unit tests * Extract waitForComponentToBeInitializedWithSelectedTab utility function * Use describe.each to run same test against controlled / uncontrolled components * Re-enable tabs when testing disabled tabs * Mock isRTL and test RTL keyboard navigation * CHANGELOG * Remove CHANGELOG entry * Fix typo --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/src/tabs/test/index.tsx | 2439 ++++++++++++------- 1 file changed, 1492 insertions(+), 947 deletions(-) diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index dcf64102c9fa6..fd9ceb38190a7 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -9,6 +9,7 @@ import { render } from '@ariakit/test/react'; * WordPress dependencies */ import { useEffect, useState } from '@wordpress/element'; +import { isRTL } from '@wordpress/i18n'; /** * Internal dependencies @@ -16,6 +17,16 @@ import { useEffect, useState } from '@wordpress/element'; import { Tabs } from '..'; import type { TabsProps } from '../types'; +// Setup mocking the `isRTL` function to test arrow key navigation behavior. +jest.mock( '@wordpress/i18n', () => { + const original = jest.requireActual( '@wordpress/i18n' ); + return { + ...original, + isRTL: jest.fn( () => false ), + }; +} ); +const mockedIsRTL = isRTL as jest.Mock; + type Tab = { tabId: string; title: string; @@ -50,6 +61,30 @@ const TABS: Tab[] = [ }, ]; +const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => + tabObj.tabId === 'alpha' + ? { + ...tabObj, + tab: { + ...tabObj.tab, + disabled: true, + }, + } + : tabObj +); + +const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) => + tabObj.tabId === 'beta' + ? { + ...tabObj, + tab: { + ...tabObj.tab, + disabled: true, + }, + } + : tabObj +); + const TABS_WITH_DELTA: Tab[] = [ ...TABS, { @@ -141,11 +176,47 @@ const ControlledTabs = ( { ); }; -const getSelectedTab = async () => - await screen.findByRole( 'tab', { selected: true } ); - let originalGetClientRects: () => DOMRectList; +async function waitForComponentToBeInitializedWithSelectedTab( + selectedTabName: string | undefined +) { + if ( ! selectedTabName ) { + // Wait for the tablist to be tabbable as a mean to know + // that ariakit has finished initializing. + await waitFor( () => + expect( screen.getByRole( 'tablist' ) ).toHaveAttribute( + 'tabindex', + expect.stringMatching( /^(0|-1)$/ ) + ) + ); + // No initially selected tabs or tabpanels. + await waitFor( () => + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument() + ); + await waitFor( () => + expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument() + ); + } else { + // Waiting for a tab to be selected is a sign that the component + // has fully initialized. + expect( + await screen.findByRole( 'tab', { + selected: true, + name: selectedTabName, + } ) + ).toBeVisible(); + // The corresponding tabpanel is also shown. + expect( + screen.getByRole( 'tabpanel', { + name: selectedTabName, + } ) + ).toBeVisible(); + } +} + describe( 'Tabs', () => { beforeAll( () => { originalGetClientRects = window.HTMLElement.prototype.getClientRects; @@ -162,13 +233,16 @@ describe( 'Tabs', () => { window.HTMLElement.prototype.getClientRects = originalGetClientRects; } ); - describe( 'Accessibility and semantics', () => { - it( 'should use the correct aria attributes', async () => { + describe( 'Adherence to spec and basic behavior', () => { + it( 'should apply the correct roles, semantics and attributes', async () => { await render( ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); + const tabList = screen.getByRole( 'tablist' ); const allTabs = screen.getAllByRole( 'tab' ); - const selectedTabPanel = await screen.findByRole( 'tabpanel' ); + const allTabpanels = screen.getAllByRole( 'tabpanel' ); expect( tabList ).toBeVisible(); expect( tabList ).toHaveAttribute( @@ -178,133 +252,103 @@ describe( 'Tabs', () => { expect( allTabs ).toHaveLength( TABS.length ); - // The selected `tab` aria-controls the active `tabpanel`, - // which is `aria-labelledby` the selected `tab`. - expect( selectedTabPanel ).toBeVisible(); + // Only 1 tab panel is accessible — the one associated with the + // selected tab. The selected `tab` aria-controls the active + /// `tabpanel`, which is `aria-labelledby` the selected `tab`. + expect( allTabpanels ).toHaveLength( 1 ); + + expect( allTabpanels[ 0 ] ).toBeVisible(); expect( allTabs[ 0 ] ).toHaveAttribute( 'aria-controls', - selectedTabPanel.getAttribute( 'id' ) + allTabpanels[ 0 ].getAttribute( 'id' ) ); - expect( selectedTabPanel ).toHaveAttribute( + expect( allTabpanels[ 0 ] ).toHaveAttribute( 'aria-labelledby', allTabs[ 0 ].getAttribute( 'id' ) ); } ); - } ); - describe( 'Focus Behavior', () => { - it( 'should focus on the related TabPanel when pressing the Tab key', async () => { - await render( ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - - const selectedTabPanel = await screen.findByRole( 'tabpanel' ); - - // Tab should initially focus the first tab in the tablist, which - // is Alpha. - await press.Tab(); - expect( - await screen.findByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus(); - - // By default the tabpanel should receive focus - await press.Tab(); - expect( selectedTabPanel ).toHaveFocus(); - } ); - it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { - const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => - tabObj.tabId === 'alpha' - ? { - ...tabObj, - content: ( - <> - Selected Tab: Alpha - - - ), - tabpanel: { focusable: false }, - } - : tabObj - ); + it( 'should associate each `tab` with the correct `tabpanel`, even if they are not rendered in the same order', async () => { + const TABS_WITH_DELTA_REVERSED = [ ...TABS_WITH_DELTA ].reverse(); await render( - + + + { TABS_WITH_DELTA.map( ( tabObj ) => ( + + { tabObj.title } + + ) ) } + + { TABS_WITH_DELTA_REVERSED.map( ( tabObj ) => ( + + { tabObj.content } + + ) ) } + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - - const alphaButton = await screen.findByRole( 'button', { - name: /alpha button/i, - } ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - // Tab should initially focus the first tab in the tablist, which - // is Alpha. - await press.Tab(); + // Select Beta, make sure the correct tabpanel is rendered + await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); expect( - await screen.findByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus(); - // Because the alpha tabpanel is set to `focusable: false`, pressing - // the Tab key should focus the button, not the tabpanel - await press.Tab(); - expect( alphaButton ).toHaveFocus(); - } ); - - it( "should focus the first tab, even if disabled, when the current selected tab id doesn't match an existing one", async () => { - const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.tabId === 'alpha' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); - - await render( - - ); - - // No tab should be selected i.e. it doesn't fall back to first tab. - await waitFor( () => - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); - - // No tabpanel should be rendered either - expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument(); - - await press.Tab(); + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); expect( - await screen.findByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus(); + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); - await press.ArrowRight(); + // Select Gamma, make sure the correct tabpanel is rendered + await click( screen.getByRole( 'tab', { name: 'Gamma' } ) ); expect( - await screen.findByRole( 'tab', { name: 'Beta' } ) - ).toHaveFocus(); - - await press.ArrowRight(); + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toBeVisible(); expect( - await screen.findByRole( 'tab', { name: 'Gamma' } ) - ).toHaveFocus(); + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); - await press.Tab(); - await press.ShiftTab(); + // Select Delta, make sure the correct tabpanel is rendered + await click( screen.getByRole( 'tab', { name: 'Delta' } ) ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Delta', + } ) + ).toBeVisible(); expect( - await screen.findByRole( 'tab', { name: 'Gamma' } ) - ).toHaveFocus(); + screen.getByRole( 'tabpanel', { + name: 'Delta', + } ) + ).toBeVisible(); } ); - } ); - describe( 'Tab Attributes', () => { it( "should apply the tab's `className` to the tab button", async () => { await render( ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); + expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveClass( 'alpha-class' ); @@ -317,908 +361,1076 @@ describe( 'Tabs', () => { } ); } ); - describe( 'Tab Activation', () => { - it( 'defaults to automatic tab activation (pointer clicks)', async () => { + describe( 'pointer interactions', () => { + it( 'should select a tab when clicked', async () => { const mockOnSelect = jest.fn(); await render( ); - // Alpha is the initially selected tab - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( - await screen.findByRole( 'tabpanel', { name: 'Alpha' } ) - ).toBeInTheDocument(); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); // Click on Beta, make sure beta is the selected tab await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( - screen.getByRole( 'tabpanel', { name: 'Beta' } ) - ).toBeInTheDocument(); + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); - // Click on Alpha, make sure beta is the selected tab + // Click on Alpha, make sure alpha is the selected tab await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( - screen.getByRole( 'tabpanel', { name: 'Alpha' } ) - ).toBeInTheDocument(); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - } ); - - it( 'defaults to automatic tab activation (arrow keys)', async () => { - const mockOnSelect = jest.fn(); - - await render( - - ); - - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - - // onSelect gets called on the initial render. It should be called - // with the first enabled tab, which is alpha. - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - - // Tab to focus the tablist. Make sure alpha is focused. - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); - await press.Tab(); - expect( await getSelectedTab() ).toHaveFocus(); - - // Navigate forward with arrow keys and make sure the Beta tab is - // selected automatically. - await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - // Navigate backwards with arrow keys. Make sure alpha is - // selected automatically. - await press.ArrowLeft(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); } ); - it( 'wraps around the last/first tab when using arrow keys', async () => { + it( 'should not select a disabled tab when clicked', async () => { const mockOnSelect = jest.fn(); await render( - + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - // onSelect gets called on the initial render. expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - // Tab to focus the tablist. Make sure Alpha is focused. - await press.Tab(); - expect( await getSelectedTab() ).toHaveFocus(); + // Clicking on Beta does not result in beta being selected + // because the tab is disabled. + await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); - // Navigate backwards with arrow keys and make sure that the Gamma tab - // (the last tab) is selected automatically. - await press.ArrowLeft(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - // Navigate forward with arrow keys. Make sure alpha (the first tab) is - // selected automatically. - await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); } ); + } ); - it( 'should not move tab selection when pressing the up/down arrow keys, unless the orientation is changed to `vertical`', async () => { - const mockOnSelect = jest.fn(); + describe( 'initial tab selection', () => { + describe( 'when a selected tab id is not specified', () => { + describe( 'when left `undefined` [Uncontrolled]', () => { + it( 'should choose the first tab as selected', async () => { + await render( ); - const { rerender } = await render( - - ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( + 'Alpha' + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); + // Press tab. The selected tab (alpha) received focus. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + } ); + + it( 'should choose the first non-disabled tab if the first tab is disabled', async () => { + await render( + + ); + + // Beta is automatically selected as the selected tab, since alpha is + // disabled. + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); + + // Press tab. The selected tab (beta) received focus. The corresponding + // tabpanel is shown. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + } ); + } ); + describe( 'when `null` [Controlled]', () => { + it( 'should not have a selected tab nor show any tabpanels, make the tablist tabbable and still allow selecting tabs', async () => { + await render( + + ); + + // No initially selected tabs or tabpanels. + await waitForComponentToBeInitializedWithSelectedTab( + undefined + ); + + // Press tab. The tablist receives focus + await press.Tab(); + expect( + await screen.findByRole( 'tablist' ) + ).toHaveFocus(); - // onSelect gets called on the initial render. - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Press right arrow to select the first tab (alpha) and + // show the related tabpanel. + await press.ArrowRight(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + expect( + await screen.findByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); + } ); + } ); + } ); - // Tab to focus the tablist. Make sure Alpha is focused. - await press.Tab(); - expect( await getSelectedTab() ).toHaveFocus(); + describe( 'when a selected tab id is specified', () => { + describe( 'through the `defaultTabId` prop [Uncontrolled]', () => { + it( 'should select the initial tab matching the `defaultTabId` prop', async () => { + await render( + + ); + + // Beta is the initially selected tab + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); + + // Press tab. The selected tab (beta) received focus. The corresponding + // tabpanel is shown. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + } ); + + it( 'should select the initial tab matching the `defaultTabId` prop even if the tab is disabled', async () => { + await render( + + ); + + // Beta is automatically selected as the selected tab despite being + // disabled, respecting the `defaultTabId` prop. + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); + + // Press tab. The selected tab (beta) received focus, since it is + // accessible despite being disabled. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + } ); + + it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab when `defaultTabId` prop does not match any known tab', async () => { + await render( + + ); + + // No initially selected tabs or tabpanels, since the `defaultTabId` + // prop is not matching any known tabs. + await waitForComponentToBeInitializedWithSelectedTab( + undefined + ); + + // Press tab. The first tab receives focus, but it's + // not selected. + await press.Tab(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + await waitFor( () => + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument() + ); + await waitFor( () => + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument() + ); - // Press the arrow up key, nothing happens. - await press.ArrowUp(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Press right arrow to select the next tab (beta) and + // show the related tabpanel. + await press.ArrowRight(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + await screen.findByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + } ); + + it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab, even when disabled, when `defaultTabId` prop does not match any known tab', async () => { + await render( + + ); + + // No initially selected tabs or tabpanels, since the `defaultTabId` + // prop is not matching any known tabs. + await waitForComponentToBeInitializedWithSelectedTab( + undefined + ); + + // Press tab. The first tab receives focus, but it's + // not selected. + await press.Tab(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + await waitFor( () => + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument() + ); + await waitFor( () => + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument() + ); - // Press the arrow down key, nothing happens - await press.ArrowDown(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Press right arrow to select the next tab (beta) and + // show the related tabpanel. + await press.ArrowRight(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + await screen.findByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + } ); + + it( 'should ignore any changes to the `defaultTabId` prop after the first render', async () => { + const mockOnSelect = jest.fn(); + + const { rerender } = await render( + + ); + + // Beta is the initially selected tab + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); + + // Changing the defaultTabId prop to gamma should not have any effect. + await rerender( + + ); - // Change orientation to `vertical`. When the orientation is vertical, - // left/right arrow keys are replaced by up/down arrow keys. - await rerender( - - ); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); - expect( screen.getByRole( 'tablist' ) ).toHaveAttribute( - 'aria-orientation', - 'vertical' - ); + expect( mockOnSelect ).not.toHaveBeenCalled(); + } ); + } ); - // Make sure alpha is still focused. - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); + describe( 'through the `selectedTabId` prop [Controlled]', () => { + describe( 'when the `selectedTabId` matches an existing tab', () => { + it( 'should choose the initial tab matching the `selectedTabId`', async () => { + await render( + + ); - // Navigate forward with arrow keys and make sure the Beta tab is - // selected automatically. - await press.ArrowDown(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); + // Beta is the initially selected tab + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); - // Navigate backwards with arrow keys. Make sure alpha is - // selected automatically. - await press.ArrowUp(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Press tab. The selected tab (beta) received focus, since it is + // accessible despite being disabled. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + } ); - // Navigate backwards with arrow keys. Make sure alpha is - // selected automatically. - await press.ArrowUp(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); - - // Navigate backwards with arrow keys. Make sure alpha is - // selected automatically. - await press.ArrowDown(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 5 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - } ); + it( 'should choose the initial tab matching the `selectedTabId` even if a `defaultTabId` is passed', async () => { + await render( + + ); - it( 'should move focus on a tab even if disabled with arrow key, but not with pointer clicks', async () => { - const mockOnSelect = jest.fn(); + // Gamma is the initially selected tab + await waitForComponentToBeInitializedWithSelectedTab( + 'Gamma' + ); - const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.tabId === 'delta' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); + // Press tab. The selected tab (gamma) received focus, since it is + // accessible despite being disabled. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toHaveFocus(); + } ); - await render( - - ); + it( 'should choose the initial tab matching the `selectedTabId` even if the tab is disabled', async () => { + await render( + + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); + // Beta is the initially selected tab + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); - // onSelect gets called on the initial render. - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Press tab. The selected tab (beta) received focus, since it is + // accessible despite being disabled. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + } ); + } ); - // Tab to focus the tablist. Make sure Alpha is focused. - await press.Tab(); - expect( await getSelectedTab() ).toHaveFocus(); + describe( "when the `selectedTabId` doesn't match an existing tab", () => { + it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab', async () => { + await render( + + ); - // Confirm onSelect has not been re-called - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + // No initially selected tabs or tabpanels, since the `selectedTabId` + // prop is not matching any known tabs. + await waitForComponentToBeInitializedWithSelectedTab( + undefined + ); - // Press the right arrow key three times. Since the delta tab is disabled: - // - it won't be selected. The gamma tab will be selected instead, since - // it was the tab that was last selected before delta. Therefore, the - // `mockOnSelect` function gets called only twice (and not three times) - // - it will receive focus, when using arrow keys - await press.ArrowRight(); - await press.ArrowRight(); - await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( - screen.getByRole( 'tab', { name: 'Delta' } ) - ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); - - // Navigate backwards with arrow keys. The gamma tab receives focus. - // The `mockOnSelect` callback doesn't fire, since the gamma tab was - // already selected. - await press.ArrowLeft(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + // Press tab. The first tab receives focus, but it's + // not selected. + await press.Tab(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + await waitFor( () => + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument() + ); + await waitFor( () => + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument() + ); - // Click on the disabled tab. Compared to using arrow keys to move the - // focus, disabled tabs ignore pointer clicks — and therefore, they don't - // receive focus, nor they cause the `mockOnSelect` function to fire. - await click( screen.getByRole( 'tab', { name: 'Delta' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( await getSelectedTab() ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); - } ); + // Press right arrow to select the next tab (beta) and + // show the related tabpanel. + await press.ArrowRight(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + await screen.findByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + } ); - it( 'should not focus the next tab when the Tab key is pressed', async () => { - await render( ); + it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab even when disabled', async () => { + await render( + + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); + // No initially selected tabs or tabpanels, since the `selectedTabId` + // prop is not matching any known tabs. + await waitForComponentToBeInitializedWithSelectedTab( + undefined + ); - // Tab should initially focus the first tab in the tablist, which - // is Alpha. - await press.Tab(); - expect( - await screen.findByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus(); + // Press tab. The first tab receives focus, but it's + // not selected. + await press.Tab(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + await waitFor( () => + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument() + ); + await waitFor( () => + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument() + ); - // Because all other tabs should have `tabindex=-1`, pressing Tab - // should NOT move the focus to the next tab, which is Beta. - // Instead, focus should go to the currently selected tabpanel (alpha). - await press.Tab(); - expect( - await screen.findByRole( 'tabpanel', { - name: 'Alpha', - } ) - ).toHaveFocus(); + // Press right arrow to select the next tab (beta) and + // show the related tabpanel. + await press.ArrowRight(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + await screen.findByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + } ); + } ); + } ); } ); + } ); - it( 'switches to manual tab activation when the `selectOnMove` prop is set to `false`', async () => { - const mockOnSelect = jest.fn(); + describe( 'keyboard interactions', () => { + describe.each( [ + [ 'Uncontrolled', UncontrolledTabs ], + [ 'Controlled', ControlledTabs ], + ] )( '[`%s`]', ( _mode, Component ) => { + it( 'should handle the tablist as one tab stop', async () => { + await render( ); - await render( - - ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); + // Press tab. The selected tab (alpha) received focus. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - // onSelect gets called on the initial render. - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // By default the tabpanel should receive focus + await press.Tab(); + expect( + await screen.findByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toHaveFocus(); + } ); - // Click on Alpha and make sure it is selected. - // onSelect shouldn't fire since the selected tab didn't change. - await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); - expect( - await screen.findByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - - // Navigate forward with arrow keys. Make sure Beta is focused, but - // that the tab selection happens only when pressing the spacebar - // or enter key. onSelect shouldn't fire since the selected tab - // didn't change. - await press.ArrowRight(); - expect( - await screen.findByRole( 'tab', { name: 'Beta' } ) - ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - - await press.Enter(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); + it( 'should not focus the tabpanel container when its `focusable` property is set to `false`', async () => { + await render( + + tabObj.tabId === 'alpha' + ? { + ...tabObj, + content: ( + <> + Selected Tab: Alpha + + + ), + tabpanel: { focusable: false }, + } + : tabObj + ) } + /> + ); - // Navigate forward with arrow keys. Make sure Gamma (last tab) is - // focused, but that tab selection happens only when pressing the - // spacebar or enter key. onSelect shouldn't fire since the selected - // tab didn't change. - await press.ArrowRight(); - expect( - await screen.findByRole( 'tab', { name: 'Gamma' } ) - ).toHaveFocus(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); - expect( - screen.getByRole( 'tab', { name: 'Gamma' } ) - ).toHaveFocus(); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - await press.Space(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); - expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); - } ); - } ); - describe( 'Uncontrolled mode', () => { - describe( 'Without `defaultTabId` prop', () => { - it( 'should render first tab', async () => { - await render( ); + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); + // In this case, the tabpanel container is skipped and focus is + // moved directly to its contents + await press.Tab(); expect( - await screen.findByRole( 'tabpanel', { name: 'Alpha' } ) - ).toBeInTheDocument(); + await screen.findByRole( 'button', { + name: 'Alpha Button', + } ) + ).toHaveFocus(); } ); - it( 'should not have a selected tab if the currently selected tab is removed', async () => { - const { rerender } = await render( - - ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( await getSelectedTab() ).not.toHaveFocus(); + it( 'should select tabs in the tablist when using the left and right arrow keys by default (automatic tab activation)', async () => { + const mockOnSelect = jest.fn(); - // Tab to focus the tablist. Make sure Alpha is focused. - await press.Tab(); - expect( await getSelectedTab() ).toHaveFocus(); + await render( + + ); - // Remove first item from `TABS` array - await rerender( ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - // No tab should be selected i.e. it doesn't fall back to first tab. - await waitFor( () => - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - // No tabpanel should be rendered either + // Focus the tablist (and the selected tab, alpha) + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await press.Tab(); expect( - screen.queryByRole( 'tabpanel' ) - ).not.toBeInTheDocument(); - } ); - } ); + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - describe( 'With `defaultTabId`', () => { - it( 'should render the tab set by `defaultTabId` prop', async () => { - await render( - - ); + // Press the right arrow key to select the beta tab + await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - } ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); - it( 'should not select a tab when `defaultTabId` does not match any known tab', async () => { - await render( - - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); - // No tab should be selected i.e. it doesn't fall back to first tab. - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument(); + // Press the right arrow key to select the gamma tab + await press.ArrowRight(); - // No tabpanel should be rendered either expect( - screen.queryByRole( 'tabpanel' ) - ).not.toBeInTheDocument(); - } ); - it( 'should not change tabs when defaultTabId is changed', async () => { - const { rerender } = await render( - - ); + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); - await rerender( - - ); + // Press the left arrow key to select the beta tab + await press.ArrowLeft(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); } ); - it( 'should not have any selected tabs if the currently selected tab is removed, even if a tab is matching the defaultTabId', async () => { + it( 'should not automatically select tabs in the tablist when pressing the left and right arrow keys if the `selectOnMove` prop is set to `false` (manual tab activation)', async () => { const mockOnSelect = jest.fn(); - const { rerender } = await render( - ); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - await rerender( - - ); + // Focus the tablist (and the selected tab, alpha) + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - // No tab should be selected i.e. it doesn't fall back to first tab. - await waitFor( () => - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); + // Press the right arrow key to move focus to the beta tab, + // but without selecting it + await press.ArrowRight(); + + expect( + screen.getByRole( 'tab', { + selected: false, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - // No tabpanel should be rendered either + // Press the space key to click the beta tab, and select it. + // The same should be true with any other mean of clicking the tab button + // (ie. mouse click, enter key). + await press.Space(); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); expect( - screen.queryByRole( 'tabpanel' ) - ).not.toBeInTheDocument(); + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); } ); - it( 'should keep the currently selected tab even if it becomes disabled', async () => { + it( 'should not select tabs in the tablist when using the up and down arrow keys, unless the `orientation` prop is set to `vertical`', async () => { const mockOnSelect = jest.fn(); const { rerender } = await render( - + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - - await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.tabId === 'alpha' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); + // Focus the tablist (and the selected tab, alpha) + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - await rerender( - - ); + // Press the up arrow key, but the focused/selected tab does not change. + await press.ArrowUp(); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - } ); - it( 'should have no active tabs when the tab associated to `defaultTabId` is removed while being the active tab', async () => { - const { rerender } = await render( - - ); + // Press the down arrow key, but the focused/selected tab does not change. + await press.ArrowDown(); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - // Remove gamma + // Change the orientation to "vertical" and rerender the component. await rerender( - ); - expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 ); - // No tab should be selected i.e. it doesn't fall back to first tab. + // Pressing the down arrow key now selects the next tab (beta). + await press.ArrowDown(); + expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument(); - // No tabpanel should be rendered either + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); expect( - screen.queryByRole( 'tabpanel' ) - ).not.toBeInTheDocument(); - } ); + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); - it( 'waits for the tab with the `defaultTabId` to be present in the `tabs` array before selecting it', async () => { - const { rerender } = await render( - - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); - // No tab should be selected i.e. it doesn't fall back to first tab. - await waitFor( () => - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); + // Pressing the up arrow key now selects the previous tab (alpha). + await press.ArrowUp(); - // No tabpanel should be rendered either expect( - screen.queryByRole( 'tabpanel' ) - ).not.toBeInTheDocument(); - - await rerender( - - ); + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Delta' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); } ); - } ); - describe( 'Disabled tab', () => { - it( 'should disable the tab when `disabled` is `true`', async () => { + it( 'should loop tab focus at the end of the tablist when using arrow keys', async () => { const mockOnSelect = jest.fn(); - const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( - ( tabObj ) => - tabObj.tabId === 'delta' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); - await render( - + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - expect( - screen.getByRole( 'tab', { name: 'Delta' } ) - ).toHaveAttribute( 'aria-disabled', 'true' ); - - // onSelect gets called on the initial render. expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - // Move focus to the tablist, make sure alpha is focused. + // Focus the tablist (and the selected tab, alpha) + // Tab should initially focus the first tab in the tablist, which + // is Alpha. await press.Tab(); expect( - screen.getByRole( 'tab', { name: 'Alpha' } ) + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) ).toHaveFocus(); - // onSelect should not be called since the disabled tab is - // highlighted, but not selected. + // Press the left arrow key to loop around and select the gamma tab await press.ArrowLeft(); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - // Delta (which is disabled) has focus expect( - screen.getByRole( 'tab', { name: 'Delta' } ) + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); - // Alpha retains the selection, even if it's not focused. - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - } ); - - it( 'should select first enabled tab when the initial tab is disabled', async () => { - const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.tabId === 'alpha' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); - - const { rerender } = await render( - - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); - // As alpha (first tab) is disabled, - // the first enabled tab should be beta. - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + // Press the right arrow key to loop around and select the alpha tab + await press.ArrowRight(); - // Re-enable all tabs - await rerender( ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - // Even if the initial tab becomes enabled again, the selected - // tab doesn't change. - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); } ); - it( 'should select the tab associated to `defaultTabId` even if the tab is disabled', async () => { - const TABS_ONLY_GAMMA_ENABLED = TABS.map( ( tabObj ) => - tabObj.tabId !== 'gamma' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); - const { rerender } = await render( - - ); + // TODO: mock writing direction to RTL + it( 'should swap the left and right arrow keys when selecting tabs if the writing direction is set to RTL', async () => { + // For this test only, mock the writing direction to RTL. + mockedIsRTL.mockImplementation( () => true ); - // As alpha (first tab), and beta (the initial tab), are both - // disabled the first enabled tab should be gamma. - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + const mockOnSelect = jest.fn(); - // Re-enable all tabs - await rerender( - + await render( + ); - // Even if the initial tab becomes enabled again, the selected tab doesn't - // change. - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - } ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - it( 'should keep the currently tab as selected even when it becomes disabled', async () => { - const mockOnSelect = jest.fn(); - const { rerender } = await render( - - ); - - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.tabId === 'alpha' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); - - // Disable alpha - await rerender( - - ); + // Focus the tablist (and the selected tab, alpha) + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + // Press the left arrow key to select the beta tab + await press.ArrowLeft(); - // Re-enable all tabs - await rerender( - - ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - } ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); - it( 'should select the tab associated to `defaultTabId` even when disabled', async () => { - const mockOnSelect = jest.fn(); + // Press the left arrow key to select the gamma tab + await press.ArrowLeft(); - const { rerender } = await render( - - ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - - const TABS_WITH_GAMMA_DISABLED = TABS.map( ( tabObj ) => - tabObj.tabId === 'gamma' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); - // Disable gamma - await rerender( - - ); + // Press the right arrow key to select the beta tab + await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); - // Re-enable all tabs - await rerender( - - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); - // Confirm that alpha is still selected, and that onSelect has - // not been called again. - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( mockOnSelect ).not.toHaveBeenCalled(); + // Restore the original implementation of the isRTL function. + mockedIsRTL.mockRestore(); } ); - } ); - } ); - - describe( 'Controlled mode', () => { - it( 'should render the tab specified by the `selectedTabId` prop', async () => { - await render( - - ); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - expect( - await screen.findByRole( 'tabpanel', { name: 'Beta' } ) - ).toBeInTheDocument(); - } ); - it( 'should render the specified `selectedTabId`, and ignore the `defaultTabId` prop', async () => { - await render( - - ); - - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - } ); - it( 'should not have a selected tab if `selectedTabId` does not match any known tab', async () => { - await render( - - ); - - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument(); + it( 'should focus tabs in the tablist even if disabled', async () => { + const mockOnSelect = jest.fn(); - // No tabpanel should be rendered either - expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument(); - } ); - it( 'should not have a selected tab if the active tab is removed, but should select a tab that gets added if it matches the selectedTabId', async () => { - const { rerender } = await render( - - ); + await render( + + ); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' ); - // Remove beta - await rerender( - tab.tabId !== 'beta' ) } - selectedTabId="beta" - /> - ); - - expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - // No tab should be selected i.e. it doesn't fall back to first tab. - // `waitFor` is needed here to prevent testing library from - // throwing a 'not wrapped in `act()`' error. - await waitFor( () => + // Focus the tablist (and the selected tab, alpha) + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await press.Tab(); expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); - - // No tabpanel should be rendered either - expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument(); - - // Restore beta - await rerender( - - ); - - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - } ); - - describe( 'Disabled tab', () => { - it( 'should `selectedTabId` refers to a disabled tab', async () => { - const TABS_WITH_DELTA_WITH_BETA_DISABLED = TABS_WITH_DELTA.map( - ( tabObj ) => - tabObj.tabId === 'beta' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); - - await render( - - ); + await screen.findByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - } ); - it( 'should keep the currently selected tab as selected even when it becomes disabled', async () => { - const { rerender } = await render( - - ); + // Pressing the right arrow key moves focus to the beta tab, but alpha + // remains the selected tab because beta is disabled. + await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - - const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) => - tabObj.tabId === 'beta' - ? { - ...tabObj, - tab: { - ...tabObj.tab, - disabled: true, - }, - } - : tabObj - ); + expect( + screen.getByRole( 'tab', { + selected: false, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); - await rerender( - - ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + // Press the right arrow key to select the gamma tab + await press.ArrowRight(); - // re-enable all tabs - await rerender( - - ); + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); } ); } ); - describe( 'When `selectedId` is changed by the controlling component', () => { + + describe( 'When `selectedId` is changed by the controlling component [Controlled]', () => { describe.each( [ true, false ] )( 'and `selectOnMove` is %s', ( selectOnMove ) => { @@ -1231,17 +1443,18 @@ describe( 'Tabs', () => { /> ); - expect( await getSelectedTab() ).toHaveTextContent( + // Beta is the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Beta' ); // Tab key should focus the currently selected tab, which is Beta. await press.Tab(); - expect( await getSelectedTab() ).toHaveTextContent( - 'Beta' - ); expect( - screen.getByRole( 'tab', { name: 'Beta' } ) + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) ).toHaveFocus(); await rerender( @@ -1253,17 +1466,28 @@ describe( 'Tabs', () => { ); // When the selected tab is changed, focus should not be changed. - expect( await getSelectedTab() ).toHaveTextContent( - 'Gamma' - ); expect( - screen.getByRole( 'tab', { name: 'Beta' } ) + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tab', { + selected: false, + name: 'Beta', + } ) ).toHaveFocus(); - // Arrow keys should move focus to the next tab, which is Gamma - await press.ArrowRight(); + // Arrow left should move focus to the previous tab (alpha). + // The alpha tab should be always focused, and should be selected + // when the `selectOnMove` prop is set to `true`. + await press.ArrowLeft(); expect( - screen.getByRole( 'tab', { name: 'Gamma' } ) + screen.getByRole( 'tab', { + selected: selectOnMove, + name: 'Alpha', + } ) ).toHaveFocus(); } ); @@ -1279,20 +1503,22 @@ describe( 'Tabs', () => { ); - expect( await getSelectedTab() ).toHaveTextContent( + // Beta is the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( 'Beta' ); // Tab key should focus the currently selected tab, which is Beta. await press.Tab(); await press.Tab(); - expect( await getSelectedTab() ).toHaveTextContent( - 'Beta' - ); expect( - screen.getByRole( 'tab', { name: 'Beta' } ) + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) ).toHaveFocus(); + // Change the selected tab to gamma via a controlled update. await rerender( <> @@ -1305,12 +1531,17 @@ describe( 'Tabs', () => { ); // When the selected tab is changed, it should not automatically receive focus. - expect( await getSelectedTab() ).toHaveTextContent( - 'Gamma' - ); - expect( - screen.getByRole( 'tab', { name: 'Beta' } ) + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tab', { + selected: false, + name: 'Beta', + } ) ).toHaveFocus(); // Press shift+tab, move focus to the button before Tabs @@ -1336,125 +1567,439 @@ describe( 'Tabs', () => { } ); } ); + } ); - describe( 'When `selectOnMove` is `true`', () => { - it( 'should automatically select a newly focused tab', async () => { - await render( - - ); + describe( 'miscellaneous runtime changes', () => { + describe( 'removing a tab', () => { + describe( 'with no explicitly set initial tab', () => { + it( 'should not select a new tab when the selected tab is removed', async () => { + const mockOnSelect = jest.fn(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + const { rerender } = await render( + + ); - await press.Tab(); + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( + 'Alpha' + ); - // Tab key should focus the currently selected tab, which is Beta. - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - expect( await getSelectedTab() ).toHaveFocus(); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); - // Arrow keys should select and move focus to the next tab. - await press.ArrowRight(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( await getSelectedTab() ).toHaveFocus(); + // Select gamma + await click( screen.getByRole( 'tab', { name: 'Gamma' } ) ); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); + + // Remove gamma + await rerender( + + ); + + expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 ); + + // No tab should be selected i.e. it doesn't fall back to gamma, + // even if it matches the `defaultTabId` prop. + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + // No tabpanel should be rendered either + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + } ); } ); + + describe.each( [ + [ 'defaultTabId', 'Uncontrolled', UncontrolledTabs ], + [ 'selectedTabId', 'Controlled', ControlledTabs ], + ] )( + 'when using the `%s` prop [%s]', + ( propName, _mode, Component ) => { + it( 'should not select a new tab when the selected tab is removed', async () => { + const mockOnSelect = jest.fn(); + + const initialComponentProps = { + tabs: TABS, + [ propName ]: 'gamma', + onSelect: mockOnSelect, + }; + + const { rerender } = await render( + + ); + + // Gamma is the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( + 'Gamma' + ); + + // Remove gamma + await rerender( + + ); + + expect( screen.getAllByRole( 'tab' ) ).toHaveLength( + 2 + ); + // No tab should be selected i.e. it doesn't fall back to first tab. + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + // No tabpanel should be rendered either + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument(); + + // Re-add gamma. Gamma becomes selected again. + await rerender( + + ); + + expect( screen.getAllByRole( 'tab' ) ).toHaveLength( + TABS.length + ); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Gamma', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Gamma', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).not.toHaveBeenCalled(); + } ); + + it( `should not select the tab matching the \`${ propName }\` prop as a fallback when the selected tab is removed`, async () => { + const mockOnSelect = jest.fn(); + + const initialComponentProps = { + tabs: TABS, + [ propName ]: 'gamma', + onSelect: mockOnSelect, + }; + + const { rerender } = await render( + + ); + + // Gamma is the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( + 'Gamma' + ); + + // Select alpha + await click( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( + 'alpha' + ); + + // Remove alpha + await rerender( + + ); + + expect( screen.getAllByRole( 'tab' ) ).toHaveLength( + 2 + ); + + // No tab should be selected i.e. it doesn't fall back to gamma, + // even if it matches the `defaultTabId` prop. + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + // No tabpanel should be rendered either + expect( + screen.queryByRole( 'tabpanel' ) + ).not.toBeInTheDocument(); + + // Re-add alpha. Alpha becomes selected again. + await rerender( + + ); + + expect( screen.getAllByRole( 'tab' ) ).toHaveLength( + TABS.length + ); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Alpha', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Alpha', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + } ); + } + ); } ); - describe( 'When `selectOnMove` is `false`', () => { - it( 'should apply focus without automatically changing the selected tab', async () => { - await render( - - ); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + describe( 'adding a tab', () => { + describe.each( [ + [ 'defaultTabId', 'Uncontrolled', UncontrolledTabs ], + [ 'selectedTabId', 'Controlled', ControlledTabs ], + ] )( + 'when using the `%s` prop [%s]', + ( propName, _mode, Component ) => { + it( `should select a newly added tab if it matches the \`${ propName }\` prop`, async () => { + const mockOnSelect = jest.fn(); + + const initialComponentProps = { + tabs: TABS, + [ propName ]: 'delta', + onSelect: mockOnSelect, + }; - // Tab key should focus the currently selected tab, which is Beta. - await press.Tab(); - await waitFor( async () => - expect( - await screen.findByRole( 'tab', { name: 'Beta' } ) - ).toHaveFocus() - ); + const { rerender } = await render( + + ); - // Arrow key should move focus but not automatically change the selected tab. - await press.ArrowRight(); - expect( - screen.getByRole( 'tab', { name: 'Gamma' } ) - ).toHaveFocus(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + // No initially selected tabs or tabpanels, since the `defaultTabId` + // prop is not matching any known tabs. + await waitForComponentToBeInitializedWithSelectedTab( + undefined + ); - // Pressing the spacebar should select the focused tab. - await press.Space(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( mockOnSelect ).not.toHaveBeenCalled(); - // Arrow key should move focus but not automatically change the selected tab. - await press.ArrowRight(); - expect( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus(); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + // Re-render with beta disabled. + await rerender( + + ); - // Pressing the enter/return should select the focused tab. - await press.Enter(); - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - } ); + // Delta becomes selected + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Delta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Delta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).not.toHaveBeenCalled(); + } ); + } + ); } ); - } ); - it( 'should associate each `Tab` with the correct `TabPanel`, even if they are not rendered in the same order', async () => { - const TABS_WITH_DELTA_REVERSED = [ ...TABS_WITH_DELTA ].reverse(); - - await render( - - - { TABS_WITH_DELTA.map( ( tabObj ) => ( - - { tabObj.title } - - ) ) } - - { TABS_WITH_DELTA_REVERSED.map( ( tabObj ) => ( - - { tabObj.content } - - ) ) } - - ); + describe( 'a tab becomes disabled', () => { + describe.each( [ + [ 'defaultTabId', 'Uncontrolled', UncontrolledTabs ], + [ 'selectedTabId', 'Controlled', ControlledTabs ], + ] )( + 'when using the `%s` prop [%s]', + ( propName, _mode, Component ) => { + it( `should keep the initial tab matching the \`${ propName }\` prop as selected even if it becomes disabled`, async () => { + const mockOnSelect = jest.fn(); + + const initialComponentProps = { + tabs: TABS, + [ propName ]: 'beta', + onSelect: mockOnSelect, + }; - // Alpha is the initially selected tab,and should render the correct tabpanel - expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); - expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( - 'Selected tab: Alpha' - ); + const { rerender } = await render( + + ); - // Select Beta, make sure the correct tabpanel is rendered - await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( - 'Selected tab: Beta' - ); + // Beta is the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( + 'Beta' + ); - // Select Gamma, make sure the correct tabpanel is rendered - await click( screen.getByRole( 'tab', { name: 'Gamma' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( - 'Selected tab: Gamma' - ); + expect( mockOnSelect ).not.toHaveBeenCalled(); - // Select Delta, make sure the correct tabpanel is rendered - await click( screen.getByRole( 'tab', { name: 'Delta' } ) ); - expect( await getSelectedTab() ).toHaveTextContent( 'Delta' ); - expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( - 'Selected tab: Delta' - ); + // Re-render with beta disabled. + await rerender( + + ); + + // Beta continues to be selected and focused, even if it is disabled. + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + // Re-enable beta. + await rerender( + + ); + + // Beta continues to be selected and focused. + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).not.toHaveBeenCalled(); + } ); + + it( 'should keep the current tab selected by the user as selected even if it becomes disabled', async () => { + const mockOnSelect = jest.fn(); + + const { rerender } = await render( + + ); + + // Alpha is automatically selected as the selected tab. + await waitForComponentToBeInitializedWithSelectedTab( + 'Alpha' + ); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( + 'alpha' + ); + + // Click on beta tab, beta becomes selected. + await click( + screen.getByRole( 'tab', { name: 'Beta' } ) + ); + + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( + 'beta' + ); + + // Re-render with beta disabled. + await rerender( + + ); + + // Beta continues to be selected, even if it is disabled. + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toHaveFocus(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + // Re-enable beta. + await rerender( + + ); + + // Beta continues to be selected and focused. + expect( + screen.getByRole( 'tab', { + selected: true, + name: 'Beta', + } ) + ).toBeVisible(); + expect( + screen.getByRole( 'tabpanel', { + name: 'Beta', + } ) + ).toBeVisible(); + + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + } ); + } + ); + } ); } ); } ); From 6f541e75ddc8c4601171f220ee17458249aa8408 Mon Sep 17 00:00:00 2001 From: James Koster Date: Fri, 29 Nov 2024 09:45:31 +0000 Subject: [PATCH 061/444] Sidebar: Update appearance of active items (#67318) Unlinked contributors: danielvann777. Co-authored-by: jameskoster Co-authored-by: jasmussen Co-authored-by: fcoveram Co-authored-by: richtabor Co-authored-by: ptesei Co-authored-by: joedolson --- .../edit-site/src/components/sidebar-dataviews/style.scss | 4 ++-- .../src/components/sidebar-navigation-item/style.scss | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-dataviews/style.scss b/packages/edit-site/src/components/sidebar-dataviews/style.scss index 14e6bf1d03fca..a36d693c4f80e 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/style.scss +++ b/packages/edit-site/src/components/sidebar-dataviews/style.scss @@ -19,11 +19,11 @@ &:focus, &[aria-current] { color: $gray-200; - background: $gray-800; } &.is-selected { - background: var(--wp-admin-theme-color); + background: $gray-800; + font-weight: $font-weight-medium; color: $white; } } diff --git a/packages/edit-site/src/components/sidebar-navigation-item/style.scss b/packages/edit-site/src/components/sidebar-navigation-item/style.scss index 202de5300076c..ac1cf8b730861 100644 --- a/packages/edit-site/src/components/sidebar-navigation-item/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-item/style.scss @@ -9,7 +9,6 @@ &:focus, &[aria-current="true"] { color: $gray-200; - background: $gray-800; .edit-site-sidebar-navigation-item__drilldown-indicator { fill: $gray-200; @@ -17,7 +16,7 @@ } &[aria-current="true"] { - background: var(--wp-admin-theme-color); + background: $gray-800; color: $white; } From aee29cb7d56c7aea6c03c4bd2a9847c87a8c5eaf Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 29 Nov 2024 11:13:25 +0100 Subject: [PATCH 062/444] Update @ariakit/react to 0.4.15 and @ariakit/test to 0.4.7 (#67404) * Remove ariakit dependencies * Update to latest version of ariakit * CHANGELOG * Dataviews CHANGELOG --- Co-authored-by: ciampo Co-authored-by: tyxla --- package-lock.json | 135 +++++++++++-------------------- package.json | 2 +- packages/components/CHANGELOG.md | 1 + packages/components/package.json | 2 +- packages/dataviews/CHANGELOG.md | 5 ++ packages/dataviews/package.json | 2 +- 6 files changed, 54 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58479ecfa2ed9..98865c9d041a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@actions/core": "1.9.1", "@actions/github": "5.0.0", "@apidevtools/json-schema-ref-parser": "11.6.4", - "@ariakit/test": "^0.4.5", + "@ariakit/test": "^0.4.7", "@babel/core": "7.25.7", "@babel/plugin-syntax-jsx": "7.25.7", "@babel/runtime-corejs3": "7.25.7", @@ -1432,14 +1432,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ariakit/core": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.14.tgz", + "integrity": "sha512-hpzZvyYzGhP09S9jW1XGsU/FD5K3BKsH1eG/QJ8rfgEeUdPS7BvHPt5lHbOeJ2cMrRzBEvsEzLi1ivfDifHsVA==", + "license": "MIT" + }, + "node_modules/@ariakit/react": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.15.tgz", + "integrity": "sha512-0V2LkNPFrGRT+SEIiObx/LQjR6v3rR+mKEDUu/3tq7jfCZ+7+6Q6EMR1rFaK+XMkaRY1RWUcj/rRDWAUWnsDww==", + "license": "MIT", + "dependencies": { + "@ariakit/react-core": "0.4.15" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.15.tgz", + "integrity": "sha512-Up8+U97nAPJdyUh9E8BCEhJYTA+eVztWpHoo1R9zZfHd4cnBWAg5RHxEmMH+MamlvuRxBQA71hFKY/735fDg+A==", + "license": "MIT", + "dependencies": { + "@ariakit/core": "0.4.14", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@ariakit/test": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.5.tgz", - "integrity": "sha512-dK9OtI8MeKfdtOiW1auDITnyaelq0O0aUTnolIqJj+RJd8LFai0gi7fQUgrun9CZHJ2wWsEad4vlviGfhfIIhQ==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.7.tgz", + "integrity": "sha512-Zb5bnulzYGjr6sDubxOeOhk5Es6BYQq5lbcIe8xNrWUlpRiHsje/FlXNFpHnI92/7ESxH6X4pHhbb+qFAho1lw==", "dev": true, "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.12", + "@ariakit/core": "0.4.14", "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { @@ -1459,13 +1497,6 @@ } } }, - "node_modules/@ariakit/test/node_modules/@ariakit/core": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.12.tgz", - "integrity": "sha512-+NNpy88tdP/w9mOBPuDrMTbtapPbo/8yVIzpQB7TAmN0sPh/Cq3nU1f2KCTCIujPmwRvAcMSW9UHOlFmbKEPOA==", - "dev": true, - "license": "MIT" - }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -53681,7 +53712,7 @@ "version": "28.13.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.4.13", + "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -53736,44 +53767,6 @@ "react-dom": "^18.0.0" } }, - "packages/components/node_modules/@ariakit/core": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.12.tgz", - "integrity": "sha512-+NNpy88tdP/w9mOBPuDrMTbtapPbo/8yVIzpQB7TAmN0sPh/Cq3nU1f2KCTCIujPmwRvAcMSW9UHOlFmbKEPOA==", - "license": "MIT" - }, - "packages/components/node_modules/@ariakit/react": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.13.tgz", - "integrity": "sha512-pTGYgoqCojfyt2xNJ5VQhejxXwwtcP7VDDqcnnVChv7TA2TWWyYerJ5m4oxViI1pgeNqnHZwKlQ79ZipF7W2kQ==", - "license": "MIT", - "dependencies": { - "@ariakit/react-core": "0.4.13" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "packages/components/node_modules/@ariakit/react-core": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.13.tgz", - "integrity": "sha512-iIjQeupP9d0pOubOzX4a0UPXbhXbp0ZCduDpkv7+u/pYP/utk/YRECD0M/QpZr6YSeltmDiNxKjdyK8r9Yhv4Q==", - "license": "MIT", - "dependencies": { - "@ariakit/core": "0.4.12", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "packages/components/node_modules/@floating-ui/react-dom": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", @@ -54051,7 +54044,7 @@ "version": "4.9.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.4.13", + "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", "@wordpress/components": "*", "@wordpress/compose": "*", @@ -54073,44 +54066,6 @@ "react": "^18.0.0" } }, - "packages/dataviews/node_modules/@ariakit/core": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.12.tgz", - "integrity": "sha512-+NNpy88tdP/w9mOBPuDrMTbtapPbo/8yVIzpQB7TAmN0sPh/Cq3nU1f2KCTCIujPmwRvAcMSW9UHOlFmbKEPOA==", - "license": "MIT" - }, - "packages/dataviews/node_modules/@ariakit/react": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.13.tgz", - "integrity": "sha512-pTGYgoqCojfyt2xNJ5VQhejxXwwtcP7VDDqcnnVChv7TA2TWWyYerJ5m4oxViI1pgeNqnHZwKlQ79ZipF7W2kQ==", - "license": "MIT", - "dependencies": { - "@ariakit/react-core": "0.4.13" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "packages/dataviews/node_modules/@ariakit/react-core": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.13.tgz", - "integrity": "sha512-iIjQeupP9d0pOubOzX4a0UPXbhXbp0ZCduDpkv7+u/pYP/utk/YRECD0M/QpZr6YSeltmDiNxKjdyK8r9Yhv4Q==", - "license": "MIT", - "dependencies": { - "@ariakit/core": "0.4.12", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "packages/date": { "name": "@wordpress/date", "version": "5.13.0", diff --git a/package.json b/package.json index 84425dbd1cff2..46a04a52aa607 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@actions/core": "1.9.1", "@actions/github": "5.0.0", "@apidevtools/json-schema-ref-parser": "11.6.4", - "@ariakit/test": "^0.4.5", + "@ariakit/test": "^0.4.7", "@babel/core": "7.25.7", "@babel/plugin-syntax-jsx": "7.25.7", "@babel/runtime-corejs3": "7.25.7", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 37da311b0547a..8fc7aff329b03 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ ### Internal - Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). +- Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)). ## 28.13.0 (2024-11-27) diff --git a/packages/components/package.json b/packages/components/package.json index 75f0d1eb1f233..a2acf8e2c203d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -32,7 +32,7 @@ "src/**/*.scss" ], "dependencies": { - "@ariakit/react": "^0.4.13", + "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 72f08b987a386..7ec1b24f8745c 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +## Internal + +- Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). +- Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)). + ## 4.9.0 (2024-11-27) ### Bug Fixes diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 8fe2e04236725..c2d16b664c904 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -43,7 +43,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@ariakit/react": "^0.4.13", + "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", "@wordpress/components": "*", "@wordpress/compose": "*", From e19a6f0175805720c611c0c74347652ee3ce546c Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Fri, 29 Nov 2024 12:21:12 +0200 Subject: [PATCH 063/444] Remove PostSlugCheck and PostSlug unused components (#67414) Co-authored-by: ntsekouras Co-authored-by: Mamaduka Co-authored-by: youknowriad --- packages/editor/README.md | 21 ------ packages/editor/src/components/index.js | 2 - .../editor/src/components/post-slug/check.js | 20 ----- .../editor/src/components/post-slug/index.js | 73 ------------------- .../editor/src/components/post-slug/panel.js | 22 ------ .../src/components/post-slug/style.scss | 5 -- .../src/components/post-slug/test/index.js | 53 -------------- packages/editor/src/style.scss | 1 - 8 files changed, 197 deletions(-) delete mode 100644 packages/editor/src/components/post-slug/check.js delete mode 100644 packages/editor/src/components/post-slug/index.js delete mode 100644 packages/editor/src/components/post-slug/panel.js delete mode 100644 packages/editor/src/components/post-slug/style.scss delete mode 100644 packages/editor/src/components/post-slug/test/index.js diff --git a/packages/editor/README.md b/packages/editor/README.md index ac655bd1c99d8..36126cb8eaee3 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -1312,27 +1312,6 @@ _Returns_ - `React.ReactNode`: The rendered component. -### PostSlug - -Renders the PostSlug component. It provide a control for editing the post slug. - -_Returns_ - -- `React.ReactNode`: The rendered component. - -### PostSlugCheck - -Wrapper component that renders its children only if the post type supports the slug. - -_Parameters_ - -- _props_ `Object`: Props. -- _props.children_ `React.ReactNode`: Children to be rendered. - -_Returns_ - -- `React.ReactNode`: The rendered component. - ### PostSticky Renders the PostSticky component. It provides a checkbox control for the sticky post feature. diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index b42566aac653b..d940532be75a3 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -68,8 +68,6 @@ export { usePostScheduleLabel, } from './post-schedule/label'; export { default as PostSchedulePanel } from './post-schedule/panel'; -export { default as PostSlug } from './post-slug'; -export { default as PostSlugCheck } from './post-slug/check'; export { default as PostSticky } from './post-sticky'; export { default as PostStickyCheck } from './post-sticky/check'; export { default as PostSwitchToDraftButton } from './post-switch-to-draft-button'; diff --git a/packages/editor/src/components/post-slug/check.js b/packages/editor/src/components/post-slug/check.js deleted file mode 100644 index 8ca7078a1a9e2..0000000000000 --- a/packages/editor/src/components/post-slug/check.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Internal dependencies - */ -import PostTypeSupportCheck from '../post-type-support-check'; - -/** - * Wrapper component that renders its children only if the post type supports the slug. - * - * @param {Object} props Props. - * @param {React.ReactNode} props.children Children to be rendered. - * - * @return {React.ReactNode} The rendered component. - */ -export default function PostSlugCheck( { children } ) { - return ( - - { children } - - ); -} diff --git a/packages/editor/src/components/post-slug/index.js b/packages/editor/src/components/post-slug/index.js deleted file mode 100644 index afff7f361ea42..0000000000000 --- a/packages/editor/src/components/post-slug/index.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * WordPress dependencies - */ -import { useDispatch, useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { safeDecodeURIComponent, cleanForSlug } from '@wordpress/url'; -import { TextControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import PostSlugCheck from './check'; -import { store as editorStore } from '../../store'; - -function PostSlugControl() { - const postSlug = useSelect( ( select ) => { - return safeDecodeURIComponent( - select( editorStore ).getEditedPostSlug() - ); - }, [] ); - const { editPost } = useDispatch( editorStore ); - const [ forceEmptyField, setForceEmptyField ] = useState( false ); - - return ( - { - editPost( { slug: newValue } ); - // When we delete the field the permalink gets - // reverted to the original value. - // The forceEmptyField logic allows the user to have - // the field temporarily empty while typing. - if ( ! newValue ) { - if ( ! forceEmptyField ) { - setForceEmptyField( true ); - } - return; - } - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - onBlur={ ( event ) => { - editPost( { - slug: cleanForSlug( event.target.value ), - } ); - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - className="editor-post-slug" - /> - ); -} - -/** - * Renders the PostSlug component. It provide a control for editing the post slug. - * - * @return {React.ReactNode} The rendered component. - */ -export default function PostSlug() { - return ( - - - - ); -} diff --git a/packages/editor/src/components/post-slug/panel.js b/packages/editor/src/components/post-slug/panel.js deleted file mode 100644 index 6ab97a28b251c..0000000000000 --- a/packages/editor/src/components/post-slug/panel.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * WordPress dependencies - */ -import { PanelRow } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import PostSlugForm from './'; -import PostSlugCheck from './check'; - -export function PostSlug() { - return ( - - - - - - ); -} - -export default PostSlug; diff --git a/packages/editor/src/components/post-slug/style.scss b/packages/editor/src/components/post-slug/style.scss deleted file mode 100644 index 551450582128e..0000000000000 --- a/packages/editor/src/components/post-slug/style.scss +++ /dev/null @@ -1,5 +0,0 @@ -.editor-post-slug { - display: flex; - flex-direction: column; - align-items: stretch; -} diff --git a/packages/editor/src/components/post-slug/test/index.js b/packages/editor/src/components/post-slug/test/index.js deleted file mode 100644 index fb40055111b77..0000000000000 --- a/packages/editor/src/components/post-slug/test/index.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * External dependencies - */ -import { act, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import PostSlug from '../'; - -jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); -jest.mock( '@wordpress/data/src/components/use-dispatch/use-dispatch', () => - jest.fn() -); - -describe( 'PostSlug', () => { - it( 'should update slug with sanitized input', async () => { - const user = userEvent.setup(); - const editPost = jest.fn(); - - useSelect.mockImplementation( ( mapSelect ) => - mapSelect( () => ( { - getPostType: () => ( { - supports: { - slug: true, - }, - } ), - getEditedPostAttribute: () => 'post', - getEditedPostSlug: () => '1', - } ) ) - ); - useDispatch.mockImplementation( () => ( { - editPost, - } ) ); - - render( ); - - const input = screen.getByRole( 'textbox', { name: 'Slug' } ); - await user.type( input, '2', { - initialSelectionStart: 0, - initialSelectionEnd: 1, - } ); - act( () => input.blur() ); - - expect( editPost ).toHaveBeenCalledWith( { slug: '2' } ); - } ); -} ); diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 88d722867d009..1504211a51e89 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -34,7 +34,6 @@ @import "./components/post-publish-panel/style.scss"; @import "./components/post-saved-state/style.scss"; @import "./components/post-schedule/style.scss"; -@import "./components/post-slug/style.scss"; @import "./components/post-status/style.scss"; @import "./components/post-sticky/style.scss"; @import "./components/post-sync-status/style.scss"; From 96647ef634baa224ebed57415b1cc3f4a0b0956e Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Fri, 29 Nov 2024 14:58:55 +0400 Subject: [PATCH 064/444] Editor: Use hooks instead of HOC in 'PostPublishButtonOrToggle' (#67413) Co-authored-by: Mamaduka Co-authored-by: ntsekouras --- .../post-publish-button-or-toggle.js | 78 +++++++++---------- .../test/post-publish-button-or-toggle.js | 33 +++++--- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/packages/editor/src/components/post-publish-button/post-publish-button-or-toggle.js b/packages/editor/src/components/post-publish-button/post-publish-button-or-toggle.js index bf742bef1429b..c3a355d243f34 100644 --- a/packages/editor/src/components/post-publish-button/post-publish-button-or-toggle.js +++ b/packages/editor/src/components/post-publish-button/post-publish-button-or-toggle.js @@ -1,8 +1,8 @@ /** * WordPress dependencies */ -import { useViewportMatch, compose } from '@wordpress/compose'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { useViewportMatch } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -10,24 +10,46 @@ import { withDispatch, withSelect } from '@wordpress/data'; import PostPublishButton from './index'; import { store as editorStore } from '../../store'; -export function PostPublishButtonOrToggle( { +const IS_TOGGLE = 'toggle'; +const IS_BUTTON = 'button'; + +export default function PostPublishButtonOrToggle( { forceIsDirty, - hasPublishAction, - isBeingScheduled, - isPending, - isPublished, - isPublishSidebarEnabled, - isPublishSidebarOpened, - isScheduled, - togglePublishSidebar, setEntitiesSavedStatesCallback, - postStatusHasChanged, - postStatus, } ) { - const IS_TOGGLE = 'toggle'; - const IS_BUTTON = 'button'; - const isSmallerThanMediumViewport = useViewportMatch( 'medium', '<' ); let component; + const isSmallerThanMediumViewport = useViewportMatch( 'medium', '<' ); + const { togglePublishSidebar } = useDispatch( editorStore ); + const { + hasPublishAction, + isBeingScheduled, + isPending, + isPublished, + isPublishSidebarEnabled, + isPublishSidebarOpened, + isScheduled, + postStatus, + postStatusHasChanged, + } = useSelect( ( select ) => { + return { + hasPublishAction: + !! select( editorStore ).getCurrentPost()?._links?.[ + 'wp:action-publish' + ] ?? false, + isBeingScheduled: + select( editorStore ).isEditedPostBeingScheduled(), + isPending: select( editorStore ).isCurrentPostPending(), + isPublished: select( editorStore ).isCurrentPostPublished(), + isPublishSidebarEnabled: + select( editorStore ).isPublishSidebarEnabled(), + isPublishSidebarOpened: + select( editorStore ).isPublishSidebarOpened(), + isScheduled: select( editorStore ).isCurrentPostScheduled(), + postStatus: + select( editorStore ).getEditedPostAttribute( 'status' ), + postStatusHasChanged: select( editorStore ).getPostEdits()?.status, + }; + }, [] ); /** * Conditions to show a BUTTON (publish directly) or a TOGGLE (open publish sidebar): @@ -76,27 +98,3 @@ export function PostPublishButtonOrToggle( { /> ); } - -export default compose( - withSelect( ( select ) => ( { - hasPublishAction: - select( editorStore ).getCurrentPost()?._links?.[ - 'wp:action-publish' - ] ?? false, - isBeingScheduled: select( editorStore ).isEditedPostBeingScheduled(), - isPending: select( editorStore ).isCurrentPostPending(), - isPublished: select( editorStore ).isCurrentPostPublished(), - isPublishSidebarEnabled: - select( editorStore ).isPublishSidebarEnabled(), - isPublishSidebarOpened: select( editorStore ).isPublishSidebarOpened(), - isScheduled: select( editorStore ).isCurrentPostScheduled(), - postStatus: select( editorStore ).getEditedPostAttribute( 'status' ), - postStatusHasChanged: select( editorStore ).getPostEdits()?.status, - } ) ), - withDispatch( ( dispatch ) => { - const { togglePublishSidebar } = dispatch( editorStore ); - return { - togglePublishSidebar, - }; - } ) -)( PostPublishButtonOrToggle ); diff --git a/packages/editor/src/components/post-publish-button/test/post-publish-button-or-toggle.js b/packages/editor/src/components/post-publish-button/test/post-publish-button-or-toggle.js index 0794c3c8995a1..a8fa8b72db9c7 100644 --- a/packages/editor/src/components/post-publish-button/test/post-publish-button-or-toggle.js +++ b/packages/editor/src/components/post-publish-button/test/post-publish-button-or-toggle.js @@ -7,13 +7,15 @@ import { render, screen } from '@testing-library/react'; * WordPress dependencies */ import { useViewportMatch } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { PostPublishButtonOrToggle } from '../post-publish-button-or-toggle'; +import PostPublishButtonOrToggle from '../post-publish-button-or-toggle'; jest.mock( '@wordpress/compose/src/hooks/use-viewport-match' ); +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); describe( 'PostPublishButtonOrToggle should render a', () => { afterEach( () => { @@ -21,23 +23,32 @@ describe( 'PostPublishButtonOrToggle should render a', () => { } ); it( 'button when the post is published (1)', () => { - render( ); + useSelect.mockImplementation( () => ( { + isPublished: true, + } ) ); + render( ); expect( screen.getByRole( 'button', { name: 'Submit for Review' } ) ).toBeVisible(); } ); it( 'button when the post is scheduled (2)', () => { - render( ); + useSelect.mockImplementation( () => ( { + isScheduled: true, + isBeingScheduled: true, + } ) ); + render( ); expect( screen.getByRole( 'button', { name: 'Submit for Review' } ) ).toBeVisible(); } ); it( 'button when the post is pending and cannot be published but the viewport is >= medium (3)', () => { - render( - - ); + useSelect.mockImplementation( () => ( { + isPending: true, + hasPublishAction: false, + } ) ); + render( ); expect( screen.getByRole( 'button', { name: 'Submit for Review' } ) @@ -46,6 +57,9 @@ describe( 'PostPublishButtonOrToggle should render a', () => { it( 'toggle when post is not (1), (2), (3), the viewport is <= medium, and the publish sidebar is enabled', () => { useViewportMatch.mockReturnValue( true ); + useSelect.mockImplementation( () => ( { + isPublishSidebarEnabled: true, + } ) ); render( ); expect( screen.getByRole( 'button', { name: 'Publish' } ) @@ -53,9 +67,10 @@ describe( 'PostPublishButtonOrToggle should render a', () => { } ); it( 'button when post is not (1), (2), (3), the viewport is >= medium, and the publish sidebar is disabled', () => { - render( - - ); + useSelect.mockImplementation( () => ( { + isPublishSidebarEnabled: false, + } ) ); + render( ); expect( screen.getByRole( 'button', { name: 'Submit for Review' } ) ).toBeVisible(); From 52b5429f52096c5e080b3cc5a2ff696e61cb7616 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:03:02 +0100 Subject: [PATCH 065/444] [mini] drag and drop: restore moving animation (#67417) Co-authored-by: ellatrix Co-authored-by: youknowriad --- .../components/use-moving-animation/index.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index ef367c0f33210..b11710acd2433 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -74,8 +74,14 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { const isSelected = isBlockSelected( clientId ); const adjustScrolling = isSelected || isFirstMultiSelectedBlock( clientId ); + const isDragging = isDraggingBlocks(); function preserveScrollPosition() { + // The user already scrolled when dragging blocks. + if ( isDragging ) { + return; + } + if ( adjustScrolling && prevRect ) { const blockRect = ref.current.getBoundingClientRect(); const diff = blockRect.top - prevRect.top; @@ -86,11 +92,6 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { } } - // Neither animate nor scroll. - if ( isDraggingBlocks() ) { - return; - } - // We disable the animation if the user has a preference for reduced // motion, if the user is typing (insertion by Enter), or if the block // count exceeds the threshold (insertion caused all the blocks that @@ -113,6 +114,13 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { isSelected || isBlockMultiSelected( clientId ) || isAncestorMultiSelected( clientId ); + + // The user already dragged the blocks to the new position, so don't + // animate the dragged blocks. + if ( isPartOfSelection && isDragging ) { + return; + } + // Make sure the other blocks move under the selected block(s). const zIndex = isPartOfSelection ? '1' : ''; From 9ec599804382de27b527866b54719929b7651511 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:06:55 +0900 Subject: [PATCH 066/444] Fix color of disabled buttons in dark toolbar (#67348) Co-authored-by: t-hamano Co-authored-by: jasmussen --- packages/block-editor/src/components/block-tools/style.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 0ed9c138519d2..80fe4c420d1e1 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -157,6 +157,11 @@ &:focus::before { box-shadow: inset 0 0 0 1px $gray-900, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } + + &:disabled, + &[aria-disabled="true"] { + color: $gray-700; + } } .block-editor-block-parent-selector .block-editor-block-parent-selector__button { From f189eab30fc68b71728e17b50a089b2aa42a2277 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:02:17 +0900 Subject: [PATCH 067/444] BorderBoxControl: Reduce gap value when unlinked (#67049) * BorderBoxControl: Reduce input field width when unlinked * Update changelog * 108px * Reduce gap value * Fix changelog entry Co-authored-by: t-hamano Co-authored-by: tyxla Co-authored-by: jasmussen Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: jameskoster --- packages/components/CHANGELOG.md | 4 ++++ .../border-box-control-split-controls/component.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8fc7aff329b03..ce151116df5bf 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,6 +7,10 @@ - `BoxControl`: Passive deprecate `onMouseOver`/`onMouseOut`. Pass to the `inputProps` prop instead ([#67332](https://github.com/WordPress/gutenberg/pull/67332)). - `BoxControl`: Deprecate 36px default size ([#66704](https://github.com/WordPress/gutenberg/pull/66704)). +### Enhancements + +- `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). + ### Experimental - `Menu`: throw when subcomponents are not rendered inside top level `Menu` ([#67411](https://github.com/WordPress/gutenberg/pull/67411)). diff --git a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx index 0c887ab5f701c..94e1728076b18 100644 --- a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx +++ b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx @@ -73,7 +73,7 @@ const BorderBoxControlSplitControls = ( const mergedRef = useMergeRefs( [ setPopoverAnchor, forwardedRef ] ); return ( - + Date: Fri, 29 Nov 2024 23:48:46 +0900 Subject: [PATCH 068/444] FontFamilyControl: Restore margin bottom (#67424) Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla Co-authored-by: matiasbenedetto --- .../block-editor/src/components/font-family/index.js | 9 +++++++++ .../block-editor/src/components/font-family/style.scss | 5 +++++ packages/block-editor/src/style.scss | 1 + 3 files changed, 15 insertions(+) create mode 100644 packages/block-editor/src/components/font-family/style.scss diff --git a/packages/block-editor/src/components/font-family/index.js b/packages/block-editor/src/components/font-family/index.js index 1e6c137daedb0..045d4d5c73ed3 100644 --- a/packages/block-editor/src/components/font-family/index.js +++ b/packages/block-editor/src/components/font-family/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + /** * WordPress dependencies */ @@ -18,6 +23,7 @@ export default function FontFamilyControl( { value = '', onChange, fontFamilies, + className, ...props } ) { const [ blockLevelFontFamilies ] = useSettings( 'typography.fontFamilies' ); @@ -59,6 +65,9 @@ export default function FontFamilyControl( { value={ value } onChange={ ( { selectedItem } ) => onChange( selectedItem.key ) } options={ options } + className={ clsx( 'block-editor-font-family-control', className, { + 'is-next-has-no-margin-bottom': __nextHasNoMarginBottom, + } ) } { ...props } /> ); diff --git a/packages/block-editor/src/components/font-family/style.scss b/packages/block-editor/src/components/font-family/style.scss new file mode 100644 index 0000000000000..7ee181ebb7953 --- /dev/null +++ b/packages/block-editor/src/components/font-family/style.scss @@ -0,0 +1,5 @@ +.block-editor-font-family-control { + &:not(.is-next-has-no-margin-bottom) { + margin-bottom: $grid-unit-10; + } +} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 213498c797aee..6b2ebf5cd841f 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -28,6 +28,7 @@ @import "./components/date-format-picker/style.scss"; @import "./components/duotone-control/style.scss"; @import "./components/font-appearance-control/style.scss"; +@import "./components/font-family/style.scss"; @import "./components/global-styles/style.scss"; @import "./components/grid/style.scss"; @import "./components/height-control/style.scss"; From 2c88f6a1b674c78bbbea5dc41aac998970ef9c2e Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 29 Nov 2024 15:16:15 +0000 Subject: [PATCH 069/444] Fix: Caption with Link in Wide-Width and Full-Width Images Appears on two lines (#67392) Co-authored-by: jorgefilipecosta Co-authored-by: youknowriad Co-authored-by: t-hamano --- packages/block-library/src/image/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 1bb19bf29da69..a7fcb8f175e4e 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -42,8 +42,8 @@ text-align: center; } - &.alignfull a, - &.alignwide a { + &.alignfull > a, + &.alignwide > a { width: 100%; } From edd6328b3ff9cefc5550878f90fae885f33c8b27 Mon Sep 17 00:00:00 2001 From: Hit Bhalodia <58802366+hbhalodia@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:52:27 +0530 Subject: [PATCH 070/444] UnitControl : Deprecate 36px default size (#66791) * Add the console warning for 36px size variation * Add the changelog for the deprecation * Update the unit test for the unitcontrol to use default 40px size * Use __shouldNotWarnDeprecated36pxSize to not throw redundant warning from parent component used * Add the missing prop for __next40pxDefaultSize on the index file and updated readme as well * Add changelog to unreleased section * Add __shouldNotWarnDeprecated36pxSize prop to supress console warning from child component * Update tools panel storybook and docs to use __next40pxDefaultSize for UnitControl * Updated the unit test to minimise the file changes * Revert changes on mobile FontSizePicker --------- Co-authored-by: hbhalodia Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/CHANGELOG.md | 1 + .../src/border-control/border-control/component.tsx | 2 ++ .../components/src/box-control/all-input-control.tsx | 1 + .../src/box-control/axial-input-controls.tsx | 1 + .../components/src/box-control/input-controls.tsx | 1 + packages/components/src/font-size-picker/index.tsx | 1 + .../src/tools-panel/stories/index.story.tsx | 12 ++++++++++++ .../components/src/tools-panel/tools-panel/README.md | 2 ++ .../src/tools-panel/tools-panel/component.tsx | 2 ++ packages/components/src/unit-control/README.md | 6 +++--- packages/components/src/unit-control/index.tsx | 11 ++++++++++- .../src/unit-control/stories/index.story.tsx | 1 + packages/components/src/unit-control/test/index.tsx | 6 +++++- packages/components/src/unit-control/types.ts | 7 +++++++ 14 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ce151116df5bf..7865993d4e995 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,7 @@ - `BoxControl`: Passive deprecate `onMouseOver`/`onMouseOut`. Pass to the `inputProps` prop instead ([#67332](https://github.com/WordPress/gutenberg/pull/67332)). - `BoxControl`: Deprecate 36px default size ([#66704](https://github.com/WordPress/gutenberg/pull/66704)). +- `UnitControl`: Deprecate 36px default size ([#66791](https://github.com/WordPress/gutenberg/pull/66791)). ### Enhancements diff --git a/packages/components/src/border-control/border-control/component.tsx b/packages/components/src/border-control/border-control/component.tsx index 2ba338c2bb30c..f71599b274778 100644 --- a/packages/components/src/border-control/border-control/component.tsx +++ b/packages/components/src/border-control/border-control/component.tsx @@ -75,6 +75,8 @@ const UnconnectedBorderControl = ( /> = ( { isShownByDefault > setWidth( next ) } @@ -86,6 +87,7 @@ export const Default: StoryFn< typeof ToolsPanel > = ( { isShownByDefault > setHeight( next ) } @@ -98,6 +100,7 @@ export const Default: StoryFn< typeof ToolsPanel > = ( { isShownByDefault > setMinHeight( next ) } @@ -167,6 +170,7 @@ export const WithNonToolsPanelItems: StoryFn< typeof ToolsPanel > = ( { isShownByDefault > setWidth( next ) } @@ -179,6 +183,7 @@ export const WithNonToolsPanelItems: StoryFn< typeof ToolsPanel > = ( { isShownByDefault > setHeight( next ) } @@ -237,6 +242,7 @@ export const WithOptionalItemsPlusIcon: StoryFn< typeof ToolsPanel > = ( { } > setMinWidth( next ) } @@ -249,6 +255,7 @@ export const WithOptionalItemsPlusIcon: StoryFn< typeof ToolsPanel > = ( { isShownByDefault={ false } > setWidth( next ) } @@ -261,6 +268,7 @@ export const WithOptionalItemsPlusIcon: StoryFn< typeof ToolsPanel > = ( { isShownByDefault={ false } > setHeight( next ) } @@ -341,6 +349,7 @@ export const WithSlotFillItems: StoryFn< typeof ToolsPanel > = ( { panelId={ panelId } > @@ -356,6 +365,7 @@ export const WithSlotFillItems: StoryFn< typeof ToolsPanel > = ( { panelId={ panelId } > @@ -441,6 +451,7 @@ export const WithConditionalDefaultControl: StoryFn< typeof ToolsPanel > = ( { isShownByDefault > @@ -541,6 +552,7 @@ export const WithConditionallyRenderedControl: StoryFn< isShownByDefault > diff --git a/packages/components/src/tools-panel/tools-panel/README.md b/packages/components/src/tools-panel/tools-panel/README.md index 1895f5ccc843e..b5e6860e2bd07 100644 --- a/packages/components/src/tools-panel/tools-panel/README.md +++ b/packages/components/src/tools-panel/tools-panel/README.md @@ -101,6 +101,7 @@ export function DimensionPanel() { isShownByDefault > setHeight() } * > * setWidth() } * > * { const [ value, setValue ] = useState( '10px' ); - return ; + return ; }; ``` @@ -128,7 +128,7 @@ const Example = () => { ]; return ( - + ); }; ``` @@ -143,7 +143,7 @@ For example, a `value` of `50%` will set the current unit to `%`. Example: ```jsx - + ``` - Required: No diff --git a/packages/components/src/unit-control/index.tsx b/packages/components/src/unit-control/index.tsx index 2dd08cc155225..9845c4eb04ef2 100644 --- a/packages/components/src/unit-control/index.tsx +++ b/packages/components/src/unit-control/index.tsx @@ -27,6 +27,7 @@ import { useControlledState } from '../utils/hooks'; import { escapeRegExp } from '../utils/strings'; import type { UnitControlProps, UnitControlOnChangeCallback } from './types'; import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props'; +import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size'; function UnforwardedUnitControl( unitControlProps: WordPressComponentProps< @@ -55,9 +56,17 @@ function UnforwardedUnitControl( units: unitsProp = CSS_UNITS, value: valueProp, onFocus: onFocusProp, + __shouldNotWarnDeprecated36pxSize, ...props } = useDeprecated36pxDefaultSizeProp( unitControlProps ); + maybeWarnDeprecated36pxSize( { + componentName: 'UnitControl', + __next40pxDefaultSize: props.__next40pxDefaultSize, + size, + __shouldNotWarnDeprecated36pxSize, + } ); + if ( 'unit' in unitControlProps ) { deprecated( 'UnitControl unit prop', { since: '5.6', @@ -246,7 +255,7 @@ function UnforwardedUnitControl( * const Example = () => { * const [ value, setValue ] = useState( '10px' ); * - * return ; + * return ; * }; * ``` */ diff --git a/packages/components/src/unit-control/stories/index.story.tsx b/packages/components/src/unit-control/stories/index.story.tsx index de8f476e26e5c..87628c4644116 100644 --- a/packages/components/src/unit-control/stories/index.story.tsx +++ b/packages/components/src/unit-control/stories/index.story.tsx @@ -59,6 +59,7 @@ export const Default: StoryFn< typeof UnitControl > = DefaultTemplate.bind( ); Default.args = { label: 'Label', + __next40pxDefaultSize: true, }; /** diff --git a/packages/components/src/unit-control/test/index.tsx b/packages/components/src/unit-control/test/index.tsx index d91498d46478b..ad98d57cae640 100644 --- a/packages/components/src/unit-control/test/index.tsx +++ b/packages/components/src/unit-control/test/index.tsx @@ -12,9 +12,13 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import UnitControl from '..'; +import _UnitControl from '..'; import { CSS_UNITS, parseQuantityAndUnitFromRawValue } from '../utils'; +const UnitControl = ( props: React.ComponentProps< typeof _UnitControl > ) => ( + <_UnitControl __next40pxDefaultSize { ...props } /> +); + const getInput = ( { isInputTypeText = false, }: { diff --git a/packages/components/src/unit-control/types.ts b/packages/components/src/unit-control/types.ts index 9164502668a2b..891945b422862 100644 --- a/packages/components/src/unit-control/types.ts +++ b/packages/components/src/unit-control/types.ts @@ -107,4 +107,11 @@ export type UnitControlProps = Pick< InputControlProps, 'size' > & * Callback when either the quantity or the unit inputs gains focus. */ onFocus?: FocusEventHandler< HTMLInputElement | HTMLSelectElement >; + /** + * Do not throw a warning for the deprecated 36px default size. + * For internal components of other components that already throw the warning. + * + * @ignore + */ + __shouldNotWarnDeprecated36pxSize?: boolean; }; From 11262acadc190ca5715424728cafb1939d17483e Mon Sep 17 00:00:00 2001 From: Raj Patel <71687258+imrraaj@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:27:08 +0530 Subject: [PATCH 071/444] Pullquote block having design issue when text-decoration is choosen strikethrough (#66707) Co-authored-by: imrraaj Co-authored-by: NidhiDhandhukiya74 Co-authored-by: jorgefilipecosta --- packages/block-library/src/pullquote/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/pullquote/style.scss b/packages/block-library/src/pullquote/style.scss index b9e28b7cdcfaa..ff5fe6068dfac 100644 --- a/packages/block-library/src/pullquote/style.scss +++ b/packages/block-library/src/pullquote/style.scss @@ -72,4 +72,5 @@ .wp-block-pullquote cite { color: inherit; + display: block; } From 9ffbd9c215b2b43e527355d83b8d3f313b68582f Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:19:13 +0000 Subject: [PATCH 072/444] Block editor: try direct drag (outside text editable) (#67305) Co-authored-by: ellatrix Co-authored-by: youknowriad Co-authored-by: jasmussen Co-authored-by: draganescu --- .../components/block-draggable/content.scss | 16 ++- .../src/components/block-list/block.js | 1 + .../src/components/block-list/content.scss | 6 + .../block-list/use-block-props/index.js | 7 + .../use-firefox-draggable-compatibility.js | 25 ++++ .../use-selected-block-event-handlers.js | 120 ++++++++++++++++-- .../src/components/iframe/content.scss | 4 + .../src/components/rich-text/index.js | 5 + .../components/use-block-drop-zone/index.js | 19 ++- .../writing-flow/use-drag-selection.js | 11 ++ packages/components/CHANGELOG.md | 4 + .../components/src/resizable-box/index.tsx | 10 ++ .../components/src/resizable-box/style.scss | 8 ++ test/e2e/specs/editor/blocks/spacer.spec.js | 4 +- .../block-bindings/custom-sources.spec.js | 10 +- .../editor/various/draggable-blocks.spec.js | 100 ++++++++++----- 16 files changed, 300 insertions(+), 50 deletions(-) create mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-firefox-draggable-compatibility.js diff --git a/packages/block-editor/src/components/block-draggable/content.scss b/packages/block-editor/src/components/block-draggable/content.scss index 102230168e213..25a0f5c256595 100644 --- a/packages/block-editor/src/components/block-draggable/content.scss +++ b/packages/block-editor/src/components/block-draggable/content.scss @@ -1,13 +1,12 @@ // This creates a "slot" where the block you're dragging appeared. // We use !important as one of the rules are meant to be overridden. .block-editor-block-list__layout .is-dragging { - background-color: currentColor !important; - opacity: 0.05 !important; + opacity: 0.1 !important; border-radius: $radius-small !important; - // Disabling pointer events during the drag event is necessary, - // lest the block might affect your drag operation. - pointer-events: none !important; + iframe { + pointer-events: none; + } // Hide the multi selection indicator when dragging. &::selection { @@ -18,3 +17,10 @@ content: none !important; } } + +// Images are draggable by default, so disable drag for them if not explicitly +// set. This is done so that the block can capture the drag event instead. +.wp-block img:not([draggable]), +.wp-block svg:not([draggable]) { + pointer-events: none; +} diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 6d4655189d972..0e3a5be5150de 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -797,6 +797,7 @@ function BlockListBlockProvider( props ) { mayDisplayParentControls, originalBlockClientId, themeSupportsLayout, + canMove, }; // Here we separate between the props passed to BlockListBlock and any other diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index 3d3b8517ca09c..cd517fced833e 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -427,3 +427,9 @@ _::-webkit-full-page-media, _:future, :root [data-has-multi-selection="true"] .b // Additional -1px is required to avoid sub pixel rounding errors allowing background to show. margin: 0 calc(-1 * var(--wp--style--root--padding-right) - 1px) 0 calc(-1 * var(--wp--style--root--padding-left) - 1px) !important; } + +// This only works in Firefox, Chrome and Safari don't accept a custom cursor +// during drag. +.is-dragging { + cursor: grabbing; +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 25b9a21f0d286..4696149dc3875 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -30,6 +30,7 @@ import { useIntersectionObserver } from './use-intersection-observer'; import { useScrollIntoView } from './use-scroll-into-view'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; import { canBindBlock } from '../../../hooks/use-bindings-attributes'; +import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility'; /** * This hook is used to lightly mark an element as a block element. The element @@ -100,11 +101,15 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isTemporarilyEditingAsBlocks, defaultClassName, isSectionBlock, + canMove, } = useContext( PrivateBlockContext ); + const canDrag = canMove && ! hasChildSelected; + // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; + const ffDragRef = useFirefoxDraggableCompatibility(); const mergedRefs = useMergeRefs( [ props.ref, useFocusFirstElement( { clientId, initialPosition } ), @@ -120,6 +125,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isEnabled: isSectionBlock, } ), useScrollIntoView( { isSelected } ), + canDrag ? ffDragRef : undefined, ] ); const blockEditContext = useBlockEditContext(); @@ -152,6 +158,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { return { tabIndex: blockEditingMode === 'disabled' ? -1 : 0, + draggable: canDrag ? true : undefined, ...wrapperProps, ...props, ref: mergedRefs, diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-firefox-draggable-compatibility.js b/packages/block-editor/src/components/block-list/use-block-props/use-firefox-draggable-compatibility.js new file mode 100644 index 0000000000000..5fa07fb9be604 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-firefox-draggable-compatibility.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; + +/** + * In Firefox, the `draggable` and `contenteditable` attributes don't play well + * together. When `contenteditable` is within a `draggable` element, selection + * doesn't get set in the right place. The only solution is to temporarily + * remove the `draggable` attribute clicking inside `contenteditable` elements. + * + * @return {Function} Cleanup function. + */ +export function useFirefoxDraggableCompatibility() { + return useRefEffect( ( node ) => { + function onDown( event ) { + node.draggable = ! event.target.isContentEditable; + } + const { ownerDocument } = node; + ownerDocument.addEventListener( 'pointerdown', onDown ); + return () => { + ownerDocument.removeEventListener( 'pointerdown', onDown ); + }; + }, [] ); +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js b/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js index 68f8a671adbe9..0a13ce6700b8e 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js @@ -5,12 +5,15 @@ import { isTextField } from '@wordpress/dom'; import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; +import { createRoot } from '@wordpress/element'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; +import BlockDraggableChip from '../../../components/block-draggable/draggable-chip'; /** * Adds block behaviour: @@ -21,12 +24,16 @@ import { unlock } from '../../../lock-unlock'; * @param {string} clientId Block client ID. */ export function useEventHandlers( { clientId, isSelected } ) { - const { getBlockRootClientId, getBlockIndex, isZoomOut } = unlock( - useSelect( blockEditorStore ) - ); - const { insertAfterBlock, removeBlock, resetZoomLevel } = unlock( - useDispatch( blockEditorStore ) - ); + const { getBlockType } = useSelect( blocksStore ); + const { getBlockRootClientId, isZoomOut, hasMultiSelection, getBlockName } = + unlock( useSelect( blockEditorStore ) ); + const { + insertAfterBlock, + removeBlock, + resetZoomLevel, + startDraggingBlocks, + stopDraggingBlocks, + } = unlock( useDispatch( blockEditorStore ) ); return useRefEffect( ( node ) => { @@ -76,7 +83,102 @@ export function useEventHandlers( { clientId, isSelected } ) { * @param {DragEvent} event Drag event. */ function onDragStart( event ) { - event.preventDefault(); + if ( + node !== event.target || + node.isContentEditable || + node.ownerDocument.activeElement !== node || + hasMultiSelection() + ) { + event.preventDefault(); + return; + } + const data = JSON.stringify( { + type: 'block', + srcClientIds: [ clientId ], + srcRootClientId: getBlockRootClientId( clientId ), + } ); + event.dataTransfer.effectAllowed = 'move'; // remove "+" cursor + event.dataTransfer.clearData(); + event.dataTransfer.setData( 'wp-blocks', data ); + const { ownerDocument } = node; + const { defaultView } = ownerDocument; + const selection = defaultView.getSelection(); + selection.removeAllRanges(); + + const domNode = document.createElement( 'div' ); + const root = createRoot( domNode ); + root.render( + + ); + document.body.appendChild( domNode ); + domNode.style.position = 'absolute'; + domNode.style.top = '0'; + domNode.style.left = '0'; + domNode.style.zIndex = '1000'; + domNode.style.pointerEvents = 'none'; + + // Setting the drag chip as the drag image actually works, but + // the behaviour is slightly different in every browser. In + // Safari, it animates, in Firefox it's slightly transparent... + // So we set a fake drag image and have to reposition it + // ourselves. + const dragElement = ownerDocument.createElement( 'div' ); + // Chrome will show a globe icon if the drag element does not + // have dimensions. + dragElement.style.width = '1px'; + dragElement.style.height = '1px'; + dragElement.style.position = 'fixed'; + dragElement.style.visibility = 'hidden'; + ownerDocument.body.appendChild( dragElement ); + event.dataTransfer.setDragImage( dragElement, 0, 0 ); + + let offset = { x: 0, y: 0 }; + + if ( document !== ownerDocument ) { + const frame = defaultView.frameElement; + if ( frame ) { + const rect = frame.getBoundingClientRect(); + offset = { x: rect.left, y: rect.top }; + } + } + + // chip handle offset + offset.x -= 58; + + function over( e ) { + domNode.style.transform = `translate( ${ + e.clientX + offset.x + }px, ${ e.clientY + offset.y }px )`; + } + + over( event ); + + function end() { + ownerDocument.removeEventListener( 'dragover', over ); + ownerDocument.removeEventListener( 'dragend', end ); + domNode.remove(); + dragElement.remove(); + stopDraggingBlocks(); + document.body.classList.remove( + 'is-dragging-components-draggable' + ); + ownerDocument.documentElement.classList.remove( + 'is-dragging' + ); + } + + ownerDocument.addEventListener( 'dragover', over ); + ownerDocument.addEventListener( 'dragend', end ); + ownerDocument.addEventListener( 'drop', end ); + + startDraggingBlocks( [ clientId ] ); + // Important because it hides the block toolbar. + document.body.classList.add( + 'is-dragging-components-draggable' + ); + ownerDocument.documentElement.classList.add( 'is-dragging' ); } node.addEventListener( 'keydown', onKeyDown ); @@ -91,11 +193,13 @@ export function useEventHandlers( { clientId, isSelected } ) { clientId, isSelected, getBlockRootClientId, - getBlockIndex, insertAfterBlock, removeBlock, isZoomOut, resetZoomLevel, + hasMultiSelection, + startDraggingBlocks, + stopDraggingBlocks, ] ); } diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index 9b02716671de7..74efb63c0e077 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -60,5 +60,9 @@ } } } + + .wp-block[draggable] { + cursor: grab; + } } } diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 8f179d08570ad..bc8eca6ea94d0 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -431,6 +431,11 @@ export function RichTextWrapper( aria-multiline={ ! disableLineBreaks } aria-readonly={ shouldDisableEditing } { ...props } + // Unset draggable (coming from block props) for contentEditable + // elements because it will interfere with multi block selection + // when the contentEditable and draggable elements are the same + // element. + draggable={ undefined } aria-label={ bindingsLabel || props[ 'aria-label' ] || placeholder } diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index 2a3e4948d40b3..221e5ab74ebb2 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -332,6 +332,7 @@ export default function useBlockDropZone( { isGroupable, isZoomOut, getSectionRootClientId, + getBlockParents, } = unlock( useSelect( blockEditorStore ) ); const { showInsertionPoint, @@ -358,13 +359,29 @@ export default function useBlockDropZone( { // So, ensure that the drag state is set when the user drags over a drop zone. startDragging(); } + + const draggedBlockClientIds = getDraggedBlockClientIds(); + const targetParents = [ + targetRootClientId, + ...getBlockParents( targetRootClientId, true ), + ]; + + // Check if the target is within any of the dragged blocks. + const isTargetWithinDraggedBlocks = draggedBlockClientIds.some( + ( clientId ) => targetParents.includes( clientId ) + ); + + if ( isTargetWithinDraggedBlocks ) { + return; + } + const allowedBlocks = getAllowedBlocks( targetRootClientId ); const targetBlockName = getBlockNamesByClientId( [ targetRootClientId, ] )[ 0 ]; const draggedBlockNames = getBlockNamesByClientId( - getDraggedBlockClientIds() + draggedBlockClientIds ); const isBlockDroppingAllowed = isDropTargetValid( getBlockType, diff --git a/packages/block-editor/src/components/writing-flow/use-drag-selection.js b/packages/block-editor/src/components/writing-flow/use-drag-selection.js index 1569c45a7c676..ea4c09b3dc957 100644 --- a/packages/block-editor/src/components/writing-flow/use-drag-selection.js +++ b/packages/block-editor/src/components/writing-flow/use-drag-selection.js @@ -80,7 +80,17 @@ export default function useDragSelection() { } ); } + let lastMouseDownTarget; + + function onMouseDown( { target } ) { + lastMouseDownTarget = target; + } + function onMouseLeave( { buttons, target, relatedTarget } ) { + if ( ! target.contains( lastMouseDownTarget ) ) { + return; + } + // If we're moving into a child element, ignore. We're tracking // the mouse leaving the element to a parent, no a child. if ( target.contains( relatedTarget ) ) { @@ -141,6 +151,7 @@ export default function useDragSelection() { } node.addEventListener( 'mouseout', onMouseLeave ); + node.addEventListener( 'mousedown', onMouseDown ); return () => { node.removeEventListener( 'mouseout', onMouseLeave ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 7865993d4e995..2785447227416 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -21,6 +21,10 @@ - Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). - Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)). +### Bug Fixes + +- `ResizableBox`: Make drag handles focusable ([#67305](https://github.com/WordPress/gutenberg/pull/67305)). + ## 28.13.0 (2024-11-27) ### Deprecations diff --git a/packages/components/src/resizable-box/index.tsx b/packages/components/src/resizable-box/index.tsx index 1b05270ea0bf2..3bf3d36aa0d5c 100644 --- a/packages/components/src/resizable-box/index.tsx +++ b/packages/components/src/resizable-box/index.tsx @@ -112,6 +112,16 @@ function UnforwardedResizableBox( showHandle && 'has-show-handle', className ) } + // Add a focusable element within the drag handle. Unfortunately, + // `re-resizable` does not make them properly focusable by default, + // causing focus to move the the block wrapper which triggers block + // drag. + handleComponent={ Object.fromEntries( + Object.keys( HANDLE_CLASSES ).map( ( key ) => [ + key, +
, + ] ) + ) } handleClasses={ HANDLE_CLASSES } handleStyles={ HANDLE_STYLES } ref={ ref } diff --git a/packages/components/src/resizable-box/style.scss b/packages/components/src/resizable-box/style.scss index 3c9efd2713646..4db3d27b5fab6 100644 --- a/packages/components/src/resizable-box/style.scss +++ b/packages/components/src/resizable-box/style.scss @@ -15,6 +15,14 @@ $resize-handler-container-size: $resize-handler-size + ($grid-unit-05 * 2); // M .components-resizable-box__container.has-show-handle & { display: block; } + + > div { + position: relative; + width: 100%; + height: 100%; + z-index: z-index(".components-resizable-box__handle"); + outline: none; + } } // Make the image inside the resize to get the full width diff --git a/test/e2e/specs/editor/blocks/spacer.spec.js b/test/e2e/specs/editor/blocks/spacer.spec.js index f089402514623..da262c9b4e26d 100644 --- a/test/e2e/specs/editor/blocks/spacer.spec.js +++ b/test/e2e/specs/editor/blocks/spacer.spec.js @@ -43,7 +43,9 @@ test.describe( 'Spacer', () => { expect( await editor.getEditedPostContent() ).toMatchSnapshot(); await expect( - editor.canvas.locator( 'role=document[name="Block: Spacer"i]' ) + editor.canvas.locator( + 'role=document[name="Block: Spacer"i] >> css=.components-resizable-box__handle >> [tabindex]' + ) ).toBeFocused(); } ); } ); diff --git a/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js index d6563ce9cb5f5..033a69e2d6170 100644 --- a/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js +++ b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js @@ -168,7 +168,10 @@ test.describe( 'Registered sources', () => { name: 'Block: Image', } ) .locator( 'img' ); - await imageBlockImg.click(); + // Playwright will complain that the pointer events are captured by + // the parent, but that's fine. + // eslint-disable-next-line playwright/no-force-option + await imageBlockImg.click( { force: true } ); // Image src is the custom field value. await expect( imageBlockImg ).toHaveAttribute( @@ -735,7 +738,10 @@ test.describe( 'Registered sources', () => { name: 'Block: Image', } ) .locator( 'img' ); - await imageBlockImg.click(); + // Playwright will complain that the pointer events are captured by + // the parent, but that's fine. + // eslint-disable-next-line playwright/no-force-option + await imageBlockImg.click( { force: true } ); // Edit the custom field value in the alt textarea. const altInputArea = page diff --git a/test/e2e/specs/editor/various/draggable-blocks.spec.js b/test/e2e/specs/editor/various/draggable-blocks.spec.js index e08030191dd60..704817f4a2c38 100644 --- a/test/e2e/specs/editor/various/draggable-blocks.spec.js +++ b/test/e2e/specs/editor/various/draggable-blocks.spec.js @@ -18,6 +18,14 @@ test.use( { }, } ); +async function dragTo( page, x, y ) { + // Call the move function twice to make sure the `dragOver` event is sent. + // @see https://github.com/microsoft/playwright/issues/17153 + for ( let i = 0; i < 2; i += 1 ) { + await page.mouse.move( x, y ); + } +} + test.describe( 'Draggable block', () => { test.beforeEach( async ( { admin } ) => { await admin.createNewPost(); @@ -60,14 +68,7 @@ test.describe( 'Draggable block', () => { 'role=document[name="Block: Paragraph"i] >> text=1' ); const firstParagraphBound = await firstParagraph.boundingBox(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - firstParagraphBound.x, - firstParagraphBound.y - ); - } + await dragTo( page, firstParagraphBound.x, firstParagraphBound.y ); await expect( page.locator( 'data-testid=block-draggable-chip >> visible=true' ) @@ -132,15 +133,11 @@ test.describe( 'Draggable block', () => { 'role=document[name="Block: Paragraph"i] >> text=2' ); const secondParagraphBound = await secondParagraph.boundingBox(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - // Make sure mouse is > 30px within the block for bottom drop indicator to appear. - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - secondParagraphBound.x + 32, - secondParagraphBound.y + secondParagraphBound.height * 0.75 - ); - } + await dragTo( + page, + secondParagraphBound.x + 32, + secondParagraphBound.y + secondParagraphBound.height * 0.75 + ); await expect( page.locator( 'data-testid=block-draggable-chip >> visible=true' ) @@ -216,14 +213,11 @@ test.describe( 'Draggable block', () => { 'role=document[name="Block: Paragraph"i] >> text=1' ); const firstParagraphBound = await firstParagraph.boundingBox(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - firstParagraphBound.x + firstParagraphBound.width * 0.25, - firstParagraphBound.y - ); - } + await dragTo( + page, + firstParagraphBound.x + firstParagraphBound.width * 0.25, + firstParagraphBound.y + ); await expect( page.locator( 'data-testid=block-draggable-chip >> visible=true' ) @@ -297,14 +291,11 @@ test.describe( 'Draggable block', () => { 'role=document[name="Block: Paragraph"i] >> text=2' ); const secondParagraphBound = await secondParagraph.boundingBox(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - secondParagraphBound.x + secondParagraphBound.width * 0.75, - secondParagraphBound.y - ); - } + await dragTo( + page, + secondParagraphBound.x + secondParagraphBound.width * 0.75, + secondParagraphBound.y + ); await expect( page.locator( 'data-testid=block-draggable-chip >> visible=true' ) @@ -465,4 +456,47 @@ test.describe( 'Draggable block', () => { ] ); } } ); + + test( 'can directly drag an image', async ( { page, editor } ) => { + await editor.insertBlock( { name: 'core/image' } ); + await editor.insertBlock( { + name: 'core/group', + attributes: { layout: { type: 'constrained' } }, + innerBlocks: [ { name: 'core/paragraph' } ], + } ); + + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + + const groupBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Group', + } ); + + await imageBlock.hover(); + await page.mouse.down(); + const groupBlockBox = await groupBlock.boundingBox(); + await dragTo( + page, + groupBlockBox.x + groupBlockBox.width * 0.5, + groupBlockBox.y + groupBlockBox.height * 0.5 + ); + await page.mouse.up(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + attributes: { + tagName: 'div', + layout: { type: 'constrained' }, + }, + innerBlocks: [ + { + name: 'core/image', + attributes: { alt: '', caption: '' }, + }, + ], + }, + ] ); + } ); } ); From 9defe9f7b6f7147756d1dba26dc0a8f8c07052d0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sat, 30 Nov 2024 08:24:55 +1100 Subject: [PATCH 073/444] Fix misc type compilation errors in editor and block editor packages (#67410) Co-authored-by: ramonjd Co-authored-by: Mamaduka --- packages/components/CHANGELOG.md | 1 + packages/components/src/index.ts | 1 + packages/editor/README.md | 7 ++++--- packages/editor/src/components/post-trash/check.js | 6 +++--- packages/editor/src/components/post-url/index.js | 3 ++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2785447227416..c9295e7ef861e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -20,6 +20,7 @@ - Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). - Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)). +- Exported the `WPCompleter` type as it was being used in block-editor/autocompleters ([#67410](https://github.com/WordPress/gutenberg/pull/67410)). ### Bug Fixes diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index e82d6da70279e..0558584fe5418 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -108,6 +108,7 @@ export { Heading as __experimentalHeading } from './heading'; export { HStack as __experimentalHStack } from './h-stack'; export { default as Icon } from './icon'; export type { IconType } from './icon'; +export type { WPCompleter } from './autocomplete/types.ts'; export { default as IconButton } from './button/deprecated'; export { ItemGroup as __experimentalItemGroup, diff --git a/packages/editor/README.md b/packages/editor/README.md index 36126cb8eaee3..8b48d773acb26 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -1462,11 +1462,11 @@ Wrapper component that renders its children only if the post can trashed. _Parameters_ - _props_ `Object`: - The component props. -- _props.children_ `React.ReactEl`: - The child components to render. +- _props.children_ `React.ReactNode`: - The child components to render. _Returns_ -- `React.ReactElement`: The rendered child components or null if the post can not trashed. +- `React.ReactNode`: The rendered child components or null if the post can not trashed. ### PostTypeSupportCheck @@ -1494,7 +1494,8 @@ _Usage_ _Parameters_ -- _onClose_ `Function`: Callback function to be executed when the popover is closed. +- _props_ `{ onClose: () => void }`: The props for the component. +- _props.onClose_ `() => void`: Callback function to be executed when the popover is closed. _Returns_ diff --git a/packages/editor/src/components/post-trash/check.js b/packages/editor/src/components/post-trash/check.js index 893d46ef9a086..d4a9139bfee05 100644 --- a/packages/editor/src/components/post-trash/check.js +++ b/packages/editor/src/components/post-trash/check.js @@ -13,10 +13,10 @@ import { GLOBAL_POST_TYPES } from '../../store/constants'; /** * Wrapper component that renders its children only if the post can trashed. * - * @param {Object} props - The component props. - * @param {React.ReactEl} props.children - The child components to render. + * @param {Object} props - The component props. + * @param {React.ReactNode} props.children - The child components to render. * - * @return {React.ReactElement} The rendered child components or null if the post can not trashed. + * @return {React.ReactNode} The rendered child components or null if the post can not trashed. */ export default function PostTrashCheck( { children } ) { const { canTrashPost } = useSelect( ( select ) => { diff --git a/packages/editor/src/components/post-url/index.js b/packages/editor/src/components/post-url/index.js index c72ca5825f6fe..f55ac973be50e 100644 --- a/packages/editor/src/components/post-url/index.js +++ b/packages/editor/src/components/post-url/index.js @@ -32,7 +32,8 @@ import { store as editorStore } from '../../store'; * * ``` * - * @param {Function} onClose Callback function to be executed when the popover is closed. + * @param {{ onClose: () => void }} props The props for the component. + * @param {() => void} props.onClose Callback function to be executed when the popover is closed. * * @return {React.ReactNode} The rendered PostURL component. */ From 961000076e74a6eea419d79ff8e16ae54a73463b Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Fri, 29 Nov 2024 23:55:22 -0800 Subject: [PATCH 074/444] Add types for shortcode package (#67416) * Add types for shortcode package * Remove WP Prefix * Update CHANGELOG.md Co-authored-by: manzoorwanijk Co-authored-by: gziolo --- packages/shortcode/CHANGELOG.md | 4 + packages/shortcode/README.md | 18 ++- packages/shortcode/package.json | 1 + packages/shortcode/src/index.js | 51 ++------ packages/shortcode/src/types.ts | 210 +++++++++++++++++++++++++++++++ packages/shortcode/tsconfig.json | 11 ++ tsconfig.json | 1 + 7 files changed, 245 insertions(+), 51 deletions(-) create mode 100644 packages/shortcode/src/types.ts create mode 100644 packages/shortcode/tsconfig.json diff --git a/packages/shortcode/CHANGELOG.md b/packages/shortcode/CHANGELOG.md index 8ee4e9aa3168d..2e461f1bc85c1 100644 --- a/packages/shortcode/CHANGELOG.md +++ b/packages/shortcode/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Enhancements + +- The package now has built-in TypeScript definitions 🎉 ([#67416](https://github.com/WordPress/gutenberg/pull/67416)) + ## 4.13.0 (2024-11-27) ## 4.12.0 (2024-11-16) diff --git a/packages/shortcode/README.md b/packages/shortcode/README.md index a1a016e75e755..b6042ab284fe4 100644 --- a/packages/shortcode/README.md +++ b/packages/shortcode/README.md @@ -32,7 +32,7 @@ _Parameters_ _Returns_ -- `WPShortcodeAttrs`: Parsed shortcode attributes. +- `import('./types').ShortcodeAttrs`: Parsed shortcode attributes. ### default @@ -40,13 +40,9 @@ Creates a shortcode instance. To access a raw representation of a shortcode, pass an `options` object, containing a `tag` string, a string or object of `attrs`, a string indicating the `type` of the shortcode ('single', 'self-closing', or 'closed'), and a `content` string. -_Parameters_ - -- _options_ `Object`: Options as described. - -_Returns_ +_Type_ -- `WPShortcode`: Shortcode instance. +- `import('./types').shortcode`Shortcode instance. ### fromMatch @@ -56,11 +52,11 @@ Accepts a `match` object from calling `regexp.exec()` on a `RegExp` generated by _Parameters_ -- _match_ `Array`: Match array. +- _match_ `import('./types').Match`: Match array. _Returns_ -- `WPShortcode`: Shortcode instance. +- `InstanceType`: Shortcode instance. ### next @@ -74,7 +70,7 @@ _Parameters_ _Returns_ -- `WPShortcodeMatch | undefined`: Matched information. +- `import('./types').ShortcodeMatch | undefined`: Matched information. ### regexp @@ -108,7 +104,7 @@ _Parameters_ - _tag_ `string`: Shortcode tag. - _text_ `string`: Text to search. -- _callback_ `Function`: Function to process the match and return replacement string. +- _callback_ `import('./types').ReplaceCallback`: Function to process the match and return replacement string. _Returns_ diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index f87fe06d9bb42..c5495364a03c4 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -26,6 +26,7 @@ "module": "build-module/index.js", "react-native": "src/index", "wpScript": true, + "types": "build-types", "dependencies": { "@babel/runtime": "7.25.7", "memize": "^2.0.1" diff --git a/packages/shortcode/src/index.js b/packages/shortcode/src/index.js index 04e69c272378b..4d99086033e95 100644 --- a/packages/shortcode/src/index.js +++ b/packages/shortcode/src/index.js @@ -3,34 +3,7 @@ */ import memize from 'memize'; -/** - * Shortcode attributes object. - * - * @typedef {Object} WPShortcodeAttrs - * - * @property {Object} named Object with named attributes. - * @property {Array} numeric Array with numeric attributes. - */ - -/** - * Shortcode object. - * - * @typedef {Object} WPShortcode - * - * @property {string} tag Shortcode tag. - * @property {WPShortcodeAttrs} attrs Shortcode attributes. - * @property {string} content Shortcode content. - * @property {string} type Shortcode type: `self-closing`, - * `closed`, or `single`. - */ - -/** - * @typedef {Object} WPShortcodeMatch - * - * @property {number} index Index the shortcode is found at. - * @property {string} content Matched content. - * @property {WPShortcode} shortcode Shortcode instance of the match. - */ +export * from './types'; /** * Find the next matching shortcode. @@ -39,7 +12,7 @@ import memize from 'memize'; * @param {string} text Text to search. * @param {number} index Index to start search from. * - * @return {WPShortcodeMatch | undefined} Matched information. + * @return {import('./types').ShortcodeMatch | undefined} Matched information. */ export function next( tag, text, index = 0 ) { const re = regexp( tag ); @@ -81,10 +54,10 @@ export function next( tag, text, index = 0 ) { /** * Replace matching shortcodes in a block of text. * - * @param {string} tag Shortcode tag. - * @param {string} text Text to search. - * @param {Function} callback Function to process the match and return - * replacement string. + * @param {string} tag Shortcode tag. + * @param {string} text Text to search. + * @param {import('./types').ReplaceCallback} callback Function to process the match and return + * replacement string. * * @return {string} Text with shortcodes replaced. */ @@ -169,7 +142,7 @@ export function regexp( tag ) { * * @param {string} text Serialised shortcode attributes. * - * @return {WPShortcodeAttrs} Parsed shortcode attributes. + * @return {import('./types').ShortcodeAttrs} Parsed shortcode attributes. */ export const attrs = memize( ( text ) => { const named = {}; @@ -224,9 +197,9 @@ export const attrs = memize( ( text ) => { * by `regexp()`. `match` can also be set to the `arguments` from a callback * passed to `regexp.replace()`. * - * @param {Array} match Match array. + * @param {import('./types').Match} match Match array. * - * @return {WPShortcode} Shortcode instance. + * @return {InstanceType} Shortcode instance. */ export function fromMatch( match ) { let type; @@ -255,9 +228,7 @@ export function fromMatch( match ) { * the `type` of the shortcode ('single', 'self-closing', or 'closed'), and a * `content` string. * - * @param {Object} options Options as described. - * - * @return {WPShortcode} Shortcode instance. + * @type {import('./types').shortcode} Shortcode instance. */ const shortcode = Object.assign( function ( options ) { @@ -328,7 +299,7 @@ Object.assign( shortcode.prototype, { * @param {(number|string)} attr Attribute key. * @param {string} value Attribute value. * - * @return {WPShortcode} Shortcode instance. + * @return {InstanceType< import('./types').shortcode >} Shortcode instance. */ set( attr, value ) { this.attrs[ typeof attr === 'number' ? 'numeric' : 'named' ][ attr ] = diff --git a/packages/shortcode/src/types.ts b/packages/shortcode/src/types.ts new file mode 100644 index 0000000000000..2b9ae084cc31a --- /dev/null +++ b/packages/shortcode/src/types.ts @@ -0,0 +1,210 @@ +/** + * Shortcode attributes object. + */ +export type ShortcodeAttrs = { + /** + * Object with named attributes. + */ + named: Record< string, string | undefined >; + + /** + * Array with numeric attributes. + */ + numeric: string[]; +}; + +export type ShortcodeMatch = { + /** + * Index the shortcode is found at. + */ + index: number; + + /** + * Matched content. + */ + content: string; + + /** + * Shortcode instance of the match. + */ + shortcode: Shortcode; +}; + +/** + * Shortcode options. + */ +export interface ShortcodeOptions { + /** + * Shortcode tag. + */ + tag: string; + + /** + * Shortcode attributes. + */ + attrs?: Partial< ShortcodeAttrs > | string; + + /** + * Shortcode content. + */ + content?: string; + + /** + * Shortcode type: `self-closing`, `closed`, or `single`. + */ + type?: 'self-closing' | 'closed' | 'single'; +} + +/** + * Shortcode object. + */ +export interface Shortcode extends ShortcodeOptions { + /** + * Shortcode attributes. + */ + attrs: ShortcodeAttrs; +} + +export type Match = + | NonNullable< ReturnType< RegExp[ 'exec' ] > > + | Array< string >; + +export type ReplaceCallback = ( shortcode: Shortcode ) => string; + +/** + * WordPress Shortcode instance. + */ +export interface shortcode { + new ( options: Partial< ShortcodeOptions > ): Shortcode & { + /** + * Transform the shortcode into a string. + * + * @return {string} String representation of the shortcode. + */ + string: () => string; + + /** + * Get a shortcode attribute. + * + * Automatically detects whether `attr` is named or numeric and routes it + * accordingly. + * + * @param {(number|string)} attr Attribute key. + * + * @return {string} Attribute value. + */ + get: ( attr: string | number ) => string | undefined; + + /** + * Set a shortcode attribute. + * + * Automatically detects whether `attr` is named or numeric and routes it + * accordingly. + * + * @param {(number|string)} attr Attribute key. + * @param {string} value Attribute value. + * + * @return {InstanceType< shortcode >} Shortcode instance. + */ + set: ( + attr: string | number, + value: string + ) => InstanceType< shortcode >; + }; + + /** + * Parse shortcode attributes. + * + * Shortcodes accept many types of attributes. These can chiefly be divided into + * named and numeric attributes: + * + * Named attributes are assigned on a key/value basis, while numeric attributes + * are treated as an array. + * + * Named attributes can be formatted as either `name="value"`, `name='value'`, + * or `name=value`. Numeric attributes can be formatted as `"value"` or just + * `value`. + * + * @param text Serialised shortcode attributes. + * + * @return Parsed shortcode attributes. + */ + attrs: ( text: string ) => ShortcodeAttrs; + + /** + * Generate a Shortcode Object from a RegExp match. + * + * Accepts a `match` object from calling `regexp.exec()` on a `RegExp` generated + * by `regexp()`. `match` can also be set to the `arguments` from a callback + * passed to `regexp.replace()`. + * + * @param match Match array. + * + * @return Shortcode instance. + */ + fromMatch: ( match: Match ) => InstanceType< shortcode >; + + /** + * Find the next matching shortcode. + * + * @param tag Shortcode tag. + * @param text Text to search. + * @param index Index to start search from. + * + * @return Matched information. + */ + next: ( + tag: string, + text: string, + index?: number + ) => ShortcodeMatch | undefined; + + /** + * Generate a RegExp to identify a shortcode. + * + * The base regex is functionally equivalent to the one found in + * `get_shortcode_regex()` in `wp-includes/shortcodes.php`. + * + * Capture groups: + * + * 1. An extra `[` to allow for escaping shortcodes with double `[[]]` + * 2. The shortcode name + * 3. The shortcode argument list + * 4. The self closing `/` + * 5. The content of a shortcode when it wraps some content. + * 6. The closing tag. + * 7. An extra `]` to allow for escaping shortcodes with double `[[]]` + * + * @param tag Shortcode tag. + * + * @return Shortcode RegExp. + */ + regexp: ( tag: string ) => RegExp; + + /** + * Replace matching shortcodes in a block of text. + * + * @param tag Shortcode tag. + * @param text Text to search. + * @param callback Function to process the match and return + * replacement string. + * + * @return Text with shortcodes replaced. + */ + replace: ( tag: string, text: string, callback: ReplaceCallback ) => string; + + /** + * Generate a string from shortcode parameters. + * + * Creates a shortcode instance and returns a string. + * + * Accepts the same `options` as the `shortcode()` constructor, containing a + * `tag` string, a string or object of `attrs`, a boolean indicating whether to + * format the shortcode using a `single` tag, and a `content` string. + * + * @param options + * + * @return String representation of the shortcode. + */ + string: ( options: ShortcodeOptions ) => string; +} diff --git a/packages/shortcode/tsconfig.json b/packages/shortcode/tsconfig.json new file mode 100644 index 0000000000000..79aa09d0ad56e --- /dev/null +++ b/packages/shortcode/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "checkJs": false + }, + "references": [], + "include": [ "src" ] +} diff --git a/tsconfig.json b/tsconfig.json index 51bb7f2d68924..1010054ea512e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -50,6 +50,7 @@ { "path": "packages/report-flaky-tests" }, { "path": "packages/rich-text" }, { "path": "packages/router" }, + { "path": "packages/shortcode" }, { "path": "packages/style-engine" }, { "path": "packages/sync" }, { "path": "packages/token-list" }, From d0383fef7532779a27afbd226c51b535232201d4 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Sun, 1 Dec 2024 02:04:43 +0000 Subject: [PATCH 075/444] Fix: Styles section does not moves stylebook to typography. (#67423) Co-authored-by: jorgefilipecosta Co-authored-by: ramonjd --- packages/edit-site/src/components/style-book/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index de4c38bd40c05..6a044d8055300 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -89,6 +89,11 @@ const scrollToSection = ( anchorId, iframe ) => { */ const getStyleBookNavigationFromPath = ( path ) => { if ( path && typeof path === 'string' ) { + if ( path.startsWith( '/typography' ) ) { + return { + block: 'typography', + }; + } let block = path.includes( '/blocks/' ) ? decodeURIComponent( path.split( '/blocks/' )[ 1 ] ) : null; From 988f259c5c32d312bd09a25789c6b658069ccb90 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Sun, 1 Dec 2024 19:38:30 +0530 Subject: [PATCH 076/444] Updated old URL in Documentation (#67446) * Updated Old URL Co-authored-by: shail-mehta Co-authored-by: Mamaduka --- docs/reference-guides/block-api/block-transforms.md | 12 ++++++------ platform-docs/docs/create-block/transforms.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md index c2c5ed49d1b19..9055ed0a3b45b 100644 --- a/docs/reference-guides/block-api/block-transforms.md +++ b/docs/reference-guides/block-api/block-transforms.md @@ -44,7 +44,7 @@ A transformation of type `block` is an object that takes the following parameter - **transform** _(function)_: a callback that receives the attributes and inner blocks of the block being processed. It should return a block object or an array of block objects. - **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user. - **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If true, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. False by default. -- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: from Paragraph block to Heading block** @@ -97,7 +97,7 @@ A transformation of type `enter` is an object that takes the following parameter - **type** _(string)_: the value `enter`. - **regExp** _(RegExp)_: the Regular Expression to use as a matcher. If the value matches, the transformation will be applied. - **transform** _(function)_: a callback that receives an object with a `content` field containing the value that has been entered. It should return a block object or an array of block objects. -- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: from --- to Separator block** @@ -124,7 +124,7 @@ A transformation of type `files` is an object that takes the following parameter - **type** _(string)_: the value `files`. - **transform** _(function)_: a callback that receives the array of files being processed. It should return a block object or an array of block objects. - **isMatch** _(function, optional)_: a callback that receives the array of files being processed and should return a boolean. Returning `false` from this function will prevent the transform from being applied. -- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: from file to File block** @@ -164,7 +164,7 @@ A transformation of type `prefix` is an object that takes the following paramete - **type** _(string)_: the value `prefix`. - **prefix** _(string)_: the character or sequence of characters that match this transform. - **transform** _(function)_: a callback that receives the content introduced. It should return a block object or an array of block objects. -- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: from text to custom block** @@ -197,7 +197,7 @@ A transformation of type `raw` is an object that takes the following parameters: - **schema** _(object|function, optional)_: defines an [HTML content model](https://html.spec.whatwg.org/multipage/dom.html#content-models) used to detect and process pasted contents. See [below](#schemas-and-content-models). - **selector** _(string, optional)_: a CSS selector string to determine whether the element matches according to the [element.matches](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches) method. The transform won't be executed if the element doesn't match. This is a shorthand and alternative to using `isMatch`, which, if present, will take precedence. - **isMatch** _(function, optional)_: a callback that receives the node being processed and should return a boolean. Returning `false` from this function will prevent the transform from being applied. -- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: from URLs to Embed block** @@ -273,7 +273,7 @@ A transformation of type `shortcode` is an object that takes the following param - **transform** _(function, optional)_: a callback that receives the shortcode attributes as the first argument and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as the second. It should return a block object or an array of block objects. When this parameter is defined, it will take precedence over the `attributes` parameter. - **attributes** _(object, optional)_: object representing where the block attributes should be sourced from, according to the attributes shape defined by the [block configuration object](./block-registration.md). If a particular attribute contains a `shortcode` key, it should be a function that receives the shortcode attributes as the first arguments and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as second, and returns a value for the attribute that will be sourced in the block's comment. - **isMatch** _(function, optional)_: a callback that receives the shortcode attributes per the [Shortcode API](https://codex.wordpress.org/Shortcode_API) and should return a boolean. Returning `false` from this function will prevent the shortcode to be transformed into this block. -- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: from shortcode to block using `transform`** diff --git a/platform-docs/docs/create-block/transforms.md b/platform-docs/docs/create-block/transforms.md index fd235b669cd72..4a4118d6850b9 100644 --- a/platform-docs/docs/create-block/transforms.md +++ b/platform-docs/docs/create-block/transforms.md @@ -37,7 +37,7 @@ A transformation of type `block` is an object that takes the following parameter - **transform** _(function)_: a callback that receives the attributes and inner blocks of the block being processed. It should return a block object or an array of block objects. - **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user. - **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If `true`, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. Returns `false` by default. -- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. **Example: Let's declare a transform from our Gutenpride block to Heading block** From 4775e7052b9e2ed7df46429e6e738de3faf2fb18 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:00:10 +0000 Subject: [PATCH 077/444] Preload: fix settings fields order (#67450) Co-authored-by: ellatrix Co-authored-by: Mamaduka --- lib/compat/wordpress-6.8/preload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.8/preload.php b/lib/compat/wordpress-6.8/preload.php index ae6c738c6627c..6d92913b41411 100644 --- a/lib/compat/wordpress-6.8/preload.php +++ b/lib/compat/wordpress-6.8/preload.php @@ -31,9 +31,9 @@ function gutenberg_block_editor_preload_paths_6_8( $paths, $context ) { 'site_icon_url', 'site_logo', 'timezone_string', - 'url', 'default_template_part_areas', 'default_template_types', + 'url', ) ); $paths[] = '/wp/v2/templates/lookup?slug=front-page'; From c5b33b86f27dd6b6e3b4450b7c72cb949e4bd95b Mon Sep 17 00:00:00 2001 From: Sunil Prajapati <61308756+akasunil@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:59:13 +0530 Subject: [PATCH 078/444] Implement image size option for featured image in cover block (#67273) Co-authored-by: akasunil Co-authored-by: aaronrobertshaw Co-authored-by: carolinan --- packages/block-library/src/cover/edit/index.js | 5 ++++- .../src/cover/edit/inspector-controls.js | 13 ++++++++++--- packages/block-library/src/cover/index.php | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 3ad7039b55892..ced3097320329 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -120,7 +120,9 @@ function CoverEdit( { select( coreStore ).getMedia( featuredImage, { context: 'view' } ), [ featuredImage ] ); - const mediaUrl = media?.source_url; + const mediaUrl = + media?.media_details?.sizes?.[ sizeSlug ]?.source_url ?? + media?.source_url; // User can change the featured image outside of the block, but we still // need to update the block when that happens. This effect should only @@ -451,6 +453,7 @@ function CoverEdit( { toggleUseFeaturedImage={ toggleUseFeaturedImage } updateDimRatio={ onUpdateDimRatio } onClearMedia={ onClearMedia } + featuredImage={ media } /> ); diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index c0807869ee1a5..b0d4b435163b7 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -96,6 +96,7 @@ export default function CoverInspectorControls( { coverRef, currentSettings, updateDimRatio, + featuredImage, } ) { const { useFeaturedImage, @@ -132,8 +133,12 @@ export default function CoverInspectorControls( { [ id, isImageBackground ] ); + const currentBackgroundImage = useFeaturedImage ? featuredImage : image; + function updateImage( newSizeSlug ) { - const newUrl = image?.media_details?.sizes?.[ newSizeSlug ]?.source_url; + const newUrl = + currentBackgroundImage?.media_details?.sizes?.[ newSizeSlug ] + ?.source_url; if ( ! newUrl ) { return null; } @@ -146,7 +151,9 @@ export default function CoverInspectorControls( { const imageSizeOptions = imageSizes ?.filter( - ( { slug } ) => image?.media_details?.sizes?.[ slug ]?.source_url + ( { slug } ) => + currentBackgroundImage?.media_details?.sizes?.[ slug ] + ?.source_url ) ?.map( ( { name, slug } ) => ( { value: slug, label: name } ) ); @@ -321,7 +328,7 @@ export default function CoverInspectorControls( { /> ) } - { ! useFeaturedImage && !! imageSizeOptions?.length && ( + { !! imageSizeOptions?.length && ( Date: Mon, 2 Dec 2024 14:43:15 +0800 Subject: [PATCH 079/444] Fix List View not updating when switching editor modes (#67379) * Add back the editorTool dependency to getEnabledClientIdsTree * Add e2e test * Switch to getEditorMode selector ---- Co-authored-by: talldan Co-authored-by: aaronrobertshaw --- .../src/store/private-selectors.js | 8 +-- .../editor/various/write-design-mode.spec.js | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 9779ae1300fb5..61b17a3625d15 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -109,16 +109,16 @@ function getEnabledClientIdsTreeUnmemoized( state, rootClientId ) { * * @return {Object[]} Tree of block objects with only clientID and innerBlocks set. */ -export const getEnabledClientIdsTree = createSelector( - getEnabledClientIdsTreeUnmemoized, - ( state ) => [ +export const getEnabledClientIdsTree = createRegistrySelector( ( select ) => + createSelector( getEnabledClientIdsTreeUnmemoized, ( state ) => [ state.blocks.order, state.derivedBlockEditingModes, state.derivedNavModeBlockEditingModes, state.blockEditingModes, state.settings.templateLock, state.blockListSettings, - ] + select( STORE_NAME ).__unstableGetEditorMode( state ), + ] ) ); /** diff --git a/test/e2e/specs/editor/various/write-design-mode.spec.js b/test/e2e/specs/editor/various/write-design-mode.spec.js index 053f4cb8ff092..2892b4aea89e9 100644 --- a/test/e2e/specs/editor/various/write-design-mode.spec.js +++ b/test/e2e/specs/editor/various/write-design-mode.spec.js @@ -121,4 +121,59 @@ test.describe( 'Write/Design mode', () => { editorSettings.getByRole( 'button', { name: 'Content' } ) ).toBeVisible(); } ); + + test( 'hides the blocks that cannot be interacted with in List View', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.setContent( '' ); + + // Insert a section with a nested block and an editable block. + await editor.insertBlock( { + name: 'core/group', + attributes: {}, + innerBlocks: [ + { + name: 'core/group', + attributes: { + metadata: { + name: 'Non-content block', + }, + }, + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { + content: 'Something', + }, + }, + ], + }, + ], + } ); + + // Select the inner paragraph block so that List View is expanded. + await editor.canvas + .getByRole( 'document', { + name: 'Block: Paragraph', + } ) + .click(); + + // Open List View. + await pageUtils.pressKeys( 'access+o' ); + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + } ); + const nonContentBlock = listView.getByRole( 'link', { + name: 'Non-content block', + } ); + + await expect( nonContentBlock ).toBeVisible(); + + // Switch to write mode. + await editor.switchEditorTool( 'Write' ); + + await expect( nonContentBlock ).toBeHidden(); + } ); } ); From d251f755481bdfe6eac99ab85f6410e3334622cc Mon Sep 17 00:00:00 2001 From: Eshaan Dabasiya <76681468+im3dabasia@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:10:03 +0530 Subject: [PATCH 080/444] Experiments: Remove trailing space in Color randomizer (#67457) Co-authored-by: im3dabasia Co-authored-by: ramonjd --- lib/experiments-page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 9033e3c2d0c1f..256a185a3af69 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -69,7 +69,7 @@ function gutenberg_initialize_experiments_settings() { add_settings_field( 'gutenberg-color-randomizer', - __( 'Color randomizer ', 'gutenberg' ), + __( 'Color randomizer', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', From 2f670d56405a6c43800e5a5068bb8ee75b9205d9 Mon Sep 17 00:00:00 2001 From: Yogesh Bhutkar Date: Mon, 2 Dec 2024 15:30:08 +0530 Subject: [PATCH 081/444] Remove inline-block display from image anchor in style.scss (#67368) * Remove inline-block display from image anchor in style.scss * Refactor: Set image anchor display to inline-block in style.scss Co-authored-by: yogeshbhutkar Co-authored-by: t-hamano Co-authored-by: Infinite-Null Co-authored-by: hanneslsm Co-authored-by: frkly --- packages/block-library/src/image/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index a7fcb8f175e4e..8ca5795cfd911 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -1,6 +1,7 @@ .wp-block-image { - a { + > a, + > figure > a { display: inline-block; } From 358fb8e04445ebf29140ba875e273bba8fd43913 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:07:39 +0000 Subject: [PATCH 082/444] Inserter: Patterns: remove loading indicator (#67072) --- .../inserter/block-patterns-tab/index.js | 18 +----------------- .../src/store/private-selectors.js | 15 --------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js index 01e41111b7c89..45db4732aa9c6 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js @@ -3,9 +3,8 @@ */ import { useState } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; -import { Button, Spinner } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -16,8 +15,6 @@ import { PatternCategoryPreviews } from './pattern-category-previews'; import { usePatternCategories } from './use-pattern-categories'; import CategoryTabs from '../category-tabs'; import InserterNoResults from '../no-results'; -import { store as blockEditorStore } from '../../../store'; -import { unlock } from '../../../lock-unlock'; function BlockPatternsTab( { onSelectCategory, @@ -31,19 +28,6 @@ function BlockPatternsTab( { const categories = usePatternCategories( rootClientId ); const isMobile = useViewportMatch( 'medium', '<' ); - const isResolvingPatterns = useSelect( - ( select ) => - unlock( select( blockEditorStore ) ).isResolvingPatterns(), - [] - ); - - if ( isResolvingPatterns ) { - return ( -
- -
- ); - } if ( ! categories.length ) { return ; diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 61b17a3625d15..c46778d889b3e 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -406,21 +406,6 @@ export const getAllPatterns = createRegistrySelector( ( select ) => }, getAllPatternsDependants( select ) ) ); -export const isResolvingPatterns = createRegistrySelector( ( select ) => - createSelector( ( state ) => { - const blockPatternsSelect = state.settings[ selectBlockPatternsKey ]; - const reusableBlocksSelect = state.settings[ reusableBlocksSelectKey ]; - return ( - ( blockPatternsSelect - ? blockPatternsSelect( select ) === undefined - : false ) || - ( reusableBlocksSelect - ? reusableBlocksSelect( select ) === undefined - : false ) - ); - }, getAllPatternsDependants( select ) ) -); - const EMPTY_ARRAY = []; export const getReusableBlocks = createRegistrySelector( From 22c43ff9e687eb5752b790316f2b1eeb99976dbb Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 2 Dec 2024 10:40:27 +0000 Subject: [PATCH 083/444] Disable Zoom Out if no section root to allow for Theme opt in (#67232) * Disable toolbar and auto inserter behaviour if no section root * Remove useless coercion Co-authored-by: Ramon * Remove more coercion copy/paste Co-authored-by: Ramon * Add section root to Zoom Out e2e test * Add test coverage * Add some test coverage * Try e2e test fix by reverting all template part changes in Theme * Remove need to exit Zoom Out * Ensure a main tag * Update tests to expect the click-through behavior * Simplify selection --------- Co-authored-by: getdave Co-authored-by: talldan Co-authored-by: jeryj Co-authored-by: ramonjd Co-authored-by: draganescu --- .../src/components/inserter/menu.js | 9 ++- .../editor/src/components/header/index.js | 16 ++++- .../editor/various/parsing-patterns.spec.js | 3 +- .../editor/various/pattern-overrides.spec.js | 70 ++++++++++++++++--- test/e2e/specs/site-editor/zoom-out.spec.js | 45 +++++++++++- 5 files changed, 127 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 8dc2f64063c8e..ef260beb85906 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -58,6 +58,11 @@ function InserterMenu( ( select ) => unlock( select( blockEditorStore ) ).isZoomOut(), [] ); + const hasSectionRootClientId = useSelect( + ( select ) => + !! unlock( select( blockEditorStore ) ).getSectionRootClientId(), + [] + ); const [ filterValue, setFilterValue, delayedFilterValue ] = useDebouncedInput( __experimentalFilterValue ); const [ hoveredItem, setHoveredItem ] = useState( null ); @@ -81,7 +86,9 @@ function InserterMenu( const [ selectedTab, setSelectedTab ] = useState( getInitialTab() ); const shouldUseZoomOut = - selectedTab === 'patterns' || selectedTab === 'media'; + hasSectionRootClientId && + ( selectedTab === 'patterns' || selectedTab === 'media' ); + useZoomOut( shouldUseZoomOut && isLargeViewport ); const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] = diff --git a/packages/editor/src/components/header/index.js b/packages/editor/src/components/header/index.js index 51c341f2c1bd1..79199b15b1ad1 100644 --- a/packages/editor/src/components/header/index.js +++ b/packages/editor/src/components/header/index.js @@ -30,6 +30,7 @@ import { PATTERN_POST_TYPE, NAVIGATION_POST_TYPE, } from '../../store/constants'; +import { unlock } from '../../lock-unlock'; const toolbarVariations = { distractionFreeDisabled: { y: '-50px' }, @@ -102,6 +103,13 @@ function Header( { ( hasFixedToolbar && ( ! hasBlockSelection || isBlockToolsCollapsed ) ) ); const hasBackButton = useHasBackButton(); + + const hasSectionRootClientId = useSelect( + ( select ) => + !! unlock( select( blockEditorStore ) ).getSectionRootClientId(), + [] + ); + /* * The edit-post-header classname is only kept for backward compatability * as some plugins might be relying on its presence. @@ -169,9 +177,11 @@ function Header( { forceIsAutosaveable={ forceIsDirty } /> - { canBeZoomedOut && isWideViewport && ( - - ) } + { canBeZoomedOut && + isWideViewport && + hasSectionRootClientId && ( + + ) } { ( isWideViewport || ! showIconLabels ) && ( diff --git a/test/e2e/specs/editor/various/parsing-patterns.spec.js b/test/e2e/specs/editor/various/parsing-patterns.spec.js index d8edc544ffa03..98261804acb58 100644 --- a/test/e2e/specs/editor/various/parsing-patterns.spec.js +++ b/test/e2e/specs/editor/various/parsing-patterns.spec.js @@ -37,9 +37,8 @@ test.describe( 'Parsing patterns', () => { } ); } ); - // Exit zoom out mode and select the inner buttons block to ensure + // Select the inner buttons block to ensure // the correct insertion point is selected. - await page.getByRole( 'button', { name: 'Zoom Out' } ).click(); await editor.selectBlocks( editor.canvas.locator( 'role=document[name="Block: Button"i]' ) ); diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 7069b4cec258a..7d2be84187ef6 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -268,12 +268,25 @@ test.describe( 'Pattern Overrides', () => { } ); await editor.setContent( '' ); + await editor.switchEditorTool( 'Design' ); + // Insert a `
` group block. + // In zoomed out and write mode it acts as the section root. + // Inside is a pattern that acts as a section. await editor.insertBlock( { - name: 'core/block', - attributes: { ref: id }, + name: 'core/group', + attributes: { tagName: 'main' }, + innerBlocks: [ + { + name: 'core/block', + attributes: { ref: id }, + }, + ], } ); + const groupBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Group', + } ); const patternBlock = editor.canvas.getByRole( 'document', { name: 'Block: Pattern', } ); @@ -290,14 +303,35 @@ test.describe( 'Pattern Overrides', () => { hasText: 'No Overrides or Binding', } ); - await test.step( 'Zoomed in / Design mode', async () => { - await editor.switchEditorTool( 'Design' ); - // In zoomed in and design mode the pattern block and child blocks - // with bindings are editable. + await test.step( 'Click-through behavior', async () => { + // With the group block selected, all the inner blocks of the pattern + // are inert due to the 'click-through' behavior, that requires the + // pattern block be selected first before its inner blocks are selectable. + await editor.selectBlocks( groupBlock ); await expect( patternBlock ).not.toHaveAttribute( 'inert', 'true' ); + await expect( blockWithOverrides ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithBindings ).toHaveAttribute( + 'inert', + 'true' + ); + await expect( blockWithoutOverridesOrBindings ).toHaveAttribute( + 'inert', + 'true' + ); + } ); + + await test.step( 'Zoomed in / Design mode', async () => { + await editor.selectBlocks( patternBlock ); + + // Once selected and in zoomed in/design mode the child blocks + // of the pattern with bindings are editable, but unbound + // blocks are inert. await expect( blockWithOverrides ).not.toHaveAttribute( 'inert', 'true' @@ -314,11 +348,16 @@ test.describe( 'Pattern Overrides', () => { await test.step( 'Zoomed in / Write mode - pattern as a section', async () => { await editor.switchEditorTool( 'Write' ); + // The pattern block is still editable as a section. await expect( patternBlock ).not.toHaveAttribute( 'inert', 'true' ); + + // Ensure the pattern block is selected. + await editor.selectBlocks( patternBlock ); + // Child blocks of the pattern with bindings are editable. await expect( blockWithOverrides ).not.toHaveAttribute( 'inert', @@ -336,11 +375,18 @@ test.describe( 'Pattern Overrides', () => { await test.step( 'Zoomed out / Write mode - pattern as a section', async () => { await page.getByLabel( 'Zoom Out' ).click(); - // In zoomed out only the pattern block is editable, as in this scenario it's a section. + // In zoomed out only the pattern block is editable, + // as in this scenario it's a section. await expect( patternBlock ).not.toHaveAttribute( 'inert', 'true' ); + + // Ensure the pattern block is selected before checking the child blocks + // to ensure the click-through behavior isn't interfering. + await editor.selectBlocks( patternBlock ); + + // None of the child blocks are editable in zoomed out mode. await expect( blockWithOverrides ).toHaveAttribute( 'inert', 'true' @@ -357,11 +403,17 @@ test.describe( 'Pattern Overrides', () => { await test.step( 'Zoomed out / Design mode - pattern as a section', async () => { await editor.switchEditorTool( 'Design' ); - // In zoomed out only the pattern block is editable, as in this scenario it's a section. + // In zoomed out only the pattern block is editable, + // as in this scenario it's a section. await expect( patternBlock ).not.toHaveAttribute( 'inert', 'true' ); + + // Ensure the pattern block is selected before checking the child blocks + // to ensure the click-through behavior isn't interfering. + await editor.selectBlocks( patternBlock ); + await expect( blockWithOverrides ).toHaveAttribute( 'inert', 'true' @@ -376,7 +428,7 @@ test.describe( 'Pattern Overrides', () => { ); } ); - // Zoom out and group the pattern. + // Zoom out and group the pattern so that it's no longer a section. await page.getByLabel( 'Zoom Out' ).click(); await editor.selectBlocks( patternBlock ); await editor.clickBlockOptionsMenuItem( 'Group' ); diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js index e698a94b7cf0d..77d121e199939 100644 --- a/test/e2e/specs/site-editor/zoom-out.spec.js +++ b/test/e2e/specs/site-editor/zoom-out.spec.js @@ -4,7 +4,8 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); const EDITOR_ZOOM_OUT_CONTENT = ` - + +

First Section Start

@@ -58,6 +59,21 @@ const EDITOR_ZOOM_OUT_CONTENT = `

Fourth Section End

+
+`; + +const EDITOR_ZOOM_OUT_CONTENT_NO_SECTION_ROOT = ` +
+

First Section Start

+ + + +

First Section Center

+ + + +

First Section End

+
`; test.describe( 'Zoom Out', () => { @@ -67,6 +83,8 @@ test.describe( 'Zoom Out', () => { test.afterAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'twentytwentyone' ); + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); } ); test.beforeEach( async ( { admin } ) => { @@ -215,4 +233,29 @@ test.describe( 'Zoom Out', () => { await expect( thirdSectionEnd ).toBeInViewport(); await expect( fourthSectionStart ).not.toBeInViewport(); } ); + + test( 'Zoom Out cannot be activated when the section root is missing', async ( { + page, + editor, + } ) => { + await editor.setContent( EDITOR_ZOOM_OUT_CONTENT_NO_SECTION_ROOT ); + + // Check that the Zoom Out toggle button is not visible. + await expect( + page.getByRole( 'button', { name: 'Zoom Out' } ) + ).toBeHidden(); + + // Check that activating the Patterns tab in the Inserter does not activate + // Zoom Out. + await page + .getByRole( 'button', { + name: 'Block Inserter', + exact: true, + } ) + .click(); + + await page.getByRole( 'tab', { name: 'Patterns' } ).click(); + + await expect( page.locator( '.is-zoomed-out' ) ).toBeHidden(); + } ); } ); From e92d57743b68753ee47f9c21645524e2a5b86ea4 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 2 Dec 2024 11:47:20 +0100 Subject: [PATCH 084/444] Remove one occurrence of incorrect usage of ItemGroup. (#67427) * Remove one occurrence of incorret usage of ItemGroup. * Fix toggling visibility of the help text. Co-authored-by: afercia Co-authored-by: gziolo Co-authored-by: Mamaduka Co-authored-by: SantosGuillamot --- packages/block-editor/src/hooks/block-bindings.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index 615804a311c0f..e10696cc1257d 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -300,13 +300,17 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { /> ) } - - + { /* + Use a div element to make the ToolsPanelHiddenInnerWrapper + toggle the visibility of this help text automatically. + */ } + +

{ __( 'Attributes connected to custom fields or other dynamic data.' ) } - - +

+
); From aef323a70e74fcf676036139bd25f657ad2a0b02 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:59:01 +0000 Subject: [PATCH 085/444] PR template: add before/after table (#62739) Co-authored-by: ellatrix Co-authored-by: t-hamano --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fd63e5e2e5312..69fd34d709bdc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,3 +20,9 @@ https://github.com/WordPress/gutenberg/blob/trunk/CONTRIBUTING.md --> ## Screenshots or screencast + + + +|Before|After| +|-|-| +||| From 8d343d155c7577d46aea33e708dceb39c571cc80 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 2 Dec 2024 12:26:27 +0100 Subject: [PATCH 086/444] Site Editor: Fix focus mode navigation (#67458) Co-authored-by: youknowriad Co-authored-by: Mamaduka Co-authored-by: carolinan --- .../use-navigate-to-entity-record.js | 2 +- packages/router/src/router.tsx | 1 + .../template-part-focus-mode.spec.js | 50 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/e2e/specs/site-editor/template-part-focus-mode.spec.js diff --git a/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js b/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js index 8cc7fdaefe2d9..66be70fcd4e2e 100644 --- a/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js +++ b/packages/edit-site/src/components/block-editor/use-navigate-to-entity-record.js @@ -17,7 +17,7 @@ export default function useNavigateToEntityRecord() { const onNavigateToEntityRecord = useCallback( ( params ) => { history.navigate( - `/${ params.postType }/${ params.id }?canvas=edit&focusMode=true` + `/${ params.postType }/${ params.postId }?canvas=edit&focusMode=true` ); }, [ history ] diff --git a/packages/router/src/router.tsx b/packages/router/src/router.tsx index 34cc542c7b573..2ac7974b4dfbc 100644 --- a/packages/router/src/router.tsx +++ b/packages/router/src/router.tsx @@ -146,6 +146,7 @@ export function useHistory() { return useMemo( () => ( { navigate, + back: history.back, } ), [ navigate ] ); diff --git a/test/e2e/specs/site-editor/template-part-focus-mode.spec.js b/test/e2e/specs/site-editor/template-part-focus-mode.spec.js new file mode 100644 index 0000000000000..29e6788779ed9 --- /dev/null +++ b/test/e2e/specs/site-editor/template-part-focus-mode.spec.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Template Part Focus mode', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyfour' ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'Should navigate to template part and back.', async ( { + admin, + page, + editor, + } ) => { + await admin.visitAdminPage( 'site-editor.php?canvas=edit' ); + await editor.setPreferences( 'core/edit-site', { + welcomeGuide: false, + } ); + + // Check that we're editing the template + await expect( page.locator( 'h1' ) ).toContainText( 'Blog Home' ); + await expect( page.locator( 'h1' ) ).toContainText( 'Template' ); + + // Click Template Part + await editor.canvas + .getByRole( 'document', { + name: 'Header', + } ) + .click(); + + // Navigate to Focus mode + await editor.clickBlockToolbarButton( 'Edit' ); + + // Check if focus mode is active + await expect( page.locator( 'h1' ) ).toContainText( 'Header' ); + await expect( page.locator( 'h1' ) ).toContainText( 'Template Part' ); + + // Go back + await page.getByRole( 'button', { name: 'Back' } ).click(); + + // Check that we're editing the template + await expect( page.locator( 'h1' ) ).toContainText( 'Blog Home' ); + await expect( page.locator( 'h1' ) ).toContainText( 'Template' ); + } ); +} ); From 141e9cd884053db9baec26e43297162841f9e7a7 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 2 Dec 2024 12:51:58 +0100 Subject: [PATCH 087/444] Site editor: Allow access to quick edit (#67469) Co-authored-by: youknowriad Co-authored-by: jsnajdr --- packages/edit-site/src/components/site-editor-routes/pages.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/site-editor-routes/pages.js b/packages/edit-site/src/components/site-editor-routes/pages.js index e8c55cd10307e..5f2a4e341e0dc 100644 --- a/packages/edit-site/src/components/site-editor-routes/pages.js +++ b/packages/edit-site/src/components/site-editor-routes/pages.js @@ -44,7 +44,7 @@ export const pagesRoute = { mobile: , edit( { query } ) { const hasQuickEdit = - ( query.layout ?? 'list' ) === 'list' && !! query.quickEdit; + ( query.layout ?? 'list' ) !== 'list' && !! query.quickEdit; return hasQuickEdit ? ( ) : undefined; @@ -59,7 +59,7 @@ export const pagesRoute = { }, edit( { query } ) { const hasQuickEdit = - ( query.layout ?? 'list' ) === 'list' && !! query.quickEdit; + ( query.layout ?? 'list' ) !== 'list' && !! query.quickEdit; return hasQuickEdit ? 380 : undefined; }, }, From e07fe5cd9c17702e242df8ab547bddd1c4d79f52 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:01:28 +0000 Subject: [PATCH 088/444] Preload: parse post ID from p (path) (#67465) Co-authored-by: ellatrix Co-authored-by: youknowriad --- backport-changelog/6.8/7695.md | 1 + lib/compat/wordpress-6.8/preload.php | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backport-changelog/6.8/7695.md b/backport-changelog/6.8/7695.md index 095c058e6fd10..f45b2039e3027 100644 --- a/backport-changelog/6.8/7695.md +++ b/backport-changelog/6.8/7695.md @@ -1,3 +1,4 @@ https://github.com/WordPress/wordpress-develop/pull/7695 * https://github.com/WordPress/gutenberg/pull/66631 +* https://github.com/WordPress/gutenberg/pull/67465 diff --git a/lib/compat/wordpress-6.8/preload.php b/lib/compat/wordpress-6.8/preload.php index 6d92913b41411..494e3ad32ec02 100644 --- a/lib/compat/wordpress-6.8/preload.php +++ b/lib/compat/wordpress-6.8/preload.php @@ -10,8 +10,16 @@ */ function gutenberg_block_editor_preload_paths_6_8( $paths, $context ) { if ( 'core/edit-site' === $context->name ) { - if ( ! empty( $_GET['postId'] ) ) { - $route_for_post = rest_get_route_for_post( $_GET['postId'] ); + $post_id = null; + if ( isset( $_GET['postId'] ) && is_numeric( $_GET['postId'] ) ) { + $post_id = (int) $_GET['postId']; + } + if ( isset( $_GET['p'] ) && preg_match( '/^\/page\/(\d+)$/', $_GET['p'], $matches ) ) { + $post_id = (int) $matches[1]; + } + + if ( $post_id ) { + $route_for_post = rest_get_route_for_post( $post_id ); if ( $route_for_post ) { $paths[] = add_query_arg( 'context', 'edit', $route_for_post ); } From 39a4d1c93fd3f9bee19db3566e92ce4a03e67544 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 2 Dec 2024 13:14:14 +0100 Subject: [PATCH 089/444] DataViews: Better handling of missing onClickItem prop (#67402) Co-authored-by: youknowriad Co-authored-by: jsnajdr --- .../src/components/dataviews-context/index.ts | 5 ++--- .../src/components/dataviews/index.tsx | 5 ++--- .../src/dataviews-layouts/grid/index.tsx | 14 +++++++------- .../src/dataviews-layouts/table/index.tsx | 16 ++++++++-------- .../utils/get-clickable-item-props.ts | 19 ++++++++++++------- packages/dataviews/src/types.ts | 2 +- 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts index 19f6b4178b7b5..4bef3ecdbcbb4 100644 --- a/packages/dataviews/src/components/dataviews-context/index.ts +++ b/packages/dataviews/src/components/dataviews-context/index.ts @@ -26,7 +26,7 @@ type DataViewsContextType< Item > = { openedFilter: string | null; setOpenedFilter: ( openedFilter: string | null ) => void; getItemId: ( item: Item ) => string; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; isItemClickable: ( item: Item ) => boolean; }; @@ -44,8 +44,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( { setOpenedFilter: () => {}, openedFilter: null, getItemId: ( item ) => item.id, - onClickItem: () => {}, - isItemClickable: () => false, + isItemClickable: () => true, } ); export default DataViewsContext; diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index ee6073f40bf3d..99d9b6d684b08 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -52,8 +52,7 @@ type DataViewsProps< Item > = { : { getItemId: ( item: Item ) => string } ); const defaultGetItemId = ( item: ItemWithId ) => item.id; -const defaultIsItemClickable = () => false; -const defaultOnClickItem = () => {}; +const defaultIsItemClickable = () => true; const EMPTY_ARRAY: any[] = []; export default function DataViews< Item >( { @@ -70,7 +69,7 @@ export default function DataViews< Item >( { defaultLayouts, selection: selectionProperty, onChangeSelection, - onClickItem = defaultOnClickItem, + onClickItem, isItemClickable = defaultIsItemClickable, header, }: DataViewsProps< Item > ) { diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index 2a09fb68efab8..17053e01604a5 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -31,7 +31,7 @@ interface GridItemProps< Item > { selection: string[]; onChangeSelection: SetSelection; getItemId: ( item: Item ) => string; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; isItemClickable: ( item: Item ) => boolean; item: Item; actions: Action< Item >[]; @@ -66,19 +66,19 @@ function GridItem< Item >( { ) : null; - const clickableMediaItemProps = getClickableItemProps( + const clickableMediaItemProps = getClickableItemProps( { item, isItemClickable, onClickItem, - 'dataviews-view-grid__media' - ); + className: 'dataviews-view-grid__media', + } ); - const clickablePrimaryItemProps = getClickableItemProps( + const clickablePrimaryItemProps = getClickableItemProps( { item, isItemClickable, onClickItem, - 'dataviews-view-grid__primary-field' - ); + className: 'dataviews-view-grid__primary-field', + } ); return ( { field: NormalizedField< Item >; item: Item; isItemClickable: ( item: Item ) => boolean; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; } interface TableColumnCombinedProps< Item > { @@ -52,7 +52,7 @@ interface TableColumnCombinedProps< Item > { item: Item; view: ViewTableType; isItemClickable: ( item: Item ) => boolean; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; } interface TableColumnProps< Item > { @@ -62,7 +62,7 @@ interface TableColumnProps< Item > { column: string; view: ViewTableType; isItemClickable: ( item: Item ) => boolean; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; } interface TableRowProps< Item > { @@ -77,7 +77,7 @@ interface TableRowProps< Item > { getItemId: ( item: Item ) => string; onChangeSelection: SetSelection; isItemClickable: ( item: Item ) => boolean; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; } function TableColumn< Item >( { @@ -118,12 +118,12 @@ function TableColumnField< Item >( { const isItemClickableField = ( i: Item ) => isItemClickable( i ) && isPrimaryField; - const clickableProps = getClickableItemProps( + const clickableProps = getClickableItemProps( { item, - isItemClickableField, + isItemClickable: isItemClickableField, onClickItem, - 'dataviews-view-table__cell-content' - ); + className: 'dataviews-view-table__cell-content', + } ); return (
( - item: Item, - isItemClickable: ( item: Item ) => boolean, - onClickItem: ( item: Item ) => void, - className: string -) { - if ( ! isItemClickable( item ) ) { +export default function getClickableItemProps< Item >( { + item, + isItemClickable, + onClickItem, + className, +}: { + item: Item; + isItemClickable: ( item: Item ) => boolean; + onClickItem?: ( item: Item ) => void; + className: string; +} ) { + if ( ! isItemClickable( item ) || ! onClickItem ) { return { className }; } diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 861dc53404f91..0bce8b8cf2c08 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -510,7 +510,7 @@ export interface ViewBaseProps< Item > { onChangeSelection: SetSelection; selection: string[]; setOpenedFilter: ( fieldId: string ) => void; - onClickItem: ( item: Item ) => void; + onClickItem?: ( item: Item ) => void; isItemClickable: ( item: Item ) => boolean; view: View; } From 340d617fa5cdab9b9402960de32c6496ef563d1d Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:31:54 +0100 Subject: [PATCH 090/444] [mini] drag and drop: fix misplaced drop indicator (#67434) Co-authored-by: ellatrix Co-authored-by: youknowriad Co-authored-by: Mamaduka Co-authored-by: ntsekouras --- .../block-editor/src/components/block-popover/inbetween.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js index 2ed9ee0bcb284..1d7c176673240 100644 --- a/packages/block-editor/src/components/block-popover/inbetween.js +++ b/packages/block-editor/src/components/block-popover/inbetween.js @@ -148,6 +148,10 @@ function BlockPopoverInbetween( { ? nextRect.left - previousRect.right : 0; } + + // Avoid a negative width which happens when the next rect + // is on the next line. + width = Math.max( width, 0 ); } return new window.DOMRect( left, top, width, height ); From c517e410017f4d65a7c6f03a31c5c2fa15cbbd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Mon, 2 Dec 2024 13:32:53 +0100 Subject: [PATCH 091/444] Extensibility: Make Block Bindings work with `editor.BlockEdit` hook (#67370) * Block Bindings: Move the place when the attributes get overriden * Fix failing unit tests * Wrap Edit with bindings logic only when the block supports it * Extend the test plugin with `editor.BlockEdit` filter * Add a new test covering the extensibility inside inspector controls * Fix the issue with missing context established by sources Co-authored-by: gziolo Co-authored-by: SantosGuillamot --- .../src/components/block-edit/edit.js | 12 +- .../with-block-bindings-support.js} | 103 ++++-------------- .../block-list/use-block-props/index.js | 2 +- .../src/components/rich-text/index.js | 2 +- .../block-editor/src/hooks/block-bindings.js | 10 +- packages/block-editor/src/hooks/index.js | 1 - .../block-editor/src/utils/block-bindings.js | 37 +++++++ packages/e2e-tests/plugins/block-bindings.php | 6 +- .../e2e-tests/plugins/block-bindings/index.js | 45 ++++++++ .../various/block-bindings/post-meta.spec.js | 41 +++++++ 10 files changed, 165 insertions(+), 94 deletions(-) rename packages/block-editor/src/{hooks/use-bindings-attributes.js => components/block-edit/with-block-bindings-support.js} (73%) diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f82..6b1ddd86f4c76 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -18,6 +18,8 @@ import { useContext, useMemo } from '@wordpress/element'; * Internal dependencies */ import BlockContext from '../block-context'; +import { withBlockBindingsSupport } from './with-block-bindings-support'; +import { canBindBlock } from '../../utils/block-bindings'; /** * Default value used for blocks which do not define their own context needs, @@ -47,6 +49,8 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); +const EditWithFiltersAndBindings = withBlockBindingsSupport( EditWithFilters ); + const EditWithGeneratedProps = ( props ) => { const { attributes = {}, name } = props; const blockType = getBlockType( name ); @@ -67,8 +71,12 @@ const EditWithGeneratedProps = ( props ) => { return null; } + const EditComponent = canBindBlock( name ) + ? EditWithFiltersAndBindings + : EditWithFilters; + if ( blockType.apiVersion > 1 ) { - return ; + return ; } // Generate a class name for the block's editable form. @@ -82,7 +90,7 @@ const EditWithGeneratedProps = ( props ) => { ); return ( - ( props ) => { const registry = useRegistry(); const blockContext = useContext( BlockContext ); @@ -108,9 +72,9 @@ export const withBlockBindingSupport = createHigherOrderComponent( () => replacePatternOverrideDefaultBindings( name, - props.attributes.metadata?.bindings + props.attributes?.metadata?.bindings ), - [ props.attributes.metadata?.bindings, name ] + [ props.attributes?.metadata?.bindings, name ] ); // While this hook doesn't directly call any selectors, `useSelect` is @@ -196,7 +160,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( const hasParentPattern = !! updatedContext[ 'pattern/overrides' ]; const hasPatternOverridesDefaultBinding = - props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] + props.attributes?.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] ?.source === 'core/pattern-overrides'; const _setAttributes = useCallback( @@ -283,40 +247,13 @@ export const withBlockBindingSupport = createHigherOrderComponent( ); return ( - <> - - + ); }, 'withBlockBindingSupport' ); - -/** - * Filters a registered block's settings to enhance a block's `edit` component - * to upgrade bound attributes. - * - * @param {WPBlockSettings} settings - Registered block settings. - * @param {string} name - Block name. - * @return {WPBlockSettings} Filtered block settings. - */ -function shimAttributeSource( settings, name ) { - if ( ! canBindBlock( name ) ) { - return settings; - } - - return { - ...settings, - edit: withBlockBindingSupport( settings.edit ), - }; -} - -addFilter( - 'blocks.registerBlockType', - 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', - shimAttributeSource -); diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 4696149dc3875..7e50b75e1b956 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -29,7 +29,7 @@ import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { useScrollIntoView } from './use-scroll-into-view'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; -import { canBindBlock } from '../../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../../utils/block-bindings'; import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility'; /** diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index bc8eca6ea94d0..768ffbb0cdd2d 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -39,7 +39,7 @@ import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/block-bindings'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index e10696cc1257d..cec80dffaeaa1 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -23,15 +23,15 @@ import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ -import { - canBindAttribute, - getBindableAttributes, -} from '../hooks/use-bindings-attributes'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; import { useBlockEditContext } from '../components/block-edit'; -import { useBlockBindingsUtils } from '../utils/block-bindings'; +import { + canBindAttribute, + getBindableAttributes, + useBlockBindingsUtils, +} from '../utils/block-bindings'; import { store as blockEditorStore } from '../store'; const { Menu } = unlock( componentsPrivateApis ); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 66ff60b691b66..7f9b29376ad1f 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -32,7 +32,6 @@ import './metadata'; import blockHooks from './block-hooks'; import blockBindingsPanel from './block-bindings'; import './block-renaming'; -import './use-bindings-attributes'; import './grid-visualizer'; createBlockEditFilter( diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js index dcf80d985473b..82f0dff53531a 100644 --- a/packages/block-editor/src/utils/block-bindings.js +++ b/packages/block-editor/src/utils/block-bindings.js @@ -13,6 +13,43 @@ function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } +export const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'id', 'url', 'title', 'alt' ], + 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], +}; + +/** + * Based on the given block name, + * check if it is possible to bind the block. + * + * @param {string} blockName - The block name. + * @return {boolean} Whether it is possible to bind the block to sources. + */ +export function canBindBlock( blockName ) { + return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; +} + +/** + * Based on the given block name and attribute name, + * check if it is possible to bind the block attribute. + * + * @param {string} blockName - The block name. + * @param {string} attributeName - The attribute name. + * @return {boolean} Whether it is possible to bind the block attribute. + */ +export function canBindAttribute( blockName, attributeName ) { + return ( + canBindBlock( blockName ) && + BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) + ); +} + +export function getBindableAttributes( blockName ) { + return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; +} + /** * Contains utils to update the block `bindings` metadata. * diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index b86673c2c523d..1fd6d8468c77d 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -41,7 +41,11 @@ function gutenberg_test_block_bindings_registration() { plugins_url( 'block-bindings/index.js', __FILE__ ), array( 'wp-blocks', - 'wp-private-apis', + 'wp-block-editor', + 'wp-components', + 'wp-compose', + 'wp-element', + 'wp-hooks', ), filemtime( plugin_dir_path( __FILE__ ) . 'block-bindings/index.js' ), true diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js index 5c364257caed1..63c463e197fa8 100644 --- a/packages/e2e-tests/plugins/block-bindings/index.js +++ b/packages/e2e-tests/plugins/block-bindings/index.js @@ -1,4 +1,9 @@ const { registerBlockBindingsSource } = wp.blocks; +const { InspectorControls } = wp.blockEditor; +const { PanelBody, TextControl } = wp.components; +const { createHigherOrderComponent } = wp.compose; +const { createElement: el, Fragment } = wp.element; +const { addFilter } = wp.hooks; const { fieldsList } = window.testingBindings || {}; const getValues = ( { bindings } ) => { @@ -46,3 +51,43 @@ registerBlockBindingsSource( { getValues, canUserEditValue: () => true, } ); + +const withBlockBindingsInspectorControl = createHigherOrderComponent( + ( BlockEdit ) => { + return ( props ) => { + if ( ! props.attributes?.metadata?.bindings?.content ) { + return el( BlockEdit, props ); + } + + return el( + Fragment, + {}, + el( BlockEdit, props ), + el( + InspectorControls, + {}, + el( + PanelBody, + { title: 'Bindings' }, + el( TextControl, { + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + label: 'Content', + value: props.attributes.content, + onChange: ( content ) => + props.setAttributes( { + content, + } ), + } ) + ) + ) + ); + }; + } +); + +addFilter( + 'editor.BlockEdit', + 'testing/bindings-inspector-control', + withBlockBindingsInspectorControl +); diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js index 32334bfc777f2..318707e22f098 100644 --- a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js +++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js @@ -524,6 +524,47 @@ test.describe( 'Post Meta source', () => { previewPage.locator( '#connected-paragraph' ) ).toHaveText( 'new value' ); } ); + + test( 'should be possible to edit the value of the connected custom fields in the inspector control registered by the plugin', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const contentInput = page.getByRole( 'textbox', { + name: 'Content', + } ); + await expect( contentInput ).toHaveValue( + 'Movie field default value' + ); + await contentInput.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'fallback content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + test( 'should be possible to connect movie fields through the attributes panel', async ( { editor, page, From d3f344fe6193d7cd86e03e7a00770032e7919ab4 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 2 Dec 2024 13:17:45 +0000 Subject: [PATCH 092/444] Prefer exact matches in Link Search results sorting (#67367) * Weight towards exact matches * Add additional test coverage Co-authored-by: getdave Co-authored-by: draganescu Co-authored-by: talldan Co-authored-by: jasmussen Co-authored-by: kevin940726 Co-authored-by: ironprogrammer Co-authored-by: annezazu --- .../__experimental-fetch-link-suggestions.ts | 25 +++++++++++-- .../__experimental-fetch-link-suggestions.js | 37 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.ts b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.ts index e1a166ee272db..2901219758903 100644 --- a/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.ts +++ b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.ts @@ -270,12 +270,29 @@ export function sortResults( results: SearchResult[], search: string ) { for ( const result of results ) { if ( result.title ) { const titleTokens = tokenize( result.title ); - const matchingTokens = titleTokens.filter( ( titleToken ) => - searchTokens.some( ( searchToken ) => - titleToken.includes( searchToken ) + const exactMatchingTokens = titleTokens.filter( ( titleToken ) => + searchTokens.some( + ( searchToken ) => titleToken === searchToken ) ); - scores[ result.id ] = matchingTokens.length / titleTokens.length; + const subMatchingTokens = titleTokens.filter( ( titleToken ) => + searchTokens.some( + ( searchToken ) => + titleToken !== searchToken && + titleToken.includes( searchToken ) + ) + ); + + // The score is a combination of exact matches and sub-matches. + // More weight is given to exact matches, as they are more relevant (e.g. "cat" vs "caterpillar"). + // Diving by the total number of tokens in the title normalizes the score and skews + // the results towards shorter titles. + const exactMatchScore = + ( exactMatchingTokens.length / titleTokens.length ) * 10; + + const subMatchScore = subMatchingTokens.length / titleTokens.length; + + scores[ result.id ] = exactMatchScore + subMatchScore; } else { scores[ result.id ] = 0; } diff --git a/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js b/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js index 6878c74332c3d..ad0014ff86ecb 100644 --- a/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js +++ b/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js @@ -393,6 +393,43 @@ describe( 'sortResults', () => { 6, ] ); } ); + + it( 'orders results to prefer direct matches over sub matches', () => { + const results = [ + { + id: 1, + title: 'News', + url: 'http://wordpress.local/news/', + type: 'page', + kind: 'post-type', + }, + { + id: 2, + title: 'Newspaper', + url: 'http://wordpress.local/newspaper/', + type: 'page', + kind: 'post-type', + }, + { + id: 3, + title: 'News Flash News', + url: 'http://wordpress.local/news-flash-news/', + type: 'page', + kind: 'post-type', + }, + { + id: 4, + title: 'News', + url: 'http://wordpress.local/news-2/', + type: 'page', + kind: 'post-type', + }, + ]; + const order = sortResults( results, 'News' ).map( + ( result ) => result.id + ); + expect( order ).toEqual( [ 1, 4, 3, 2 ] ); + } ); } ); describe( 'tokenize', () => { From cd26001761b5f93143671ce1505b25732c51692e Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Mon, 2 Dec 2024 05:51:31 -0800 Subject: [PATCH 093/444] =?UTF-8?q?Fix=20Meta=20boxes=20saving=20when=20th?= =?UTF-8?q?ey=E2=80=99re=20not=20present=20(#67254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initialize meta boxes whether or not they’re visible * Add hook for initialization of meta boxes * Minimize hook for meta boxes initialization * Name the export Co-authored-by: stokesman Co-authored-by: afercia Co-authored-by: t-hamano Co-authored-by: Mamaduka Co-authored-by: enricobattocchi --- .../edit-post/src/components/layout/index.js | 13 ++++--- .../src/components/meta-boxes/index.js | 37 ++----------------- .../meta-boxes/use-meta-box-initialization.js | 32 ++++++++++++++++ 3 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 packages/edit-post/src/components/meta-boxes/use-meta-box-initialization.js diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 5dcbfa2c82cea..b8061571ec66c 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -74,6 +74,7 @@ import useEditPostCommands from '../../commands/use-commands'; import { usePaddingAppender } from './use-padding-appender'; import { useShouldIframe } from './use-should-iframe'; import useNavigateToEntityRecord from '../../hooks/use-navigate-to-entity-record'; +import { useMetaBoxInitialization } from '../meta-boxes/use-meta-box-initialization'; const { getLayoutStyles } = unlock( blockEditorPrivateApis ); const { useCommands } = unlock( coreCommandsPrivateApis ); @@ -413,6 +414,8 @@ function Layout( { const { isZoomOut } = unlock( select( blockEditorStore ) ); const { getEditorMode, getRenderingMode } = select( editorStore ); const isRenderingPostOnly = getRenderingMode() === 'post-only'; + const isNotDesignPostType = + ! DESIGN_POST_TYPES.includes( currentPostType ); return { mode: getEditorMode(), @@ -423,9 +426,7 @@ function Layout( { !! select( blockEditorStore ).getBlockSelectionStart(), showIconLabels: get( 'core', 'showIconLabels' ), isDistractionFree: get( 'core', 'distractionFree' ), - showMetaBoxes: - ! DESIGN_POST_TYPES.includes( currentPostType ) && - ! isZoomOut(), + showMetaBoxes: isNotDesignPostType && ! isZoomOut(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), templateId: supportsTemplateMode && @@ -435,9 +436,7 @@ function Layout( { ? getTemplateId( currentPostType, currentPostId ) : null, enablePaddingAppender: - ! isZoomOut() && - isRenderingPostOnly && - ! DESIGN_POST_TYPES.includes( currentPostType ), + ! isZoomOut() && isRenderingPostOnly && isNotDesignPostType, }; }, [ @@ -447,6 +446,8 @@ function Layout( { settings.supportsTemplateMode, ] ); + useMetaBoxInitialization( hasActiveMetaboxes ); + const [ paddingAppenderRef, paddingStyle ] = usePaddingAppender( enablePaddingAppender ); diff --git a/packages/edit-post/src/components/meta-boxes/index.js b/packages/edit-post/src/components/meta-boxes/index.js index 14728c97cf6b6..fdc74a5df4ce9 100644 --- a/packages/edit-post/src/components/meta-boxes/index.js +++ b/packages/edit-post/src/components/meta-boxes/index.js @@ -1,9 +1,7 @@ /** * WordPress dependencies */ -import { useSelect, useRegistry } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; -import { store as editorStore } from '@wordpress/editor'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -13,38 +11,11 @@ import MetaBoxVisibility from './meta-box-visibility'; import { store as editPostStore } from '../../store'; export default function MetaBoxes( { location } ) { - const registry = useRegistry(); - const { metaBoxes, areMetaBoxesInitialized, isEditorReady } = useSelect( - ( select ) => { - const { __unstableIsEditorReady } = select( editorStore ); - const { - getMetaBoxesPerLocation, - areMetaBoxesInitialized: _areMetaBoxesInitialized, - } = select( editPostStore ); - return { - metaBoxes: getMetaBoxesPerLocation( location ), - areMetaBoxesInitialized: _areMetaBoxesInitialized(), - isEditorReady: __unstableIsEditorReady(), - }; - }, - [ location ] + const metaBoxes = useSelect( + ( select ) => + select( editPostStore ).getMetaBoxesPerLocation[ location ] ); - const hasMetaBoxes = !! metaBoxes?.length; - - // When editor is ready, initialize postboxes (wp core script) and metabox - // saving. This initializes all meta box locations, not just this specific - // one. - useEffect( () => { - if ( isEditorReady && hasMetaBoxes && ! areMetaBoxesInitialized ) { - registry.dispatch( editPostStore ).initializeMetaBoxes(); - } - }, [ isEditorReady, hasMetaBoxes, areMetaBoxesInitialized ] ); - - if ( ! areMetaBoxesInitialized ) { - return null; - } - return ( <> { ( metaBoxes ?? [] ).map( ( { id } ) => ( diff --git a/packages/edit-post/src/components/meta-boxes/use-meta-box-initialization.js b/packages/edit-post/src/components/meta-boxes/use-meta-box-initialization.js new file mode 100644 index 0000000000000..4309d85e3c22b --- /dev/null +++ b/packages/edit-post/src/components/meta-boxes/use-meta-box-initialization.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as editPostStore } from '../../store'; + +/** + * Initializes WordPress `postboxes` script and the logic for saving meta boxes. + * + * @param { boolean } enabled + */ +export const useMetaBoxInitialization = ( enabled ) => { + const isEnabledAndEditorReady = useSelect( + ( select ) => + enabled && select( editorStore ).__unstableIsEditorReady(), + [ enabled ] + ); + const { initializeMetaBoxes } = useDispatch( editPostStore ); + // The effect has to rerun when the editor is ready because initializeMetaBoxes + // will noop until then. + useEffect( () => { + if ( isEnabledAndEditorReady ) { + initializeMetaBoxes(); + } + }, [ isEnabledAndEditorReady, initializeMetaBoxes ] ); +}; From d0c372c881cb7d68bf56703df58f057614817c02 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:06:19 +0100 Subject: [PATCH 094/444] Fix write mode persisting after disabling the experiment Co-authored-by: SantosGuillamot Co-authored-by: getdave --- packages/block-editor/src/store/selectors.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 75c43770f7e17..dc90f35173252 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -3041,9 +3041,7 @@ export const getBlockEditingMode = createRegistrySelector( clientId = ''; } - const isNavMode = - select( preferencesStore )?.get( 'core', 'editorTool' ) === - 'navigation'; + const isNavMode = isNavigationMode( state ); // If the editor is currently not in navigation mode, check if the clientId // has an editing mode set in the regular derived map. From 65fa4f3b0273afd7a7b578614a6e9ffd80d05de5 Mon Sep 17 00:00:00 2001 From: Hit Bhalodia <58802366+hbhalodia@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:11:50 +0530 Subject: [PATCH 095/444] NumberControl: Deprecate 36px default size (#66730) * Add the deprecation for 36px default size to number control * Add the changelog for the deprecation * Update unit test and supress warning for 40px default size warning from child component * Add the deperection changelog to unreleased section and updated the component to use __next40pxDefaultSize for NumberControl * Refactor the test for NumberControl component to reduce changes * Update box control files to use supress warning prop before default 40px prop * Supress the console warning for deprecation message from child component * Addressed the feedbacks on the PR and updated the component based on that --------- Co-authored-by: hbhalodia Co-authored-by: mirka <0mirka00@git.wordpress.org> --- .../src/components/line-height-control/index.js | 1 + packages/components/CHANGELOG.md | 1 + packages/components/src/angle-picker-control/index.tsx | 2 +- .../components/src/color-picker/input-with-slider.tsx | 2 +- packages/components/src/number-control/README.md | 3 ++- packages/components/src/number-control/index.tsx | 9 +++++++++ .../src/number-control/stories/index.story.tsx | 1 + packages/components/src/number-control/test/index.tsx | 6 +++++- packages/components/src/number-control/types.ts | 7 +++++++ packages/components/src/range-control/index.tsx | 1 + packages/components/src/unit-control/index.tsx | 1 + 11 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/line-height-control/index.js b/packages/block-editor/src/components/line-height-control/index.js index b2c99c03f8784..e6af602c2875a 100644 --- a/packages/block-editor/src/components/line-height-control/index.js +++ b/packages/block-editor/src/components/line-height-control/index.js @@ -93,6 +93,7 @@ const LineHeightControl = ( {
} spinControls="none" - size="__unstable-large" /> { return ( {}; @@ -53,9 +54,17 @@ function UnforwardedNumberControl( size = 'default', suffix, onChange = noop, + __shouldNotWarnDeprecated36pxSize, ...restProps } = useDeprecated36pxDefaultSizeProp< NumberControlProps >( props ); + maybeWarnDeprecated36pxSize( { + componentName: 'NumberControl', + size, + __next40pxDefaultSize: restProps.__next40pxDefaultSize, + __shouldNotWarnDeprecated36pxSize, + } ); + if ( hideHTMLArrows ) { deprecated( 'wp.components.NumberControl hideHTMLArrows prop ', { alternative: 'spinControls="none"', diff --git a/packages/components/src/number-control/stories/index.story.tsx b/packages/components/src/number-control/stories/index.story.tsx index 3feb0d63eadc2..8710839fea6ea 100644 --- a/packages/components/src/number-control/stories/index.story.tsx +++ b/packages/components/src/number-control/stories/index.story.tsx @@ -62,4 +62,5 @@ const Template: StoryFn< typeof NumberControl > = ( { export const Default = Template.bind( {} ); Default.args = { label: 'Value', + __next40pxDefaultSize: true, }; diff --git a/packages/components/src/number-control/test/index.tsx b/packages/components/src/number-control/test/index.tsx index 3cf3368f1636b..bf97b520673ea 100644 --- a/packages/components/src/number-control/test/index.tsx +++ b/packages/components/src/number-control/test/index.tsx @@ -12,9 +12,13 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import NumberControl from '..'; +import _NumberControl from '..'; import type { NumberControlProps } from '../types'; +const NumberControl = ( + props: React.ComponentProps< typeof _NumberControl > +) => <_NumberControl __next40pxDefaultSize { ...props } />; + function StatefulNumberControl( props: NumberControlProps ) { const [ value, setValue ] = useState( props.value ); const handleOnChange = ( v: string | undefined ) => setValue( v ); diff --git a/packages/components/src/number-control/types.ts b/packages/components/src/number-control/types.ts index 8d198e777bd55..2a0fbf402d356 100644 --- a/packages/components/src/number-control/types.ts +++ b/packages/components/src/number-control/types.ts @@ -91,4 +91,11 @@ export type NumberControlProps = Omit< * The value of the input. */ value?: number | string; + /** + * Do not throw a warning for the deprecated 36px default size. + * For internal components of other components that already throw the warning. + * + * @ignore + */ + __shouldNotWarnDeprecated36pxSize?: boolean; }; diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx index 916571c3ee0e0..89dd8248a1e61 100644 --- a/packages/components/src/range-control/index.tsx +++ b/packages/components/src/range-control/index.tsx @@ -350,6 +350,7 @@ function UnforwardedRangeControl( step={ step } // @ts-expect-error TODO: Investigate if the `null` value is necessary value={ inputSliderValue } + __shouldNotWarnDeprecated36pxSize /> ) } { allowReset && ( diff --git a/packages/components/src/unit-control/index.tsx b/packages/components/src/unit-control/index.tsx index 9845c4eb04ef2..65e1e56cda3b3 100644 --- a/packages/components/src/unit-control/index.tsx +++ b/packages/components/src/unit-control/index.tsx @@ -224,6 +224,7 @@ function UnforwardedUnitControl( return ( Date: Mon, 2 Dec 2024 16:07:57 +0100 Subject: [PATCH 096/444] useEditorTitle: fix wrong request without ID (#67475) Co-authored-by: ellatrix Co-authored-by: youknowriad --- .../src/components/editor/use-editor-title.js | 4 ++ test/e2e/specs/site-editor/preload.spec.js | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 test/e2e/specs/site-editor/preload.spec.js diff --git a/packages/edit-site/src/components/editor/use-editor-title.js b/packages/edit-site/src/components/editor/use-editor-title.js index 727b190117e02..6f0b36f8e3b8b 100644 --- a/packages/edit-site/src/components/editor/use-editor-title.js +++ b/packages/edit-site/src/components/editor/use-editor-title.js @@ -22,6 +22,10 @@ function useEditorTitle( postType, postId ) { const { getEditedEntityRecord, hasFinishedResolution } = select( coreStore ); + if ( ! postId ) { + return { isLoaded: false }; + } + const _record = getEditedEntityRecord( 'postType', postType, diff --git a/test/e2e/specs/site-editor/preload.spec.js b/test/e2e/specs/site-editor/preload.spec.js new file mode 100644 index 0000000000000..1e93f783a8a91 --- /dev/null +++ b/test/e2e/specs/site-editor/preload.spec.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Preload', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'Should make no requests before the iframe is loaded', async ( { + page, + admin, + } ) => { + // Do not use `visitSiteEditor` because it waits for the iframe to load. + await admin.visitAdminPage( 'site-editor.php' ); + + const requests = []; + let isLoaded = false; + + page.on( 'request', ( request ) => { + if ( request.resourceType() === 'document' ) { + // The iframe also "requests" a blob document. This is the most + // reliable way to wait for the iframe to start loading. + // `waitForSelector` is always a bit delayed, and we don't want + // to detect requests after the iframe mounts. + isLoaded = true; + } else if ( ! isLoaded && request.resourceType() === 'fetch' ) { + requests.push( request.url() ); + } + } ); + + await page.waitForFunction( ( _isLoaded ) => _isLoaded, [ isLoaded ] ); + + expect( requests ).toEqual( [] ); + } ); +} ); From 6689c778e6bbb9774dd4487c7f1c29e1ffd207f5 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Mon, 2 Dec 2024 17:02:52 +0100 Subject: [PATCH 097/444] SlotFill: remove explicit rerender from portal version (#67471) * SlotFill: remove explicit rerender from portal version * Add changelog entry Co-authored-by: jsnajdr Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + .../src/slot-fill/bubbles-virtually/fill.tsx | 18 +++++++----------- .../bubbles-virtually/slot-fill-provider.tsx | 15 ++------------- .../src/slot-fill/bubbles-virtually/slot.tsx | 11 ++++------- packages/components/src/slot-fill/types.ts | 7 ++++--- 5 files changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 748525d6d9c15..94dfd1b3c3811 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -22,6 +22,7 @@ - Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). - Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)). - Exported the `WPCompleter` type as it was being used in block-editor/autocompleters ([#67410](https://github.com/WordPress/gutenberg/pull/67410)). +- `SlotFill`: remove manual rerenders from the portal `Fill` component ([#67471](https://github.com/WordPress/gutenberg/pull/67471)). ### Bug Fixes diff --git a/packages/components/src/slot-fill/bubbles-virtually/fill.tsx b/packages/components/src/slot-fill/bubbles-virtually/fill.tsx index d5287adfab417..ef7bc94ff540b 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/fill.tsx +++ b/packages/components/src/slot-fill/bubbles-virtually/fill.tsx @@ -4,7 +4,6 @@ import { useObservableValue } from '@wordpress/compose'; import { useContext, - useReducer, useRef, useEffect, createPortal, @@ -20,18 +19,15 @@ import type { FillComponentProps } from '../types'; export default function Fill( { name, children }: FillComponentProps ) { const registry = useContext( SlotFillContext ); const slot = useObservableValue( registry.slots, name ); - const [ , rerender ] = useReducer( () => [], [] ); - const ref = useRef( { rerender } ); + const instanceRef = useRef( {} ); + // We register fills so we can keep track of their existence. + // Slots can use the `useSlotFills` hook to know if there're already fills + // registered so they can choose to render themselves or not. useEffect( () => { - // We register fills so we can keep track of their existence. - // Some Slot implementations need to know if there're already fills - // registered so they can choose to render themselves or not. - const refValue = ref.current; - registry.registerFill( name, refValue ); - return () => { - registry.unregisterFill( name, refValue ); - }; + const instance = instanceRef.current; + registry.registerFill( name, instance ); + return () => registry.unregisterFill( name, instance ); }, [ registry, name ] ); if ( ! slot || ! slot.ref.current ) { diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx index 1dc5ef35ceccf..cf692700eef79 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx @@ -23,13 +23,7 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext { ref, fillProps ) => { - const slot = slots.get( name ); - - slots.set( name, { - ...slot, - ref: ref || slot?.ref, - fillProps: fillProps || slot?.fillProps || {}, - } ); + slots.set( name, { ref, fillProps } ); }; const unregisterSlot: SlotFillBubblesVirtuallyContext[ 'unregisterSlot' ] = @@ -66,12 +60,7 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext { return; } - slot.fillProps = fillProps; - const slotFills = fills.get( name ); - if ( slotFills ) { - // Force update fills. - slotFills.forEach( ( fill ) => fill.rerender() ); - } + slots.set( name, { ref, fillProps } ); }; const registerFill: SlotFillBubblesVirtuallyContext[ 'registerFill' ] = ( diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot.tsx b/packages/components/src/slot-fill/bubbles-virtually/slot.tsx index b8ead7fc7ea8b..e65c055c410a6 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot.tsx +++ b/packages/components/src/slot-fill/bubbles-virtually/slot.tsx @@ -39,8 +39,7 @@ function Slot( ...restProps } = props; - const { registerSlot, unregisterSlot, ...registry } = - useContext( SlotFillContext ); + const registry = useContext( SlotFillContext ); const ref = useRef< HTMLElement >( null ); @@ -54,11 +53,9 @@ function Slot( }, [ fillProps ] ); useLayoutEffect( () => { - registerSlot( name, ref, fillPropsRef.current ); - return () => { - unregisterSlot( name, ref ); - }; - }, [ registerSlot, unregisterSlot, name ] ); + registry.registerSlot( name, ref, fillPropsRef.current ); + return () => registry.unregisterSlot( name, ref ); + }, [ registry, name ] ); useLayoutEffect( () => { registry.updateSlot( name, ref, fillPropsRef.current ); diff --git a/packages/components/src/slot-fill/types.ts b/packages/components/src/slot-fill/types.ts index 15f082cf3f755..6668057323edd 100644 --- a/packages/components/src/slot-fill/types.ts +++ b/packages/components/src/slot-fill/types.ts @@ -110,15 +110,16 @@ export type SlotFillProviderProps = { export type SlotRef = RefObject< HTMLElement >; export type Rerenderable = { rerender: () => void }; +export type FillInstance = {}; export type SlotFillBubblesVirtuallyContext = { slots: ObservableMap< SlotKey, { ref: SlotRef; fillProps: FillProps } >; - fills: ObservableMap< SlotKey, Rerenderable[] >; + fills: ObservableMap< SlotKey, FillInstance[] >; registerSlot: ( name: SlotKey, ref: SlotRef, fillProps: FillProps ) => void; unregisterSlot: ( name: SlotKey, ref: SlotRef ) => void; updateSlot: ( name: SlotKey, ref: SlotRef, fillProps: FillProps ) => void; - registerFill: ( name: SlotKey, ref: Rerenderable ) => void; - unregisterFill: ( name: SlotKey, ref: Rerenderable ) => void; + registerFill: ( name: SlotKey, instance: FillInstance ) => void; + unregisterFill: ( name: SlotKey, instance: FillInstance ) => void; /** * This helps the provider know if it's using the default context value or not. From fa10d2fbd3faea76835374d6c850864d103a98c9 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 2 Dec 2024 17:07:55 +0100 Subject: [PATCH 098/444] Fix EntitiesSavedStates panel dialog props. (#67351) * Fix EntitiesSavedStates panel dialog props. * Remove renderDialog default value. Co-authored-by: afercia Co-authored-by: ntsekouras Co-authored-by: Mayank-Tripathi32 Co-authored-by: jameskoster --- .../edit-site/src/components/save-panel/index.js | 12 +++++++++--- packages/editor/README.md | 2 +- .../src/components/entities-saved-states/index.js | 15 ++++++--------- .../src/components/post-publish-button/index.js | 2 ++ .../src/components/save-publish-panels/index.js | 6 +++++- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/edit-site/src/components/save-panel/index.js b/packages/edit-site/src/components/save-panel/index.js index 81a0f99557df0..95ec9b9ffc8c4 100644 --- a/packages/edit-site/src/components/save-panel/index.js +++ b/packages/edit-site/src/components/save-panel/index.js @@ -31,7 +31,7 @@ const { EntitiesSavedStatesExtensible, NavigableRegion } = unlock( privateApis ); const { useLocation } = unlock( routerPrivateApis ); -const EntitiesSavedStatesForPreview = ( { onClose } ) => { +const EntitiesSavedStatesForPreview = ( { onClose, renderDialog } ) => { const isDirtyProps = useEntitiesSavedStatesIsDirty(); let activateSaveLabel; if ( isDirtyProps.isDirty ) { @@ -75,14 +75,20 @@ const EntitiesSavedStatesForPreview = ( { onClose } ) => { onSave, saveEnabled: true, saveLabel: activateSaveLabel, + renderDialog, } } /> ); }; -const _EntitiesSavedStates = ( { onClose, renderDialog = undefined } ) => { +const _EntitiesSavedStates = ( { onClose, renderDialog } ) => { if ( isPreviewingTheme() ) { - return ; + return ( + + ); } return ( diff --git a/packages/editor/README.md b/packages/editor/README.md index 8b48d773acb26..dd7b53f421a1d 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -401,7 +401,7 @@ _Parameters_ - _props_ `Object`: The component props. - _props.close_ `Function`: The function to close the dialog. -- _props.renderDialog_ `Function`: The function to render the dialog. +- _props.renderDialog_ `boolean`: Whether to render the component with modal dialog behavior. _Returns_ diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index ea05bca522941..ad584b0df7557 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -31,14 +31,11 @@ function identity( values ) { * * @param {Object} props The component props. * @param {Function} props.close The function to close the dialog. - * @param {Function} props.renderDialog The function to render the dialog. + * @param {boolean} props.renderDialog Whether to render the component with modal dialog behavior. * * @return {React.ReactNode} The rendered component. */ -export default function EntitiesSavedStates( { - close, - renderDialog = undefined, -} ) { +export default function EntitiesSavedStates( { close, renderDialog } ) { const isDirtyProps = useIsDirty(); return ( @@ -102,7 +103,10 @@ export default function SavePublishPanels( { return ( <> { isEntitiesSavedStatesOpen && ( - + ) } { ! isEntitiesSavedStatesOpen && unmountableContent } From 656110814c85d346669dd4fa2c9d3de670d35cbb Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 2 Dec 2024 16:39:38 +0000 Subject: [PATCH 099/444] Correctly apply current-menu-ancestor class to
  • in Nav block #67169 Co-authored-by: getdave Co-authored-by: MaggieCabrera Co-authored-by: carolinan Co-authored-by: mrwweb Co-authored-by: juanfra Co-authored-by: draganescu Co-authored-by: bph Co-authored-by: jordesign Co-authored-by: webmandesign --- packages/block-library/src/navigation-submenu/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation-submenu/index.php b/packages/block-library/src/navigation-submenu/index.php index 92b55e291606e..d61dbb2426c24 100644 --- a/packages/block-library/src/navigation-submenu/index.php +++ b/packages/block-library/src/navigation-submenu/index.php @@ -222,7 +222,7 @@ function render_block_core_navigation_submenu( $attributes, $content, $block ) { if ( strpos( $inner_blocks_html, 'current-menu-item' ) ) { $tag_processor = new WP_HTML_Tag_Processor( $html ); - while ( $tag_processor->next_tag( array( 'class_name' => 'wp-block-navigation-item__content' ) ) ) { + while ( $tag_processor->next_tag( array( 'class_name' => 'wp-block-navigation-item' ) ) ) { $tag_processor->add_class( 'current-menu-ancestor' ); } $html = $tag_processor->get_updated_html(); From 1d06b35940ef1d255b08f51cf242ed776d0077b0 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:55:55 +0100 Subject: [PATCH 100/444] Site Editor: Pages: Preload template lookup (#66654) Co-authored-by: ellatrix Co-authored-by: Mamaduka Co-authored-by: jorgefilipecosta --- backport-changelog/6.8/7695.md | 2 ++ lib/compat/wordpress-6.8/preload.php | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/backport-changelog/6.8/7695.md b/backport-changelog/6.8/7695.md index f45b2039e3027..d69a59f2d67d1 100644 --- a/backport-changelog/6.8/7695.md +++ b/backport-changelog/6.8/7695.md @@ -2,3 +2,5 @@ https://github.com/WordPress/wordpress-develop/pull/7695 * https://github.com/WordPress/gutenberg/pull/66631 * https://github.com/WordPress/gutenberg/pull/67465 +* https://github.com/WordPress/gutenberg/pull/66579 +* https://github.com/WordPress/gutenberg/pull/66654 diff --git a/lib/compat/wordpress-6.8/preload.php b/lib/compat/wordpress-6.8/preload.php index 494e3ad32ec02..879e263f5a189 100644 --- a/lib/compat/wordpress-6.8/preload.php +++ b/lib/compat/wordpress-6.8/preload.php @@ -10,18 +10,26 @@ */ function gutenberg_block_editor_preload_paths_6_8( $paths, $context ) { if ( 'core/edit-site' === $context->name ) { - $post_id = null; + $post = null; if ( isset( $_GET['postId'] ) && is_numeric( $_GET['postId'] ) ) { - $post_id = (int) $_GET['postId']; + $post = get_post( (int) $_GET['postId'] ); } if ( isset( $_GET['p'] ) && preg_match( '/^\/page\/(\d+)$/', $_GET['p'], $matches ) ) { - $post_id = (int) $matches[1]; + $post = get_post( (int) $matches[1] ); } - if ( $post_id ) { - $route_for_post = rest_get_route_for_post( $post_id ); + if ( $post ) { + $route_for_post = rest_get_route_for_post( $post ); if ( $route_for_post ) { $paths[] = add_query_arg( 'context', 'edit', $route_for_post ); + if ( 'page' === $post->post_type ) { + $paths[] = add_query_arg( + 'slug', + // @see https://github.com/WordPress/gutenberg/blob/489f6067c623926bce7151a76755bb68d8e22ea7/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js#L139-L140 + 'page-' . $post->post_name, + '/wp/v2/templates/lookup' + ); + } } } From d8a457b56b6671fa25dedf2b40841a13bdabe6f1 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:02:59 +0200 Subject: [PATCH 101/444] Block Editor: Animate useScaleCanvas() only when toggling zoomed out mode (#67481) Co-authored-by: tyxla Co-authored-by: ellatrix --- .../src/components/iframe/use-scale-canvas.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/iframe/use-scale-canvas.js b/packages/block-editor/src/components/iframe/use-scale-canvas.js index c72266e82e2b0..2d8cb217a3255 100644 --- a/packages/block-editor/src/components/iframe/use-scale-canvas.js +++ b/packages/block-editor/src/components/iframe/use-scale-canvas.js @@ -2,7 +2,11 @@ * WordPress dependencies */ import { useEffect, useRef, useCallback } from '@wordpress/element'; -import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; +import { + usePrevious, + useReducedMotion, + useResizeObserver, +} from '@wordpress/compose'; /** * @typedef {Object} TransitionState @@ -280,6 +284,7 @@ export function useScaleCanvas( { transitionFromRef.current = transitionToRef.current; }, [ iframeDocument ] ); + const previousIsZoomedOut = usePrevious( isZoomedOut ); /** * Runs when zoom out mode is toggled, and sets the startAnimation flag * so the animation will start when the next useEffect runs. We _only_ @@ -287,7 +292,7 @@ export function useScaleCanvas( { * changes due to the container resizing. */ useEffect( () => { - if ( ! iframeDocument ) { + if ( ! iframeDocument || previousIsZoomedOut === isZoomedOut ) { return; } @@ -300,7 +305,7 @@ export function useScaleCanvas( { return () => { iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); }; - }, [ iframeDocument, isZoomedOut ] ); + }, [ iframeDocument, isZoomedOut, previousIsZoomedOut ] ); /** * This handles: From 232d14f33f57fcf6f0c07e52bdb5c52ba8f3dcae Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 3 Dec 2024 02:32:19 +0900 Subject: [PATCH 102/444] DropdownMenu: Increase option height to 40px (#67435) * DropdownMenu: Increase option height to 40px * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 3 +++ packages/components/src/dropdown-menu/index.tsx | 1 + packages/components/src/dropdown-menu/style.scss | 2 +- packages/components/src/menu-items-choice/style.scss | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 94dfd1b3c3811..b482a4801c2ea 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -12,6 +12,9 @@ ### Enhancements - `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). +- `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `MenuItem`: Increase height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `MenuItemsChoice`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). ### Experimental diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index 0e4501be4839c..de83695978c2d 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -164,6 +164,7 @@ function UnconnectedDropdownMenu( dropdownMenuProps: DropdownMenuProps ) { { controlSets?.flatMap( ( controlSet, indexOfSet ) => controlSet.map( ( control, indexOfControl ) => ( ' @@ -15,8 +22,10 @@ function DeletedNavigationWarning( { onCreateNew, isNotice = false } ) { button: ( + ) } + renderContent={ ( { onToggle } ) => ( + + { + setShowModal( true ); + onToggle(); + } } + > + { __( 'Swap template' ) } + + { + // The default template in a post is indicated by an empty string + value !== '' && ( + { + onChangeControl( '' ); + onToggle(); + } } + > + { __( 'Use default template' ) } + + ) + } + + ) } + /> + { showModal && ( + setShowModal( false ) } + overlayClassName="fields-controls__template-modal" + isFullScreen + > +
    + { + onChangeControl( template.name ); + setShowModal( false ); + } } + /> +
    +
    + ) } + + ); +}; diff --git a/packages/fields/src/style.scss b/packages/fields/src/style.scss index 05cf565224877..582044235aef1 100644 --- a/packages/fields/src/style.scss +++ b/packages/fields/src/style.scss @@ -1,3 +1,4 @@ @import "./components/create-template-part-modal/style.scss"; @import "./fields/slug/style.scss"; @import "./fields/featured-image/style.scss"; +@import "./fields/template/style.scss"; diff --git a/packages/fields/tsconfig.json b/packages/fields/tsconfig.json index 531afb5bb2d87..46ac86d48e11e 100644 --- a/packages/fields/tsconfig.json +++ b/packages/fields/tsconfig.json @@ -27,6 +27,7 @@ { "path": "../private-apis" }, { "path": "../router" }, { "path": "../url" }, + { "path": "../block-editor" }, { "path": "../warning" } ], "include": [ "src" ] From 4d225cc2ba6f09822227e7a820b8a555be7c4d48 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:22:28 +0100 Subject: [PATCH 121/444] [mini] Preload: add post type (#67518) Co-authored-by: ellatrix Co-authored-by: Mamaduka --- backport-changelog/6.8/7695.md | 1 + lib/compat/wordpress-6.8/preload.php | 1 + 2 files changed, 2 insertions(+) diff --git a/backport-changelog/6.8/7695.md b/backport-changelog/6.8/7695.md index d69a59f2d67d1..08b780e2afb0d 100644 --- a/backport-changelog/6.8/7695.md +++ b/backport-changelog/6.8/7695.md @@ -4,3 +4,4 @@ https://github.com/WordPress/wordpress-develop/pull/7695 * https://github.com/WordPress/gutenberg/pull/67465 * https://github.com/WordPress/gutenberg/pull/66579 * https://github.com/WordPress/gutenberg/pull/66654 +* https://github.com/WordPress/gutenberg/pull/67518 diff --git a/lib/compat/wordpress-6.8/preload.php b/lib/compat/wordpress-6.8/preload.php index 0a36ea7f7227d..0e887fc081bcb 100644 --- a/lib/compat/wordpress-6.8/preload.php +++ b/lib/compat/wordpress-6.8/preload.php @@ -22,6 +22,7 @@ function gutenberg_block_editor_preload_paths_6_8( $paths, $context ) { $route_for_post = rest_get_route_for_post( $post ); if ( $route_for_post ) { $paths[] = add_query_arg( 'context', 'edit', $route_for_post ); + $paths[] = add_query_arg( 'context', 'edit', '/wp/v2/types/' . $post->post_type ); if ( 'page' === $post->post_type ) { $paths[] = add_query_arg( 'slug', From 0ff919f2c6a94e657987a8440c75b5e4f2db0b13 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 3 Dec 2024 12:49:06 +0000 Subject: [PATCH 122/444] Docs: Remove invalid key projects links on the documentation. (#67491) Co-authored-by: jorgefilipecosta --- docs/contributors/repository-management.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/contributors/repository-management.md b/docs/contributors/repository-management.md index e57f762a60539..5bb971bfaf2ef 100644 --- a/docs/contributors/repository-management.md +++ b/docs/contributors/repository-management.md @@ -165,9 +165,3 @@ If you meet this criterion of several meaningful contributions having been accep ## Projects We use [GitHub projects](https://github.com/WordPress/gutenberg/projects) to keep track of details that aren't immediately actionable, but that we want to keep around for future reference. - -Some key projects include: - -- [Phase 2](https://github.com/WordPress/gutenberg/projects/13) - Development tasks needed for Phase 2 of Gutenberg. -- [Phase 2 design](https://github.com/WordPress/gutenberg/projects/21) - Tasks for design in Phase 2. Note: specific projects may have their own boards. -- [Ideas](https://github.com/WordPress/gutenberg/projects/8) - Project containing tickets that, while closed for the time being, can be revisited in the future. From 1c3cea43b0fca853f351e4bc08ba840df7de2469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Gomes?= Date: Tue, 3 Dec 2024 14:24:57 +0000 Subject: [PATCH 123/444] Exclude Set instance methods from polyfills (#67230) * Exclude Set instance methods from polyfills * Switch to regexp exclusions --- .../polyfill-exclusions.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/babel-preset-default/polyfill-exclusions.js b/packages/babel-preset-default/polyfill-exclusions.js index 507396c930b99..ca8c045d12414 100644 --- a/packages/babel-preset-default/polyfill-exclusions.js +++ b/packages/babel-preset-default/polyfill-exclusions.js @@ -7,4 +7,25 @@ module.exports = [ // This is an IE-only feature which we don't use, and don't want to polyfill. // @see https://github.com/WordPress/gutenberg/pull/49234 'web.immediate', + // Remove Set feature polyfills. + // + // The Babel/core-js integration has a severe limitation, in that any Set + // objects (e.g. `new Set()`) are assumed to need all instance methods, and + // get them all polyfilled. There is no validation as to whether those + // methods are actually in use. + // + // This limitation causes a number of packages to unnecessarily get a + // dependency on `wp-polyfill`, which in most cases gets loaded as part of + // the critical path and can thus have an impact on performance. + // + // There is no good solution to this, and the one we've opted for here is + // to disable polyfilling these features entirely. Developers will need to + // take care not to use them in scenarios where the code may be running in + // older browsers without native support for them. + // + // These need to be specified as both `es.` and `esnext.` due to the way + // internal dependencies are set up in Babel / core-js. + // + // @see https://github.com/WordPress/gutenberg/pull/67230 + /^es(next)?\.set\./, ]; From 7631986644c82b2c8ff7481e95a64124644f7c1d Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Tue, 3 Dec 2024 08:24:33 -0800 Subject: [PATCH 124/444] Split view with meta boxes even with legacy canvas (#66706) * Split view with meta boxes with non-iframed canvas * Fix scrolling of device previews * Consolidate styles and add comments * Do the same thing without adding a prop to BlockCanvas * Fix horizontal overflow of device previews Co-authored-by: stokesman Co-authored-by: t-hamano Co-authored-by: cbravobernal Co-authored-by: jartes Co-authored-by: bph Co-authored-by: ndiego Co-authored-by: MadtownLems --- .../src/components/block-canvas/index.js | 8 ++--- .../src/components/use-resize-canvas/index.js | 2 +- .../edit-post/src/components/layout/index.js | 23 +++--------- .../src/components/layout/style.scss | 8 ++--- .../components/editor-interface/style.scss | 3 +- .../src/components/visual-editor/index.js | 1 + .../src/components/visual-editor/style.scss | 35 +++++++++++++++---- 7 files changed, 44 insertions(+), 36 deletions(-) diff --git a/packages/block-editor/src/components/block-canvas/index.js b/packages/block-editor/src/components/block-canvas/index.js index c399f38054ed4..36aca7fa1c722 100644 --- a/packages/block-editor/src/components/block-canvas/index.js +++ b/packages/block-editor/src/components/block-canvas/index.js @@ -56,7 +56,8 @@ export function ExperimentalBlockCanvas( { return ( { children } @@ -81,6 +78,7 @@ export function ExperimentalBlockCanvas( { return ( +
  • ) } ); diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 4674b5e5fc3ca..11dbae7bc66b1 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -6,7 +6,7 @@ cursor: pointer; } -.edit-site-global-styles-preview__iframe { +.edit-site-global-styles-preview__wrapper { max-width: 100%; display: block; width: 100%; @@ -230,10 +230,6 @@ // The &#{&} is a workaround for the specificity of the Card component. &#{&}, &#{&} .edit-site-global-styles-screen-root__active-style-tile-preview { - box-shadow: none; border-radius: $radius-small; - .block-editor-iframe__container { - border: 1px solid $gray-200; - } } } diff --git a/packages/edit-site/src/components/global-styles/typography-example.js b/packages/edit-site/src/components/global-styles/typography-example.js index a491ca57bf5be..9c0a4e0e1cb13 100644 --- a/packages/edit-site/src/components/global-styles/typography-example.js +++ b/packages/edit-site/src/components/global-styles/typography-example.js @@ -14,7 +14,9 @@ import { unlock } from '../../lock-unlock'; import { getFamilyPreviewStyle } from './font-library-modal/utils/preview-styles'; import { getFontFamilies } from './utils'; -const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); +const { useGlobalStyle, GlobalStylesContext } = unlock( + blockEditorPrivateApis +); const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); export default function PreviewTypography( { fontSize, variation } ) { @@ -23,6 +25,9 @@ export default function PreviewTypography( { fontSize, variation } ) { if ( variation ) { config = mergeBaseAndUserConfigs( base, variation ); } + + const [ textColor ] = useGlobalStyle( 'color.text' ); + const [ bodyFontFamilies, headingFontFamilies ] = getFontFamilies( config ); const bodyPreviewStyle = bodyFontFamilies ? getFamilyPreviewStyle( bodyFontFamilies ) @@ -31,6 +36,11 @@ export default function PreviewTypography( { fontSize, variation } ) { ? getFamilyPreviewStyle( headingFontFamilies ) : {}; + if ( textColor ) { + bodyPreviewStyle.color = textColor; + headingPreviewStyle.color = textColor; + } + if ( fontSize ) { bodyPreviewStyle.fontSize = fontSize; headingPreviewStyle.fontSize = fontSize; @@ -52,6 +62,7 @@ export default function PreviewTypography( { fontSize, variation } ) { } } style={ { textAlign: 'center', + lineHeight: 1, } } > diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/content.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/content.js index 986238697f734..ce8cd32aa009c 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/content.js @@ -2,54 +2,26 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; import { __experimentalVStack as VStack } from '@wordpress/components'; -import { BlockEditorProvider } from '@wordpress/block-editor'; /** * Internal dependencies */ import StyleVariationsContainer from '../global-styles/style-variations-container'; -import { unlock } from '../../lock-unlock'; -import { store as editSiteStore } from '../../store'; import ColorVariations from '../global-styles/variations/variations-color'; import TypographyVariations from '../global-styles/variations/variations-typography'; -const noop = () => {}; - export default function SidebarNavigationScreenGlobalStylesContent() { - const { storedSettings } = useSelect( ( select ) => { - const { getSettings } = unlock( select( editSiteStore ) ); - - return { - storedSettings: getSettings(), - }; - }, [] ); - const gap = 3; - // Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are - // loaded. This is necessary because the Iframe component waits until - // the block editor store's `__internalIsInitialized` is true before - // rendering the iframe. Without this, the iframe previews will not render - // in mobile viewport sizes, where the editor canvas is hidden. return ( - - - - - - - + + + + ); } From 959bb6b006a8e3deecff9aad4e07658878029c26 Mon Sep 17 00:00:00 2001 From: Sarthak Nagoshe <83178197+sarthaknagoshe2002@users.noreply.github.com> Date: Sat, 7 Dec 2024 09:23:09 +0530 Subject: [PATCH 197/444] Simplify description and option names in the Lock modal dialog (#67437) * Refactor Lock modal dialog for improved clarity and simplicity * Fix: modify the e2e test cases * Fix: shorten the text & make it feel more specific Co-authored-by: sarthaknagoshe2002 Co-authored-by: t-hamano Co-authored-by: afercia Co-authored-by: mtias --- .../block-editor/src/components/block-lock/modal.js | 10 ++++------ test/e2e/specs/editor/blocks/columns.spec.js | 2 +- test/e2e/specs/editor/various/block-locking.spec.js | 4 ++-- test/e2e/specs/editor/various/block-switcher.spec.js | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/block-lock/modal.js b/packages/block-editor/src/components/block-lock/modal.js index 3be23f6adde14..df267e97165e3 100644 --- a/packages/block-editor/src/components/block-lock/modal.js +++ b/packages/block-editor/src/components/block-lock/modal.js @@ -99,9 +99,7 @@ export default function BlockLockModal( { clientId, onClose } ) { >
    - { __( - 'Choose specific attributes to restrict or lock all available options.' - ) } + { __( 'Select the features you want to lock' ) } { /* * Disable reason: The `list` ARIA role is redundant but @@ -137,7 +135,7 @@ export default function BlockLockModal( { clientId, onClose } ) {
  • setLock( ( prevLock ) => ( { @@ -159,7 +157,7 @@ export default function BlockLockModal( { clientId, onClose } ) {
  • setLock( ( prevLock ) => ( { @@ -178,7 +176,7 @@ export default function BlockLockModal( { clientId, onClose } ) {
  • setLock( ( prevLock ) => ( { diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js index eea6e321aacb1..29262aef810d2 100644 --- a/test/e2e/specs/editor/blocks/columns.spec.js +++ b/test/e2e/specs/editor/blocks/columns.spec.js @@ -63,7 +63,7 @@ test.describe( 'Columns', () => { ); await editor.clickBlockToolbarButton( 'Options' ); await page.click( 'role=menuitem[name="Lock"i]' ); - await page.locator( 'role=checkbox[name="Prevent removal"i]' ).check(); + await page.locator( 'role=checkbox[name="Lock removal"i]' ).check(); await page.click( 'role=button[name="Apply"i]' ); // Select columns block diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js index eafb468902ef9..a8895d282fb95 100644 --- a/test/e2e/specs/editor/various/block-locking.spec.js +++ b/test/e2e/specs/editor/various/block-locking.spec.js @@ -16,7 +16,7 @@ test.describe( 'Block Locking', () => { await editor.clickBlockOptionsMenuItem( 'Lock' ); - await page.click( 'role=checkbox[name="Prevent removal"]' ); + await page.click( 'role=checkbox[name="Lock removal"]' ); await page.click( 'role=button[name="Apply"]' ); await expect( @@ -35,7 +35,7 @@ test.describe( 'Block Locking', () => { await editor.clickBlockOptionsMenuItem( 'Lock' ); - await page.click( 'role=checkbox[name="Disable movement"]' ); + await page.click( 'role=checkbox[name="Lock movement"]' ); await page.click( 'role=button[name="Apply"]' ); // Hide options. diff --git a/test/e2e/specs/editor/various/block-switcher.spec.js b/test/e2e/specs/editor/various/block-switcher.spec.js index 41b0b1ee49c88..cb95c4d395c66 100644 --- a/test/e2e/specs/editor/various/block-switcher.spec.js +++ b/test/e2e/specs/editor/various/block-switcher.spec.js @@ -107,7 +107,7 @@ test.describe( 'Block Switcher', () => { await expect( button ).toBeEnabled(); await editor.clickBlockOptionsMenuItem( 'Lock' ); - await page.click( 'role=checkbox[name="Prevent removal"]' ); + await page.click( 'role=checkbox[name="Lock removal"]' ); await page.click( 'role=button[name="Apply"]' ); // Verify the block switcher isn't enabled. From 72417c3b89b6c51fae4b1bf550833738f89aefaf Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:42:40 +0900 Subject: [PATCH 198/444] Style book: Fix critical error when blocks are not registered (#67703) Co-authored-by: t-hamano Co-authored-by: ramonjd --- .../src/components/style-book/examples.tsx | 75 ++++++++++++------- .../src/components/style-book/types.ts | 2 +- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/edit-site/src/components/style-book/examples.tsx b/packages/edit-site/src/components/style-book/examples.tsx index c944b87b09d7e..cb7b6afcb422c 100644 --- a/packages/edit-site/src/components/style-book/examples.tsx +++ b/packages/edit-site/src/components/style-book/examples.tsx @@ -12,7 +12,12 @@ import { /** * Internal dependencies */ -import type { BlockExample, ColorOrigin, MultiOriginPalettes } from './types'; +import type { + Block, + BlockExample, + ColorOrigin, + MultiOriginPalettes, +} from './types'; import ColorExamples from './color-examples'; import DuotoneExamples from './duotone-examples'; import { STYLE_BOOK_COLOR_GROUPS } from './constants'; @@ -91,30 +96,33 @@ function getOverviewBlockExamples( examples.push( themeColorexample ); } - const headingBlock = createBlock( 'core/heading', { - content: __( - `AaBbCcDdEeFfGgHhiiJjKkLIMmNnOoPpQakRrssTtUuVVWwXxxYyZzOl23356789X{(…)},2!*&:/A@HELFO™` - ), - level: 1, - } ); - const firstParagraphBlock = createBlock( 'core/paragraph', { - content: __( - `A paragraph in a website refers to a distinct block of text that is used to present and organize information. It is a fundamental unit of content in web design and is typically composed of a group of related sentences or thoughts focused on a particular topic or idea. Paragraphs play a crucial role in improving the readability and user experience of a website. They break down the text into smaller, manageable chunks, allowing readers to scan the content more easily.` - ), - } ); - const secondParagraphBlock = createBlock( 'core/paragraph', { - content: __( - `Additionally, paragraphs help structure the flow of information and provide logical breaks between different concepts or pieces of information. In terms of formatting, paragraphs in websites are commonly denoted by a vertical gap or indentation between each block of text. This visual separation helps visually distinguish one paragraph from another, creating a clear and organized layout that guides the reader through the content smoothly.` - ), - } ); + // Get examples for typography blocks. + const typographyBlockExamples: Block[] = []; + + if ( getBlockType( 'core/heading' ) ) { + const headingBlock = createBlock( 'core/heading', { + content: __( + `AaBbCcDdEeFfGgHhiiJjKkLIMmNnOoPpQakRrssTtUuVVWwXxxYyZzOl23356789X{(…)},2!*&:/A@HELFO™` + ), + level: 1, + } ); + typographyBlockExamples.push( headingBlock ); + } + + if ( getBlockType( 'core/paragraph' ) ) { + const firstParagraphBlock = createBlock( 'core/paragraph', { + content: __( + `A paragraph in a website refers to a distinct block of text that is used to present and organize information. It is a fundamental unit of content in web design and is typically composed of a group of related sentences or thoughts focused on a particular topic or idea. Paragraphs play a crucial role in improving the readability and user experience of a website. They break down the text into smaller, manageable chunks, allowing readers to scan the content more easily.` + ), + } ); + const secondParagraphBlock = createBlock( 'core/paragraph', { + content: __( + `Additionally, paragraphs help structure the flow of information and provide logical breaks between different concepts or pieces of information. In terms of formatting, paragraphs in websites are commonly denoted by a vertical gap or indentation between each block of text. This visual separation helps visually distinguish one paragraph from another, creating a clear and organized layout that guides the reader through the content smoothly.` + ), + } ); - const textExample = { - name: 'typography', - title: __( 'Typography' ), - category: 'overview', - blocks: [ - headingBlock, - createBlock( + if ( getBlockType( 'core/group' ) ) { + const groupBlock = createBlock( 'core/group', { layout: { @@ -129,10 +137,21 @@ function getOverviewBlockExamples( }, }, [ firstParagraphBlock, secondParagraphBlock ] - ), - ], - }; - examples.push( textExample ); + ); + typographyBlockExamples.push( groupBlock ); + } else { + typographyBlockExamples.push( firstParagraphBlock ); + } + } + + if ( !! typographyBlockExamples.length ) { + examples.push( { + name: 'typography', + title: __( 'Typography' ), + category: 'overview', + blocks: typographyBlockExamples, + } ); + } const otherBlockExamples = [ 'core/image', diff --git a/packages/edit-site/src/components/style-book/types.ts b/packages/edit-site/src/components/style-book/types.ts index e7be17b17dd4d..9f65039121856 100644 --- a/packages/edit-site/src/components/style-book/types.ts +++ b/packages/edit-site/src/components/style-book/types.ts @@ -1,4 +1,4 @@ -type Block = { +export type Block = { name: string; attributes: Record< string, unknown >; innerBlocks?: Block[]; From df98e3731b300c01a93d0900a59ffb1c60a3a0f3 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Sat, 7 Dec 2024 15:48:51 +0100 Subject: [PATCH 199/444] CreateTemplatePartModal: replace ts-ignore with ts-expect-error (#67709) Co-authored-by: gigitux Co-authored-by: ciampo --- .../src/components/create-template-part-modal/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/fields/src/components/create-template-part-modal/index.tsx b/packages/fields/src/components/create-template-part-modal/index.tsx index 03b39a8fdcdf3..c7dc54df84a2a 100644 --- a/packages/fields/src/components/create-template-part-modal/index.tsx +++ b/packages/fields/src/components/create-template-part-modal/index.tsx @@ -28,7 +28,7 @@ import { symbolFilled as symbolFilledIcon, } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; -// @ts-ignore +// @ts-expect-error serialize is not typed import { serialize } from '@wordpress/blocks'; /** @@ -64,7 +64,7 @@ export default function CreateTemplatePartModal( { } & CreateTemplatePartModalContentsProps ) { const defaultModalTitle = useSelect( ( select ) => - // @ts-ignore + // @ts-expect-error getPostType is not typed with 'wp_template_part' as argument. select( coreStore ).getPostType( 'wp_template_part' )?.labels ?.add_new_item, [] @@ -77,7 +77,6 @@ export default function CreateTemplatePartModal( { focusOnMount="firstContentElement" size="medium" > - { /* @ts-ignore */ } ); From 7cf1ceddbef6ea3802014c43ea16d193febcb6d1 Mon Sep 17 00:00:00 2001 From: Eshaan Dabasiya <76681468+im3dabasia@users.noreply.github.com> Date: Sun, 8 Dec 2024 07:44:53 +0530 Subject: [PATCH 200/444] Remove .components-item-group selector in edit-site components[2] (#67575) * fix: Removed .components-item-group selector class usage in scss file in edit-site components Applied horizontal margins to the following screens 1. Navigation 2. Pages 3. Templates 4. Patterns * fix: Added sidebar-navigation-screen-main/style.scss file correctly * refactor: CSS code for sidebar in editor Co-authored-by: im3dabasia Co-authored-by: t-hamano Co-authored-by: afercia --- packages/edit-site/src/components/sidebar-dataviews/index.js | 2 +- .../edit-site/src/components/sidebar-dataviews/style.scss | 5 +++++ .../src/components/sidebar-navigation-screen-main/index.js | 2 +- .../src/components/sidebar-navigation-screen-main/style.scss | 4 ++++ .../sidebar-navigation-screen-navigation-menus/index.js | 2 +- .../sidebar-navigation-screen-navigation-menus/style.scss | 5 +++++ .../components/sidebar-navigation-screen-patterns/style.scss | 2 ++ .../sidebar-navigation-screen-templates-browse/content.js | 2 +- .../sidebar-navigation-screen-templates-browse/style.scss | 4 ++++ .../src/components/sidebar-navigation-screen/style.scss | 5 ----- packages/edit-site/src/style.scss | 4 +++- 11 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-main/style.scss create mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/style.scss diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js index 410767650c6f3..312bf43d6df65 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -26,7 +26,7 @@ export default function DataViewsSidebarContent( { postType } ) { return ( <> - + { defaultViews.map( ( dataview ) => { return ( + - + { navigationMenus?.map( ( { id, title, status }, index ) => ( + Date: Mon, 9 Dec 2024 13:42:53 +1100 Subject: [PATCH 201/444] Update global stylesheet docblocks with `custom-css` parameter. (#67716) Co-authored-by: tellthemachines Co-authored-by: ramonjd --- backport-changelog/6.8/7976.md | 3 +++ lib/class-wp-theme-json-gutenberg.php | 2 ++ lib/global-styles-and-settings.php | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 backport-changelog/6.8/7976.md diff --git a/backport-changelog/6.8/7976.md b/backport-changelog/6.8/7976.md new file mode 100644 index 0000000000000..e2942d5e4fbe1 --- /dev/null +++ b/backport-changelog/6.8/7976.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7976 + +* https://github.com/WordPress/gutenberg/pull/67716 \ No newline at end of file diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 083ce3516b71a..778dcdbec78d9 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1319,6 +1319,8 @@ public function get_settings() { * - `variables`: only the CSS Custom Properties for presets & custom ones. * - `styles`: only the styles section in theme.json. * - `presets`: only the classes for the presets. + * - `base-layout-styles`: only the base layout styles. + * - `custom-css`: only the custom CSS. * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. * @param array $options An array of options for now used for internal purposes only (may change without notice). * The options currently supported are: diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index c4446cf29cf01..3ff5e6cb135e1 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -9,7 +9,7 @@ * Returns the stylesheet resulting of merging core, theme, and user data. * * @param array $types Types of styles to load. Optional. - * It accepts as values: 'variables', 'presets', 'styles', 'base-layout-styles. + * See {@see 'WP_Theme_JSON::get_stylesheet'} for all valid types. * If empty, it'll load the following: * - for themes without theme.json: 'variables', 'presets', 'base-layout-styles'. * - for themes with theme.json: 'variables', 'presets', 'styles'. @@ -142,6 +142,8 @@ function gutenberg_get_global_settings( $path = array(), $context = array() ) { /** * Gets the global styles custom css from theme.json. * + * @deprecated Gutenberg 18.6.0 Use {@see 'gutenberg_get_global_stylesheet'} instead for top-level custom CSS, or {@see 'WP_Theme_JSON_Gutenberg::get_styles_for_block'} for block-level custom CSS. + * * @return string */ function gutenberg_get_global_styles_custom_css() { From 57c5f849a5fc6ef6b3ba3f90e52c66fbe4ca8c5e Mon Sep 17 00:00:00 2001 From: Dan Knauss <273554+dknauss@users.noreply.github.com> Date: Mon, 9 Dec 2024 01:46:06 -0700 Subject: [PATCH 202/444] Improve DataViews README (#67711) Co-authored-by: dknauss Co-authored-by: oandregal --- packages/dataviews/README.md | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 651d87268c28e..6f74a13d8f197 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1,6 +1,6 @@ # The `@wordpress/dataviews` package -The DataViews package offers two React components and a few utilites to work with a list of data: +The DataViews package offers two React components and a few utilities to work with a list of data: - `DataViews`: to render the dataset using different types of layouts (table, grid, list) and interaction capabilities (search, filters, sorting, etc.). - `DataForm`: to edit the items of the dataset. @@ -15,7 +15,7 @@ npm install @wordpress/dataviews --save ## `DataViews` - + **Important note** If you're trying to use the `DataViews` component in a WordPress plugin or theme and you're building your scripts using the `@wordpress/scripts` package, you need to import the components from `@wordpress/dataviews/wp` instead of `@wordpress/dataviews`. @@ -68,13 +68,13 @@ const data = [ ]; ``` -The data can come from anywhere, from a static JSON file to a dynamic source like a HTTP Request. It's the consumer's responsiblity to query the data source appropiately and update the dataset based on the user's choices for sorting, filtering, etc. +The data can come from anywhere, from a static JSON file to a dynamic source like an HTTP Request. It's the consumer's responsibility to query the data source appropriately and update the dataset based on the user's choices for sorting, filtering, etc. Each record should have an `id` that identifies them uniquely. If they don't, the consumer should provide the `getItemId` property to `DataViews`: a function that returns an unique identifier for the record. #### `getItemId`: `function` -Function that receives an item and returns an unique identifier for it. +A function that receives an item and returns a unique identifier for it. It's optional. The field will get a default implementation by `DataViews` that returns the value of the `item[ id ]`. @@ -185,9 +185,9 @@ Properties: - `field`: the field used for sorting the dataset. - `direction`: the direction to use for sorting, one of `asc` or `desc`. -- `titleField`: The id of the field reprensenting the title of the record. -- `mediaField`: The id of the field reprensenting the media of the record. -- `descriptionField`: The id of field the reprensenting the description of the record. +- `titleField`: The id of the field representing the title of the record. +- `mediaField`: The id of the field representing the media of the record. +- `descriptionField`: The id of the field representing the description of the record. - `showTitle`: Whether the title should be shown in the UI. `true` by default. - `showMedia`: Whether the media should be shown in the UI. `true` by default. - `showDescription`: Whether the description should be shown in the UI. `true` by default. @@ -205,7 +205,7 @@ Properties: Callback executed when the view has changed. It receives the new view object as a parameter. -The view is a representation of the visible state of the dataset: what type of layout is used to display it (table, grid, etc.), how the dataset is filtered, how it is sorted or paginated. It's the consumer's responsibility to use the view config to query the data provider and make sure the user decisions (sort, pagination, filters, etc.) are respected. +The view is a representation of the visible state of the dataset: what type of layout is used to display it (table, grid, etc.), how the dataset is filtered, and how it is sorted or paginated. The consumer is responsible for using the view config to query the data provider and ensure the user decisions (sort, pagination, filters, etc.) are respected. The following example shows how a view object is used to query the WordPress REST API via the entities abstraction. The same can be done with any other data provider. @@ -337,7 +337,7 @@ Whether the data is loading. `false` by default. #### `defaultLayouts`: `Record< string, view >` -This property provides layout information about the view types that are active. If empty, enables all layout types (see "Layout Types") with empty layout data. +This property provides layout information about active view types. If empty, this enables all layout types (see "Layout Types") with empty layout data. For example, this is how you'd enable only the table view type: @@ -358,17 +358,17 @@ The `defaultLayouts` property should be an object that includes properties named The list of selected items' ids. -If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves as a controlled component, otherwise, it behaves like an uncontrolled component. +If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component. #### `onChangeSelection`: `function` -Callback that signals the user selected one of more items. It receives the list of selected items' ids as a parameter. +Callback that signals the user selected one of more items. It receives the list of selected items' IDs as a parameter. -If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves as a controlled component, otherwise, it behaves like an uncontrolled component. +If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component. ### `isItemClickable`: `function` -A function that determines if a media field or a primary field are clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. +A function that determines if a media field or a primary field is clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. ### `onClickItem`: `function` @@ -405,7 +405,7 @@ const Example = () => { A single item to be edited. -It can be think of as a single record coming from the `data` property of `DataViews` — though it doesn't need to be. It can be totally separated or a mix of records if your app supports bulk editing. +It can be thought of as a single record coming from the `data` property of `DataViews` — though it doesn't need to be. It can be totally separated or a mix of records if your app supports bulk editing. #### `fields`: `Object[]` @@ -500,7 +500,7 @@ Returns an object containing: ### `isItemValid` -Utility to determine whether or not the given item's value is valid according to the current fields and form config. +Utility is used to determine whether or not the given item's value is valid according to the current fields and form configuration. Parameters: @@ -687,7 +687,7 @@ Example: Field type. One of `text`, `integer`, `datetime`. -If a field declares a `type`, it gets default implementations for the `sort`, `isValid`, and `Edit` functions. They will overriden if the field provides its own. +If a field declares a `type`, it gets default implementations for the `sort`, `isValid`, and `Edit` functions if no other values are specified. - Type: `string`. - Optional. @@ -732,7 +732,7 @@ Example: ### `getValue` -React component that returns the value of a field. This value is used in sorting the fields, or when filtering. +React component that returns the value of a field. This value is used to sort or filter the fields. - Type: React component. - Optional. @@ -977,13 +977,13 @@ Example: ### `elements` -List of valid values for a field. If provided, it creates a DataViews' filter for the field. DataForm's edit control will use these values as well (see `Edit` field property). +List of valid values for a field. If provided, it creates a DataViews' filter for the field. DataForm's edit control will also use these values. (See `Edit` field property.) - Type: `array` of objects. - Optional. - Each object can have the following properties: - - `value`: required, the value to match against the field's value. - - `label`: required, the name to display to users. + - `value`: the value to match against the field's value. (Required) + - `label`: the name to display to users. (Required) - `description`: optional, a longer description of the item. Example: @@ -1006,7 +1006,7 @@ Configuration of the filters. - Type: `object`. - Optional. - Properties: - - `operators`: the list of operators supported by the field. See "operators" below. By default, a filter will support the `isAny` and `isNone` multi-selection operators. + - `operators`: the list of operators supported by the field. See "operators" below. A filter will support the `isAny` and `isNone` multi-selection operators by default. - `isPrimary`: boolean, optional. Indicates if the filter is primary. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. Operators: @@ -1020,7 +1020,7 @@ Operators: | `isAll` | Multiple items | `AND`. The item's field has all of the values in the list. | Category is all: Book, Review, Science Fiction | | `isNotAll` | Multiple items | `NOT AND`. The item's field doesn't have all of the values in the list. | Category is not all: Book, Review, Science Fiction | -`is` and `isNot` are single-selection operators, while `isAny`, `isNone`, `isAll`, and `isNotALl` are multi-selection. By default, a filter with no operators declared will support the `isAny` and `isNone` multi-selection operators. A filter cannot mix single-selection & multi-selection operators; if a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded and the filter won't allow selecting more than one item. +`is` and `isNot` are single-selection operators, while `isAny`, `isNone`, `isAll`, and `isNotALl` are multi-selection. A filter with no operators declared will support the `isAny` and `isNone` multi-selection operators by default. A filter cannot mix single-selection & multi-selection operators; if a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded, and the filter won't allow selecting more than one item. Example: From 9e0397303f23d9540e2d6aae2a7aeb30268bd3ce Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 9 Dec 2024 20:45:04 +1100 Subject: [PATCH 203/444] Data views: expand config drop down on mobile (#67715) In this view, ensure that the content is horizontally scrollable and the width takes up the screen.Co-authored-by: ramonjd Co-authored-by: ntsekouras Co-authored-by: megane9988 --- .../src/components/dataviews-view-config/index.tsx | 12 ++++++++++-- .../src/components/dataviews-view-config/style.scss | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 28e48525ffa73..3146064a41922 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -53,7 +53,11 @@ interface ViewTypeMenuProps { defaultLayouts?: SupportedLayouts; } -const DATAVIEWS_CONFIG_POPOVER_PROPS = { placement: 'bottom-end', offset: 9 }; +const DATAVIEWS_CONFIG_POPOVER_PROPS = { + className: 'dataviews-config__popover', + placement: 'bottom-end', + offset: 9, +}; function ViewTypeMenu( { defaultLayouts = { list: {}, grid: {}, table: {} }, @@ -619,6 +623,7 @@ function DataviewsViewConfigDropdown() { ); return ( ( - + diff --git a/packages/dataviews/src/components/dataviews-view-config/style.scss b/packages/dataviews/src/components/dataviews-view-config/style.scss index 8427796b6e15e..0fd97b916b4aa 100644 --- a/packages/dataviews/src/components/dataviews-view-config/style.scss +++ b/packages/dataviews/src/components/dataviews-view-config/style.scss @@ -6,6 +6,14 @@ line-height: $default-line-height; } +.dataviews-config__popover.is-expanded .dataviews-config__popover-content-wrapper { + overflow-y: scroll; + height: 100%; + .dataviews-view-config { + width: auto; + } +} + .dataviews-view-config__sort-direction .components-toggle-group-control-option-base { text-transform: uppercase; } From ce01840f5d937ce13f780a19bfb7deba9bfa9ea6 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 9 Dec 2024 14:34:28 +0400 Subject: [PATCH 204/444] Data: Include more details when shallow equality fails in 'useSelect' (#67713) Co-authored-by: Mamaduka Co-authored-by: jsnajdr Co-authored-by: tyxla --- .../data/src/components/use-select/index.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 15a3c88d2d554..6931743151773 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -19,6 +19,25 @@ import useAsyncMode from '../async-mode-provider/use-async-mode'; const renderQueue = createQueue(); +function warnOnUnstableReference( a, b ) { + if ( ! a || ! b ) { + return; + } + + const keys = + typeof a === 'object' && typeof b === 'object' + ? Object.keys( a ).filter( ( k ) => a[ k ] !== b[ k ] ) + : []; + + // eslint-disable-next-line no-console + console.warn( + 'The `useSelect` hook returns different values when called with the same state and parameters.\n' + + 'This can lead to unnecessary re-renders and performance issues if not fixed.\n\n' + + 'Non-equal value keys: %s\n\n', + keys.join( ', ' ) + ); +} + /** * @typedef {import('../../types').StoreDescriptor} StoreDescriptor * @template {import('../../types').AnyConfig} C @@ -159,10 +178,7 @@ function Store( registry, suspense ) { if ( ! didWarnUnstableReference ) { const secondMapResult = mapSelect( select, registry ); if ( ! isShallowEqual( mapResult, secondMapResult ) ) { - // eslint-disable-next-line no-console - console.warn( - `The 'useSelect' hook returns different values when called with the same state and parameters. This can lead to unnecessary rerenders.` - ); + warnOnUnstableReference( mapResult, secondMapResult ); didWarnUnstableReference = true; } } From 0d757db8ccb47988d9d3655c2b3ca86c1a6072dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=A4gy?= Date: Mon, 9 Dec 2024 11:45:07 +0100 Subject: [PATCH 205/444] Feature: add ability to show drop cap setting in paragraph block by default via defaultControls config (#45994) Co-authored-by: fabiankaegy Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: aaronrobertshaw Co-authored-by: jasmussen Co-authored-by: carolinan Co-authored-by: jeffpaul Co-authored-by: jordesign --- packages/block-library/src/paragraph/edit.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index d32b4e8d5eca0..02ca1feceae55 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -21,6 +21,7 @@ import { useSettings, useBlockEditingMode, } from '@wordpress/block-editor'; +import { getBlockSupport } from '@wordpress/blocks'; import { formatLtr } from '@wordpress/icons'; /** @@ -47,7 +48,7 @@ function hasDropCapDisabled( align ) { return align === ( isRTL() ? 'left' : 'right' ) || align === 'center'; } -function DropCapControl( { clientId, attributes, setAttributes } ) { +function DropCapControl( { clientId, attributes, setAttributes, name } ) { // Please do not add a useSelect call to the paragraph block unconditionally. // Every useSelect added to a (frequently used) block will degrade load // and type performance. By moving it within InspectorControls, the subscription is @@ -69,11 +70,18 @@ function DropCapControl( { clientId, attributes, setAttributes } ) { helpText = __( 'Show a large initial letter.' ); } + const isDropCapControlEnabledByDefault = getBlockSupport( + name, + 'typography.defaultControls.dropCap', + false + ); + return ( !! dropCap } label={ __( 'Drop cap' ) } + isShownByDefault={ isDropCapControlEnabledByDefault } onDeselect={ () => setAttributes( { dropCap: undefined } ) } resetAllFilter={ () => ( { dropCap: undefined } ) } panelId={ clientId } @@ -99,6 +107,7 @@ function ParagraphBlock( { setAttributes, clientId, isSelected: isSingleSelected, + name, } ) { const { align, content, direction, dropCap, placeholder } = attributes; const blockProps = useBlockProps( { @@ -136,6 +145,7 @@ function ParagraphBlock( { ) } { isSingleSelected && ( Date: Mon, 9 Dec 2024 11:46:54 +0100 Subject: [PATCH 206/444] Feature: Allow Post Template block to get deeply nested within Query Block (#67657) Co-authored-by: fabiankaegy Co-authored-by: carolinan Co-authored-by: youknowriad Co-authored-by: Mamaduka Co-authored-by: ntsekouras --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/post-template/block.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 29033e278c348..71ad6359adca0 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -660,7 +660,7 @@ Contains the block elements used to render a post, like the title, date, feature - **Name:** core/post-template - **Category:** theme -- **Parent:** core/query +- **Ancestor:** core/query - **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ ## Post Terms diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index da576a83312a4..6e1f58155590f 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -4,7 +4,7 @@ "name": "core/post-template", "title": "Post Template", "category": "theme", - "parent": [ "core/query" ], + "ancestor": [ "core/query" ], "description": "Contains the block elements used to render a post, like the title, date, featured image, content or excerpt, and more.", "textdomain": "default", "usesContext": [ From 5e3d5752cca4e9005f09b1c2a8ec1a1ff89275bc Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 9 Dec 2024 13:38:06 +0200 Subject: [PATCH 207/444] DataViews: Refactor actions to render modal outside of the menu (#67664) Co-authored-by: ntsekouras Co-authored-by: oandregal Co-authored-by: ciampo Co-authored-by: doekenorg --- .../dataviews-bulk-actions/index.tsx | 46 ++++- .../dataviews-item-actions/index.tsx | 186 ++++++++---------- .../src/dataviews-layouts/list/index.tsx | 14 ++ .../src/components/post-actions/index.js | 133 +++++-------- 4 files changed, 194 insertions(+), 185 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx b/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx index 92a3fe85f67e7..86f0bb6db0ba8 100644 --- a/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import type { ReactElement } from 'react'; + /** * WordPress dependencies */ @@ -15,11 +20,46 @@ import { closeSmall } from '@wordpress/icons'; * Internal dependencies */ import DataViewsContext from '../dataviews-context'; -import { ActionWithModal } from '../dataviews-item-actions'; -import type { Action } from '../../types'; +import { ActionModal } from '../dataviews-item-actions'; +import type { Action, ActionModal as ActionModalType } from '../../types'; import type { SetSelection } from '../../private-types'; import type { ActionTriggerProps } from '../dataviews-item-actions'; +interface ActionWithModalProps< Item > { + action: ActionModalType< Item >; + items: Item[]; + ActionTriggerComponent: ( + props: ActionTriggerProps< Item > + ) => ReactElement; +} + +function ActionWithModal< Item >( { + action, + items, + ActionTriggerComponent, +}: ActionWithModalProps< Item > ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const actionTriggerProps = { + action, + onClick: () => { + setIsModalOpen( true ); + }, + items, + }; + return ( + <> + + { isModalOpen && ( + setIsModalOpen( false ) } + /> + ) } + + ); +} + export function useHasAPossibleBulkAction< Item >( actions: Action< Item >[], item: Item @@ -160,7 +200,7 @@ function ActionButton< Item >( { key={ action.id } action={ action } items={ selectedEligibleItems } - ActionTrigger={ ActionTrigger } + ActionTriggerComponent={ ActionTrigger } /> ); } diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index c5e1cb09adf15..abe63e27a15b3 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { MouseEventHandler, ReactElement } from 'react'; +import type { MouseEventHandler } from 'react'; /** * WordPress dependencies @@ -32,20 +32,17 @@ export interface ActionTriggerProps< Item > { items: Item[]; } -interface ActionModalProps< Item > { +export interface ActionModalProps< Item > { action: ActionModalType< Item >; items: Item[]; - closeModal?: () => void; -} - -interface ActionWithModalProps< Item > extends ActionModalProps< Item > { - ActionTrigger: ( props: ActionTriggerProps< Item > ) => ReactElement; - isBusy?: boolean; + closeModal: () => void; } interface ActionsMenuGroupProps< Item > { actions: Action< Item >[]; item: Item; + registry: ReturnType< typeof useRegistry >; + setActiveModalAction: ( action: ActionModalType< Item > | null ) => void; } interface ItemActionsProps< Item > { @@ -58,6 +55,7 @@ interface CompactItemActionsProps< Item > { item: Item; actions: Action< Item >[]; isSmall?: boolean; + registry: ReturnType< typeof useRegistry >; } interface PrimaryActionsProps< Item > { @@ -65,12 +63,6 @@ interface PrimaryActionsProps< Item > { actions: Action< Item >[]; registry: ReturnType< typeof useRegistry >; } -interface ActionsListProps< Item > { - item: Item; - actions: Action< Item >[]; - registry: ReturnType< typeof useRegistry >; - ActionTrigger: ( props: ActionTriggerProps< Item > ) => ReactElement; -} function ButtonTrigger< Item >( { action, @@ -98,10 +90,7 @@ function MenuItemTrigger< Item >( { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - + { label } ); @@ -118,7 +107,7 @@ export function ActionModal< Item >( { {} ) } + onRequestClose={ closeModal } focusOnMount="firstContentElement" size="medium" overlayClassName={ `dataviews-action-modal dataviews-action-modal__${ kebabCase( @@ -130,48 +119,28 @@ export function ActionModal< Item >( { ); } -export function ActionWithModal< Item >( { - action, - items, - ActionTrigger, - isBusy, -}: ActionWithModalProps< Item > ) { - const [ isModalOpen, setIsModalOpen ] = useState( false ); - const actionTriggerProps = { - action, - onClick: () => { - setIsModalOpen( true ); - }, - items, - isBusy, - }; - return ( - <> - - { isModalOpen && ( - setIsModalOpen( false ) } - /> - ) } - - ); -} - export function ActionsMenuGroup< Item >( { actions, item, + registry, + setActiveModalAction, }: ActionsMenuGroupProps< Item > ) { - const registry = useRegistry(); return ( - + { actions.map( ( action ) => ( + { + if ( 'RenderModal' in action ) { + setActiveModalAction( action ); + return; + } + action.callback( [ item ], { registry } ); + } } + items={ [ item ] } + /> + ) ) } ); } @@ -210,6 +179,7 @@ export default function ItemActions< Item >( { item={ item } actions={ eligibleActions } isSmall + registry={ registry } /> ); } @@ -239,7 +209,11 @@ export default function ItemActions< Item >( { actions={ primaryActions } registry={ registry } /> - + ); } @@ -248,23 +222,41 @@ function CompactItemActions< Item >( { item, actions, isSmall, + registry, }: CompactItemActionsProps< Item > ) { + const [ activeModalAction, setActiveModalAction ] = useState( + null as ActionModalType< Item > | null + ); return ( - + + } + placement="bottom-end" + > + - } - placement="bottom-end" - > - - + + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } + ); } @@ -273,45 +265,33 @@ function PrimaryActions< Item >( { actions, registry, }: PrimaryActionsProps< Item > ) { + const [ activeModalAction, setActiveModalAction ] = useState( null as any ); if ( ! Array.isArray( actions ) || actions.length === 0 ) { return null; } return ( - - ); -} - -function ActionsList< Item >( { - item, - actions, - registry, - ActionTrigger, -}: ActionsListProps< Item > ) { - return actions.map( ( action ) => { - if ( 'RenderModal' in action ) { - return ( - + { actions.map( ( action ) => ( + { + if ( 'RenderModal' in action ) { + setActiveModalAction( action ); + return; + } + action.callback( [ item ], { registry } ); + } } items={ [ item ] } - ActionTrigger={ ActionTrigger } /> - ); - } - return ( - { - action.callback( [ item ], { registry } ); - } } - items={ [ item ] } - /> - ); - } ); + ) ) } + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } + + ); } diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index d400cc6274169..62d813c1485af 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -40,6 +40,7 @@ import type { NormalizedField, ViewList as ViewListType, ViewListProps, + ActionModal as ActionModalType, } from '../../types'; interface ListViewItemProps< Item > { @@ -154,7 +155,11 @@ function ListItem< Item >( { const labelId = `${ idPrefix }-label`; const descriptionId = `${ idPrefix }-description`; + const registry = useRegistry(); const [ isHovered, setIsHovered ] = useState( false ); + const [ activeModalAction, setActiveModalAction ] = useState( + null as ActionModalType< Item > | null + ); const handleHover: React.MouseEventHandler = ( { type } ) => { const isHover = type === 'mouseenter'; setIsHovered( isHover ); @@ -233,8 +238,17 @@ function ListItem< Item >( { + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) }
  • ) } diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index ab11b5e318b5a..bfdddb4a5a062 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useRegistry, useSelect } from '@wordpress/data'; import { useState, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { @@ -21,7 +21,7 @@ import { usePostActions } from './actions'; const { Menu, kebabCase } = unlock( componentsPrivateApis ); export default function PostActions( { postType, postId, onActionPerformed } ) { - const [ isActionsMenuOpen, setIsActionsMenuOpen ] = useState( false ); + const [ activeModalAction, setActiveModalAction ] = useState( null ); const { item, permissions } = useSelect( ( select ) => { const { getEditedEntityRecord, getEntityRecordPermissions } = @@ -54,32 +54,34 @@ export default function PostActions( { postType, postId, onActionPerformed } ) { }, [ allActions, itemWithPermissions ] ); return ( - - setIsActionsMenuOpen( ! isActionsMenuOpen ) - } + <> + + } + placement="bottom-end" + > + - } - onOpenChange={ setIsActionsMenuOpen } - placement="bottom-end" - > - { - setIsActionsMenuOpen( false ); - } } - /> - + + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } + ); } @@ -88,78 +90,51 @@ export default function PostActions( { postType, postId, onActionPerformed } ) { // and the dataviews package should not be using the editor packages directly, // so duplicating the code here seems like the least bad option. -// Copied as is from packages/dataviews/src/item-actions.js function DropdownMenuItemTrigger( { action, onClick, items } ) { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - + { label } ); } -// Copied as is from packages/dataviews/src/item-actions.js -// With an added onClose prop. -function ActionWithModal( { action, item, ActionTrigger, onClose } ) { - const [ isModalOpen, setIsModalOpen ] = useState( false ); - const actionTriggerProps = { - action, - onClick: () => setIsModalOpen( true ), - items: [ item ], - }; - const { RenderModal, hideModalHeader } = action; +export function ActionModal( { action, items, closeModal } ) { + const label = + typeof action.label === 'string' ? action.label : action.label( items ); return ( - <> - - { isModalOpen && ( - { - setIsModalOpen( false ); - } } - overlayClassName={ `editor-action-modal editor-action-modal__${ kebabCase( - action.id - ) }` } - focusOnMount="firstContentElement" - size="medium" - > - { - setIsModalOpen( false ); - onClose(); - } } - /> - - ) } - + {} ) } + focusOnMount="firstContentElement" + size="medium" + overlayClassName={ `editor-action-modal editor-action-modal__${ kebabCase( + action.id + ) }` } + > + + ); } -// Copied as is from packages/dataviews/src/item-actions.js -// With an added onClose prop. -function ActionsDropdownMenuGroup( { actions, item, onClose } ) { +function ActionsDropdownMenuGroup( { actions, item, setActiveModalAction } ) { + const registry = useRegistry(); return ( { actions.map( ( action ) => { - if ( action.RenderModal ) { - return ( - - ); - } return ( action.callback( [ item ] ) } + onClick={ () => { + if ( 'RenderModal' in action ) { + setActiveModalAction( action ); + return; + } + action.callback( [ item ], { registry } ); + } } items={ [ item ] } /> ); From 91b130b2db14028768e8e85fdf83ac8e5cb0b5b8 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 9 Dec 2024 14:55:49 +0200 Subject: [PATCH 208/444] [Block Library]: Update the relationship of `No results` block to `ancestor` (#48348) Co-authored-by: ntsekouras Co-authored-by: Mamaduka Co-authored-by: gziolo Co-authored-by: webmandesign --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/query-no-results/block.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 71ad6359adca0..7800eae6c7f2e 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -724,7 +724,7 @@ Contains the block elements used to render content when no query results are fou - **Name:** core/query-no-results - **Category:** theme -- **Parent:** core/query +- **Ancestor:** core/query - **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ ## Pagination diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json index 2f656594afa30..c7d3ff500e0f4 100644 --- a/packages/block-library/src/query-no-results/block.json +++ b/packages/block-library/src/query-no-results/block.json @@ -5,7 +5,7 @@ "title": "No results", "category": "theme", "description": "Contains the block elements used to render content when no query results are found.", - "parent": [ "core/query" ], + "ancestor": [ "core/query" ], "textdomain": "default", "usesContext": [ "queryId", "query" ], "example": { From 01543397474a600dc1b75f309071d03d6103d4f5 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 9 Dec 2024 10:14:12 -0300 Subject: [PATCH 209/444] Stylebook: render overview colors in 4 columns (#67597) * render overview colors in 4 columns * use templateColums instead of colums to enable responsive columns * use templateColumns instead of columns * tweak CSS Co-authored-by: matiasbenedetto Co-authored-by: tellthemachines Co-authored-by: jasmussen --- .../components/style-book/color-examples.tsx | 24 ++++++++++++++++--- .../src/components/style-book/constants.ts | 1 - .../src/components/style-book/examples.tsx | 7 +++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/edit-site/src/components/style-book/color-examples.tsx b/packages/edit-site/src/components/style-book/color-examples.tsx index 97bdeecee32d3..bdc7bc7936bc1 100644 --- a/packages/edit-site/src/components/style-book/color-examples.tsx +++ b/packages/edit-site/src/components/style-book/color-examples.tsx @@ -18,13 +18,25 @@ import { */ import type { Color, Gradient } from './types'; -const ColorExamples = ( { colors, type } ): JSX.Element | null => { +type Props = { + colors: Color[] | Gradient[]; + type: 'colors' | 'gradients'; + templateColumns?: string | number; + itemHeight?: string; +}; + +const ColorExamples = ( { + colors, + type, + templateColumns = '1fr 1fr', + itemHeight = '52px', +}: Props ): JSX.Element | null => { if ( ! colors ) { return null; } return ( - + { colors.map( ( color: Color | Gradient ) => { const className = type === 'gradients' @@ -35,7 +47,13 @@ const ColorExamples = ( { colors, type } ): JSX.Element | null => { className ); - return ; + return ( + + ); } ) } ); diff --git a/packages/edit-site/src/components/style-book/constants.ts b/packages/edit-site/src/components/style-book/constants.ts index 0cbbaec07086d..401d532b98cbb 100644 --- a/packages/edit-site/src/components/style-book/constants.ts +++ b/packages/edit-site/src/components/style-book/constants.ts @@ -220,7 +220,6 @@ export const STYLE_BOOK_IFRAME_STYLES = ` } .edit-site-style-book__color-example { - height: 32px; border: 1px solid color-mix( in srgb, currentColor 10%, transparent ); } diff --git a/packages/edit-site/src/components/style-book/examples.tsx b/packages/edit-site/src/components/style-book/examples.tsx index cb7b6afcb422c..b585d556341f8 100644 --- a/packages/edit-site/src/components/style-book/examples.tsx +++ b/packages/edit-site/src/components/style-book/examples.tsx @@ -89,7 +89,12 @@ function getOverviewBlockExamples( title: __( 'Colors' ), category: 'overview', content: ( - + ), }; From e7724382e790db993682263121e917120d4daa68 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:00:48 +0200 Subject: [PATCH 210/444] Navigation: Fix active item hover color (#67732) * Navigation: Fix active item hover color * Add CHANGELOG entry * Fix duplicate Enhancements sections Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/CHANGELOG.md | 13 +++++-------- .../src/navigation/styles/navigation-styles.tsx | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2d2761924fb63..047d190ee3c5d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,11 @@ - `GradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) - `CustomGradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) - `RangeControl`: Update the design of the range control marks ([#67611](https://github.com/WordPress/gutenberg/pull/67611)) +- `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). +- `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `MenuItem`: Increase height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `MenuItemsChoice`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `Navigation`: Fix active item hover color ([#67732](https://github.com/WordPress/gutenberg/pull/67732)). ### Deprecations @@ -22,14 +27,6 @@ - `FormFileUpload`: Deprecate 36px default size ([#67438](https://github.com/WordPress/gutenberg/pull/67438)). - `FormTokenField`: Deprecate 36px default size ([#67454](https://github.com/WordPress/gutenberg/pull/67454)). - -### Enhancements - -- `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). -- `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). -- `MenuItem`: Increase height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). -- `MenuItemsChoice`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). - ### Experimental - `Menu`: throw when subcomponents are not rendered inside top level `Menu` ([#67411](https://github.com/WordPress/gutenberg/pull/67411)). diff --git a/packages/components/src/navigation/styles/navigation-styles.tsx b/packages/components/src/navigation/styles/navigation-styles.tsx index 580c0eef4dba8..71dbccd7717c4 100644 --- a/packages/components/src/navigation/styles/navigation-styles.tsx +++ b/packages/components/src/navigation/styles/navigation-styles.tsx @@ -137,6 +137,7 @@ export const ItemBaseUI = styled.li` color: ${ COLORS.white }; > button, + .components-button:hover, > a { color: ${ COLORS.white }; opacity: 1; From 896b316bc774f0b2da4b4b916196fba4877945a0 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 9 Dec 2024 23:09:46 +0900 Subject: [PATCH 211/444] Components: Deprecate `COLORS.white` (#67649) * Components: Deprecate `COLORS.white` * Add changelogs * Update snapshot Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 3 ++ packages/components/src/menu/styles.ts | 2 +- .../navigation/styles/navigation-styles.tsx | 4 +- .../test/__snapshots__/index.tsx.snap | 44 +++++++++---------- .../styles.ts | 12 ++--- .../toggle-group-control/styles.ts | 2 +- .../components/src/utils/colors-values.js | 3 ++ .../components/src/utils/config-values.js | 1 - 8 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 047d190ee3c5d..249d4f3f65b93 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,9 @@ - `GradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) - `CustomGradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) +- `Menu`: Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). +- `Navigation` (deprecated): Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). +- `ToggleGroupControl`: Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). - `RangeControl`: Update the design of the range control marks ([#67611](https://github.com/WordPress/gutenberg/pull/67611)) - `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). - `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). diff --git a/packages/components/src/menu/styles.ts b/packages/components/src/menu/styles.ts index 0e0752bf24cd1..cda5c7321f38b 100644 --- a/packages/components/src/menu/styles.ts +++ b/packages/components/src/menu/styles.ts @@ -201,7 +201,7 @@ const baseItem = css` [aria-disabled='true'] ) { background-color: ${ COLORS.theme.accent }; - color: ${ COLORS.white }; + color: ${ COLORS.theme.accentInverted }; } /* Keyboard focus (focus-visible) */ diff --git a/packages/components/src/navigation/styles/navigation-styles.tsx b/packages/components/src/navigation/styles/navigation-styles.tsx index 71dbccd7717c4..aa0976a9a0f27 100644 --- a/packages/components/src/navigation/styles/navigation-styles.tsx +++ b/packages/components/src/navigation/styles/navigation-styles.tsx @@ -134,12 +134,12 @@ export const ItemBaseUI = styled.li` &.is-active { background-color: ${ COLORS.theme.accent }; - color: ${ COLORS.white }; + color: ${ COLORS.theme.accentInverted }; > button, .components-button:hover, > a { - color: ${ COLORS.white }; + color: ${ COLORS.theme.accentInverted }; opacity: 1; } } diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index f344cd6ba1652..91e9f291ddf01 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -72,7 +72,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -134,7 +134,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -158,12 +158,12 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; padding-right: 0; - color: #fff; + color: var(--wp-components-color-foreground-inverted, #fff); } @media not ( prefers-reduced-motion ) { @@ -183,7 +183,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-12:active { @@ -211,7 +211,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -235,7 +235,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; @@ -259,7 +259,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = } .emotion-17:active { - background: #fff; + background: var(--wp-components-color-background, #fff); }
    @@ -437,7 +437,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -499,7 +499,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -542,7 +542,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-13 { @@ -706,7 +706,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -768,7 +768,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -792,12 +792,12 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; padding-right: 0; - color: #fff; + color: var(--wp-components-color-foreground-inverted, #fff); } @media not ( prefers-reduced-motion ) { @@ -817,7 +817,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-12:active { @@ -845,7 +845,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -869,7 +869,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; @@ -893,7 +893,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] } .emotion-17:active { - background: #fff; + background: var(--wp-components-color-background, #fff); }
    @@ -1065,7 +1065,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -1127,7 +1127,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -1170,7 +1170,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-13 { diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts index c0248f9b3f7f2..a53eced1219db 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts @@ -38,7 +38,7 @@ export const buttonView = ( { background: transparent; border: none; border-radius: ${ CONFIG.radiusXSmall }; - color: ${ COLORS.gray[ 700 ] }; + color: ${ COLORS.theme.gray[ 700 ] }; fill: currentColor; cursor: pointer; display: flex; @@ -70,7 +70,7 @@ export const buttonView = ( { } &:active { - background: ${ CONFIG.controlBackgroundColor }; + background: ${ COLORS.ui.background }; } ${ isDeselectable && deselectable } @@ -79,7 +79,7 @@ export const buttonView = ( { `; const pressed = css` - color: ${ COLORS.white }; + color: ${ COLORS.theme.foregroundInverted }; &:active { background: transparent; @@ -87,11 +87,11 @@ const pressed = css` `; const deselectable = css` - color: ${ COLORS.gray[ 900 ] }; + color: ${ COLORS.theme.foreground }; &:focus { box-shadow: - inset 0 0 0 1px ${ COLORS.white }, + inset 0 0 0 1px ${ COLORS.ui.background }, 0 0 0 ${ CONFIG.borderWidthFocus } ${ COLORS.theme.accent }; outline: 2px solid transparent; } @@ -112,7 +112,7 @@ const isIconStyles = ( { }; return css` - color: ${ COLORS.gray[ 900 ] }; + color: ${ COLORS.theme.foreground }; height: ${ iconButtonSizes[ size ] }; aspect-ratio: 1; padding-left: 0; diff --git a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts index bb6efe476b2b2..8376b66a5a86c 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts @@ -39,7 +39,7 @@ export const toggleGroupControl = ( { content: ''; position: absolute; pointer-events: none; - background: ${ COLORS.gray[ 900 ] }; + background: ${ COLORS.theme.foreground }; // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; diff --git a/packages/components/src/utils/colors-values.js b/packages/components/src/utils/colors-values.js index 1ad528d13f010..439d464f1460b 100644 --- a/packages/components/src/utils/colors-values.js +++ b/packages/components/src/utils/colors-values.js @@ -75,6 +75,9 @@ export const COLORS = Object.freeze( { * @deprecated Use semantic aliases in `COLORS.ui` or theme-ready variables in `COLORS.theme.gray`. */ gray: GRAY, // TODO: Stop exporting this when everything is migrated to `theme` or `ui` + /** + * @deprecated Prefer theme-ready variables in `COLORS.theme`. + */ white, alert: ALERT, /** diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index 1bc3945f9b3b1..13b704540e9c4 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -12,7 +12,6 @@ const CONTROL_PROPS = { controlPaddingXSmall: 8, controlPaddingXLarge: 12 * 1.3334, // TODO: Deprecate - controlBackgroundColor: COLORS.white, controlBoxShadowFocus: `0 0 0 0.5px ${ COLORS.theme.accent }`, controlHeight: CONTROL_HEIGHT, controlHeightXSmall: `calc( ${ CONTROL_HEIGHT } * 0.6 )`, From 9da58a749e2958f65c51352afd2f469379057fc4 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 9 Dec 2024 18:38:42 +0400 Subject: [PATCH 212/444] Data: Expose 'useSelect' warning to third-party consumers (#67735) Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: gziolo Co-authored-by: tyxla --- .../block-editor/src/hooks/test/font-size.js | 27 ++++++++++--------- .../src/hooks/test/use-query-select.js | 12 +++++++++ .../data/src/components/use-select/index.js | 2 +- .../src/components/use-select/test/index.js | 13 +++++++++ .../src/components/with-select/test/index.js | 13 +++++++++ 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/hooks/test/font-size.js b/packages/block-editor/src/hooks/test/font-size.js index 11cd024bf8a28..dc3fc9100e475 100644 --- a/packages/block-editor/src/hooks/test/font-size.js +++ b/packages/block-editor/src/hooks/test/font-size.js @@ -19,6 +19,15 @@ import { import _fontSize from '../font-size'; const noop = () => {}; +const EMPTY_ARRAY = []; +const EMPTY_OBJECT = {}; +const fontSizes = [ + { + name: 'A larger font', + size: '32px', + slug: 'larger', + }, +]; function addUseSettingFilter( callback ) { addFilter( @@ -55,13 +64,7 @@ describe( 'useBlockProps', () => { registerBlockType( blockSettings.name, blockSettings ); addUseSettingFilter( ( result, path ) => { if ( 'typography.fontSizes' === path ) { - return [ - { - name: 'A larger font', - size: '32px', - slug: 'larger', - }, - ]; + return fontSizes; } if ( 'typography.fluid' === path ) { @@ -69,7 +72,7 @@ describe( 'useBlockProps', () => { } if ( 'layout' === path ) { - return {}; + return EMPTY_OBJECT; } return result; @@ -95,7 +98,7 @@ describe( 'useBlockProps', () => { registerBlockType( blockSettings.name, blockSettings ); addUseSettingFilter( ( result, path ) => { if ( 'typography.fontSizes' === path ) { - return []; + return EMPTY_ARRAY; } if ( 'typography.fluid' === path ) { @@ -103,7 +106,7 @@ describe( 'useBlockProps', () => { } if ( 'layout' === path ) { - return {}; + return EMPTY_OBJECT; } return result; @@ -132,7 +135,7 @@ describe( 'useBlockProps', () => { registerBlockType( blockSettings.name, blockSettings ); addUseSettingFilter( ( result, path ) => { if ( 'typography.fontSizes' === path ) { - return []; + return EMPTY_ARRAY; } if ( 'typography.fluid' === path ) { @@ -140,7 +143,7 @@ describe( 'useBlockProps', () => { } if ( 'layout' === path ) { - return {}; + return EMPTY_OBJECT; } return result; diff --git a/packages/core-data/src/hooks/test/use-query-select.js b/packages/core-data/src/hooks/test/use-query-select.js index 894851f79a9c7..873b897fff4e7 100644 --- a/packages/core-data/src/hooks/test/use-query-select.js +++ b/packages/core-data/src/hooks/test/use-query-select.js @@ -17,9 +17,16 @@ import { render, screen, waitFor } from '@testing-library/react'; */ import useQuerySelect from '../use-query-select'; +/* eslint-disable @wordpress/wp-global-usage */ describe( 'useQuerySelect', () => { + const initialScriptDebug = globalThis.SCRIPT_DEBUG; let registry; + beforeAll( () => { + // Do not run hook in development mode; it will call `mapSelect` an extra time. + globalThis.SCRIPT_DEBUG = false; + } ); + beforeEach( () => { registry = createRegistry(); registry.registerStore( 'testStore', { @@ -31,6 +38,10 @@ describe( 'useQuerySelect', () => { } ); } ); + afterAll( () => { + globalThis.SCRIPT_DEBUG = initialScriptDebug; + } ); + const getTestComponent = ( mapSelectSpy, dependencyKey ) => ( props ) => { const dependencies = props[ dependencyKey ]; mapSelectSpy.mockImplementation( ( select ) => ( { @@ -188,3 +199,4 @@ describe( 'useQuerySelect', () => { ); } ); } ); +/* eslint-enable @wordpress/wp-global-usage */ diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 6931743151773..ea4869bb4e7a9 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -174,7 +174,7 @@ function Store( registry, suspense ) { listeningStores ); - if ( process.env.NODE_ENV === 'development' ) { + if ( globalThis.SCRIPT_DEBUG ) { if ( ! didWarnUnstableReference ) { const secondMapResult = mapSelect( select, registry ); if ( ! isShallowEqual( mapResult, secondMapResult ) ) { diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 3320f7d4638a5..6bb47d3c68e9e 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -32,12 +32,24 @@ function counterStore( initialCount = 0, step = 1 ) { }; } +/* eslint-disable @wordpress/wp-global-usage */ describe( 'useSelect', () => { + const initialScriptDebug = globalThis.SCRIPT_DEBUG; let registry; + + beforeAll( () => { + // Do not run hook in development mode; it will call `mapSelect` an extra time. + globalThis.SCRIPT_DEBUG = false; + } ); + beforeEach( () => { registry = createRegistry(); } ); + afterAll( () => { + globalThis.SCRIPT_DEBUG = initialScriptDebug; + } ); + it( 'passes the relevant data to the component', () => { registry.registerStore( 'testStore', { reducer: () => ( { foo: 'bar' } ), @@ -1257,3 +1269,4 @@ describe( 'useSelect', () => { } ); } ); } ); +/* eslint-enable @wordpress/wp-global-usage */ diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index fe798354cba20..9e01bb17cbb7e 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -18,7 +18,19 @@ import withDispatch from '../../with-dispatch'; import { createRegistry } from '../../../registry'; import { RegistryProvider } from '../../registry-provider'; +/* eslint-disable @wordpress/wp-global-usage */ describe( 'withSelect', () => { + const initialScriptDebug = globalThis.SCRIPT_DEBUG; + + beforeAll( () => { + // Do not run HOC in development mode; it will call `mapSelect` an extra time. + globalThis.SCRIPT_DEBUG = false; + } ); + + afterAll( () => { + globalThis.SCRIPT_DEBUG = initialScriptDebug; + } ); + it( 'passes the relevant data to the component', () => { const registry = createRegistry(); registry.registerStore( 'reactReducer', { @@ -615,3 +627,4 @@ describe( 'withSelect', () => { expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'second' ); } ); } ); +/* eslint-enable @wordpress/wp-global-usage */ From e85937ff6868a4b35856845755636c39dbbc555a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=A4gy?= Date: Mon, 9 Dec 2024 15:51:18 +0100 Subject: [PATCH 213/444] Feature: Add `navigation.isLoading` state to core/router store (#67680) --- packages/interactivity-router/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts index 8a46d4ce35011..0c10e896ce1ef 100644 --- a/packages/interactivity-router/src/index.ts +++ b/packages/interactivity-router/src/index.ts @@ -223,6 +223,7 @@ interface Store { state: { url: string; navigation: { + isLoading: boolean; hasStarted: boolean; hasFinished: boolean; }; @@ -237,6 +238,7 @@ export const { state, actions } = store< Store >( 'core/router', { state: { url: window.location.href, navigation: { + isLoading: false, hasStarted: false, hasFinished: false, }, @@ -289,6 +291,7 @@ export const { state, actions } = store< Store >( 'core/router', { return; } + navigation.isLoading = true; if ( loadingAnimation ) { navigation.hasStarted = true; navigation.hasFinished = false; @@ -328,6 +331,7 @@ export const { state, actions } = store< Store >( 'core/router', { // Update the navigation status once the the new page rendering // has been completed. + navigation.isLoading = false; if ( loadingAnimation ) { navigation.hasStarted = false; navigation.hasFinished = true; From 6b8e47fbd17c0710a4e55d4104dce185fdfac53b Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 9 Dec 2024 07:09:23 -0800 Subject: [PATCH 214/444] TypeScript: Convert factory utils in data package to TS (#67667) * TypeScript: Convert factory utils in data package to TS * Fix docgen build error * Update docs * Revert and fix TS error * Extract and improve signature --- packages/core-data/src/private-selectors.ts | 2 +- packages/data/README.md | 17 +++------ ...{create-selector.js => create-selector.ts} | 4 --- packages/data/src/{factory.js => factory.ts} | 35 +++++++++++++------ 4 files changed, 30 insertions(+), 28 deletions(-) rename packages/data/src/{create-selector.js => create-selector.ts} (55%) rename packages/data/src/{factory.js => factory.ts} (75%) diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 7779051265306..abdf2c837e8ed 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -92,7 +92,7 @@ export function getEntityRecordPermissions( name: string, id: string ) { - return getEntityRecordsPermissions( state, kind, name, id )[ 0 ]; + return getEntityRecordsPermissions( state, kind, name, [ id ] )[ 0 ]; } /** diff --git a/packages/data/README.md b/packages/data/README.md index b6e0e03b1d8b7..67c01af24bde3 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -418,11 +418,11 @@ When registering a control created with `createRegistryControl` with a store, th _Parameters_ -- _registryControl_ `Function`: Function receiving a registry object and returning a control. +- _registryControl_ `T & { isRegistryControl?: boolean; }`: Function receiving a registry object and returning a control. _Returns_ -- `Function`: Registry control that can be registered with a store. +- Registry control that can be registered with a store. ### createRegistrySelector @@ -471,11 +471,11 @@ with a store. _Parameters_ -- _registrySelector_ `Function`: Function receiving a registry `select` function and returning a state selector. +- _registrySelector_ `( select: ) => Selector`: Function receiving a registry `select` function and returning a state selector. _Returns_ -- `Function`: Registry selector that can be registered with a store. +- `RegistrySelector< Selector >`: Registry selector that can be registered with a store. ### createSelector @@ -485,15 +485,6 @@ _Related_ - The documentation for the `rememo` package from which the `createSelector` function is reexported. -_Parameters_ - -- _selector_ `Function`: Selector function that calculates a value from state and parameters. -- _getDependants_ `Function`: Function that returns an array of "dependant" objects. - -_Returns_ - -- `Function`: A memoized version of `selector` that caches the calculated return values. - ### dispatch Given a store descriptor, returns an object of the store's action creators. Calling an action creator will cause it to be dispatched, updating the state value accordingly. diff --git a/packages/data/src/create-selector.js b/packages/data/src/create-selector.ts similarity index 55% rename from packages/data/src/create-selector.js rename to packages/data/src/create-selector.ts index 069932e8007e2..bfb7a1d283733 100644 --- a/packages/data/src/create-selector.js +++ b/packages/data/src/create-selector.ts @@ -3,9 +3,5 @@ * and the selector parameters, and recomputes the values only when any of them changes. * * @see The documentation for the `rememo` package from which the `createSelector` function is reexported. - * - * @param {Function} selector Selector function that calculates a value from state and parameters. - * @param {Function} getDependants Function that returns an array of "dependant" objects. - * @return {Function} A memoized version of `selector` that caches the calculated return values. */ export { default as createSelector } from 'rememo'; diff --git a/packages/data/src/factory.js b/packages/data/src/factory.ts similarity index 75% rename from packages/data/src/factory.js rename to packages/data/src/factory.ts index be4ef8cf673c5..8218fd2cdb07d 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.ts @@ -1,3 +1,14 @@ +/** + * Internal dependencies + */ +import type { select as globalSelect } from './select'; + +type RegistrySelector< Selector extends ( ...args: any[] ) => any > = { + ( ...args: Parameters< Selector > ): ReturnType< Selector >; + isRegistrySelector?: boolean; + registry?: any; +}; + /** * Creates a selector function that takes additional curried argument with the * registry `select` function. While a regular selector has signature @@ -33,17 +44,21 @@ * registry as argument. The registry binding happens automatically when registering the selector * with a store. * - * @param {Function} registrySelector Function receiving a registry `select` - * function and returning a state selector. + * @param registrySelector Function receiving a registry `select` + * function and returning a state selector. * - * @return {Function} Registry selector that can be registered with a store. + * @return Registry selector that can be registered with a store. */ -export function createRegistrySelector( registrySelector ) { +export function createRegistrySelector< + Selector extends ( ...args: any[] ) => any, +>( + registrySelector: ( select: typeof globalSelect ) => Selector +): RegistrySelector< Selector > { const selectorsByRegistry = new WeakMap(); // Create a selector function that is bound to the registry referenced by `selector.registry` // and that has the same API as a regular selector. Binding it in such a way makes it // possible to call the selector directly from another selector. - const wrappedSelector = ( ...args ) => { + const wrappedSelector: RegistrySelector< Selector > = ( ...args ) => { let selector = selectorsByRegistry.get( wrappedSelector.registry ); // We want to make sure the cache persists even when new registry // instances are created. For example patterns create their own editors @@ -60,8 +75,6 @@ export function createRegistrySelector( registrySelector ) { * Flag indicating that the selector is a registry selector that needs the correct registry * reference to be assigned to `selector.registry` to make it work correctly. * be mapped as a registry selector. - * - * @type {boolean} */ wrappedSelector.isRegistrySelector = true; @@ -84,11 +97,13 @@ export function createRegistrySelector( registrySelector ) { * When registering a control created with `createRegistryControl` with a store, the store * knows which calling convention to use when executing the control. * - * @param {Function} registryControl Function receiving a registry object and returning a control. + * @param registryControl Function receiving a registry object and returning a control. * - * @return {Function} Registry control that can be registered with a store. + * @return Registry control that can be registered with a store. */ -export function createRegistryControl( registryControl ) { +export function createRegistryControl< T extends ( ...args: any ) => any >( + registryControl: T & { isRegistryControl?: boolean } +) { registryControl.isRegistryControl = true; return registryControl; From 9ae9ec35ff68f3426d3a75e3c543d80c1f16c3e2 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 9 Dec 2024 16:39:08 +0100 Subject: [PATCH 215/444] DataViews: Fix filters lost when switching layouts (#67740) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-list/index.js | 90 ++++++++++--------- test/e2e/specs/site-editor/page-list.spec.js | 56 ++++++++++++ 2 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 test/e2e/specs/site-editor/page-list.spec.js diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 145a5e8243ac5..a67a505795b3c 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -13,7 +13,7 @@ import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; import { drawerRight } from '@wordpress/icons'; -import { usePrevious } from '@wordpress/compose'; +import { useEvent, usePrevious } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; /** @@ -112,53 +112,50 @@ function useView( postType ) { }; } ); - const setViewWithUrlUpdate = useCallback( - ( newView ) => { - if ( newView.type === LAYOUT_LIST && ! layout ) { - // Skip updating the layout URL param if - // it is not present and the newView.type is LAYOUT_LIST. - } else if ( newView.type !== layout ) { - history.navigate( - addQueryArgs( path, { - layout: newView.type, - } ) - ); - } + const setViewWithUrlUpdate = useEvent( ( newView ) => { + setView( newView ); - setView( newView ); + if ( isCustom === 'true' && editedEntityRecord?.id ) { + editEntityRecord( + 'postType', + 'wp_dataviews', + editedEntityRecord?.id, + { + content: JSON.stringify( newView ), + } + ); + } - if ( isCustom === 'true' && editedEntityRecord?.id ) { - editEntityRecord( - 'postType', - 'wp_dataviews', - editedEntityRecord?.id, - { - content: JSON.stringify( newView ), - } - ); - } - }, - [ - history, - isCustom, - editEntityRecord, - editedEntityRecord?.id, - layout, - path, - ] - ); + const currentUrlLayout = layout ?? LAYOUT_LIST; + if ( newView.type !== currentUrlLayout ) { + history.navigate( + addQueryArgs( path, { + layout: newView.type, + } ) + ); + } + } ); // When layout URL param changes, update the view type // without affecting any other config. + const onUrlLayoutChange = useEvent( () => { + setView( ( prevView ) => { + const layoutToApply = layout ?? LAYOUT_LIST; + if ( layoutToApply === prevView.type ) { + return prevView; + } + return { + ...prevView, + type: layout ?? LAYOUT_LIST, + }; + } ); + } ); useEffect( () => { - setView( ( prevView ) => ( { - ...prevView, - type: layout ?? LAYOUT_LIST, - } ) ); - }, [ layout ] ); + onUrlLayoutChange(); + }, [ onUrlLayoutChange, layout ] ); // When activeView or isCustom URL parameters change, reset the view. - useEffect( () => { + const onUrlActiveViewChange = useEvent( () => { let newView; if ( isCustom === 'true' ) { newView = getCustomView( editedEntityRecord ); @@ -173,9 +170,18 @@ function useView( postType ) { type, } ); } - }, [ activeView, isCustom, layout, defaultViews, editedEntityRecord ] ); + } ); + useEffect( () => { + onUrlActiveViewChange(); + }, [ + onUrlActiveViewChange, + activeView, + isCustom, + defaultViews, + editedEntityRecord, + ] ); - return [ view, setViewWithUrlUpdate, setViewWithUrlUpdate ]; + return [ view, setViewWithUrlUpdate ]; } const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All but 'trash'. diff --git a/test/e2e/specs/site-editor/page-list.spec.js b/test/e2e/specs/site-editor/page-list.spec.js new file mode 100644 index 0000000000000..fa9cb86cd1d62 --- /dev/null +++ b/test/e2e/specs/site-editor/page-list.spec.js @@ -0,0 +1,56 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Page List', () => { + test.beforeAll( async ( { requestUtils } ) => { + // Activate a theme with permissions to access the site editor. + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.createPage( { + title: 'Privacy Policy', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Sample Page', + status: 'publish', + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + // Go back to the default theme. + await Promise.all( [ + requestUtils.activateTheme( 'twentytwentyone' ), + requestUtils.deleteAllPages(), + ] ); + } ); + + test.beforeEach( async ( { admin, page } ) => { + // Go to the pages page, as it has the list layout enabled by default. + await admin.visitSiteEditor(); + await page.getByRole( 'button', { name: 'Pages' } ).click(); + } ); + + test( 'Persists filter/search when switching layout', async ( { + page, + } ) => { + // Search pages + await page + .getByRole( 'searchbox', { name: 'Search' } ) + .fill( 'Privacy' ); + + // Switch layout + await page.getByRole( 'button', { name: 'Layout' } ).click(); + await page.getByRole( 'menuitemradio', { name: 'Table' } ).click(); + + // Confirm the table is visible + await expect( page.getByRole( 'table' ) ).toContainText( + 'Privacy Policy' + ); + + // The search should still contain the search term + await expect( + page.getByRole( 'searchbox', { name: 'Search' } ) + ).toHaveValue( 'Privacy' ); + } ); +} ); From d03eeae0d48b35fb3e5e05a90bbc7b956a6b72f5 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:15:04 +0000 Subject: [PATCH 216/444] wp-env: Add phpMyAdmin support (#67588) * wp-env: Add phpMyAdmin support Defines two new docker-compose services: `phpmyadmin` and `tests-phpmyadmin`. These are off by default. They can be individually turned on by: - Specifying a port in any enviroment's config via key `phpmyadminPort` (see bottom of commit message for example). - By setting environment variable `WP_ENV_PHPMYADMIN_PORT` and/or `WP_ENV_TESTS_PHPMYADMIN_PORT`. * Opt into the phpMyAdmin service in the Gutenberg environment * wp-env: start: refactor computation of command output - Refactor repeating logic into getPublicDockerPort - Remove trailing newlines from dockerCompose output - Refactor evaluation of spinner.prefixText so as to clearly reveal newlines. { env: { development: { ... phpmyadminPort: 9000 }, tests: { ... } } } --------- Co-authored-by: Riad Benguella --- .wp-env.json | 3 + packages/env/CHANGELOG.md | 4 + packages/env/README.md | 2 +- .../env/lib/build-docker-compose-config.js | 27 +++++++ packages/env/lib/commands/start.js | 76 +++++++++++++++---- .../get-config-from-environment-vars.js | 7 ++ packages/env/lib/config/parse-config.js | 27 +++++-- .../__snapshots__/config-integration.js.snap | 8 ++ packages/env/lib/config/test/parse-config.js | 54 +++++++++++++ packages/env/lib/validate-run-container.js | 1 + schemas/json/wp-env.json | 7 +- 11 files changed, 190 insertions(+), 26 deletions(-) diff --git a/.wp-env.json b/.wp-env.json index 05ea05b2809f9..d368f3ea1c72a 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -4,6 +4,9 @@ "plugins": [ "." ], "themes": [ "./test/emptytheme" ], "env": { + "development": { + "phpmyadminPort": 9000 + }, "tests": { "mappings": { "wp-content/plugins/gutenberg": ".", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 651d6b285e1bd..6ee37efdf850c 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Add phpMyAdmin as an optional service. Enabled via the new `phpmyadminPort` environment config, as well as env vars `WP_ENV_PHPMYADMIN_PORT` and `WP_ENV_TESTS_PHPMYADMIN_PORT`. + ## 10.13.0 (2024-11-27) ## 10.12.0 (2024-11-16) diff --git a/packages/env/README.md b/packages/env/README.md index 35b00fe83e8f4..af037f5ee80ab 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -349,7 +349,7 @@ containers. Positionals: container The Docker service to run the command on. [string] [required] [choices: "mysql", "tests-mysql", "wordpress", - "tests-wordpress", "cli", "tests-cli", "composer", "phpunit"] + "tests-wordpress", "cli", "tests-cli", "composer", "phpmyadmin"] command The command to run. [required] Options: diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index a394537e36087..a1a4f68256b68 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -174,6 +174,13 @@ module.exports = function buildDockerComposeConfig( config ) { config.env.tests.mysqlPort ?? '' }}:3306`; + const developmentPhpmyadminPorts = `\${WP_ENV_PHPMYADMIN_PORT:-${ + config.env.development.phpmyadminPort ?? '' + }}:80`; + const testsPhpmyadminPorts = `\${WP_ENV_TESTS_PHPMYADMIN_PORT:-${ + config.env.tests.phpmyadminPort ?? '' + }}:80`; + return { services: { mysql: { @@ -266,6 +273,26 @@ module.exports = function buildDockerComposeConfig( config ) { }, extra_hosts: [ 'host.docker.internal:host-gateway' ], }, + phpmyadmin: { + image: 'phpmyadmin', + ports: [ developmentPhpmyadminPorts ], + environment: { + PMA_PORT: 3306, + PMA_HOST: 'mysql', + PMA_USER: 'root', + PMA_PASSWORD: 'password', + }, + }, + 'tests-phpmyadmin': { + image: 'phpmyadmin', + ports: [ testsPhpmyadminPorts ], + environment: { + PMA_PORT: 3306, + PMA_HOST: 'tests-mysql', + PMA_USER: 'root', + PMA_PASSWORD: 'password', + }, + }, }, volumes: { ...( ! config.env.development.coreSource && { wordpress: {} } ), diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 4203ac7463228..24d57d71fd3c6 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -180,6 +180,24 @@ module.exports = async function start( { } ); + if ( config.env.development.phpmyadminPort ) { + await dockerCompose.upOne( 'phpmyadmin', { + ...dockerComposeConfig, + commandOptions: shouldConfigureWp + ? [ '--build', '--force-recreate' ] + : [], + } ); + } + + if ( config.env.tests.phpmyadminPort ) { + await dockerCompose.upOne( 'tests-phpmyadmin', { + ...dockerComposeConfig, + commandOptions: shouldConfigureWp + ? [ '--build', '--force-recreate' ] + : [], + } ); + } + // Make sure we've consumed the custom CLI dockerfile. if ( shouldConfigureWp ) { await dockerCompose.buildOne( [ 'cli' ], { ...dockerComposeConfig } ); @@ -225,35 +243,61 @@ module.exports = async function start( { const siteUrl = config.env.development.config.WP_SITEURL; const testsSiteUrl = config.env.tests.config.WP_SITEURL; - const { out: mySQLAddress } = await dockerCompose.port( + const mySQLPort = await getPublicDockerPort( 'mysql', 3306, dockerComposeConfig ); - const mySQLPort = mySQLAddress.split( ':' ).pop(); - const { out: testsMySQLAddress } = await dockerCompose.port( + const testsMySQLPort = await getPublicDockerPort( 'tests-mysql', 3306, dockerComposeConfig ); - const testsMySQLPort = testsMySQLAddress.split( ':' ).pop(); - - spinner.prefixText = 'WordPress development site started' - .concat( siteUrl ? ` at ${ siteUrl }` : '.' ) - .concat( '\n' ) - .concat( 'WordPress test site started' ) - .concat( testsSiteUrl ? ` at ${ testsSiteUrl }` : '.' ) - .concat( '\n' ) - .concat( `MySQL is listening on port ${ mySQLPort }` ) - .concat( - `MySQL for automated testing is listening on port ${ testsMySQLPort }` - ) - .concat( '\n' ); + const phpmyadminPort = config.env.development.phpmyadminPort + ? await getPublicDockerPort( 'phpmyadmin', 80, dockerComposeConfig ) + : null; + + const testsPhpmyadminPort = config.env.tests.phpmyadminPort + ? await getPublicDockerPort( + 'tests-phpmyadmin', + 80, + dockerComposeConfig + ) + : null; + + spinner.prefixText = [ + 'WordPress development site started' + + ( siteUrl ? ` at ${ siteUrl }` : '.' ), + 'WordPress test site started' + + ( testsSiteUrl ? ` at ${ testsSiteUrl }` : '.' ), + `MySQL is listening on port ${ mySQLPort }`, + `MySQL for automated testing is listening on port ${ testsMySQLPort }`, + phpmyadminPort && + `phpMyAdmin started at http://localhost:${ phpmyadminPort }`, + testsPhpmyadminPort && + `phpMyAdmin for automated testing started at http://localhost:${ testsPhpmyadminPort }`, + ] + .filter( Boolean ) + .join( '\n' ); + spinner.prefixText += '\n\n'; spinner.text = 'Done!'; }; +async function getPublicDockerPort( + service, + containerPort, + dockerComposeConfig +) { + const { out: address } = await dockerCompose.port( + service, + containerPort, + dockerComposeConfig + ); + return address.split( ':' ).pop().trim(); +} + /** * Checks for legacy installs and provides * the user the option to delete them. diff --git a/packages/env/lib/config/get-config-from-environment-vars.js b/packages/env/lib/config/get-config-from-environment-vars.js index beaf721ea1c0c..618b5fff25792 100644 --- a/packages/env/lib/config/get-config-from-environment-vars.js +++ b/packages/env/lib/config/get-config-from-environment-vars.js @@ -20,6 +20,7 @@ const { checkPort, checkVersion, checkString } = require( './validate-config' ); * @property {?number} mysqlPort An override for the development environment's MySQL port. * @property {?number} testsPort An override for the testing environment's port. * @property {?number} testsMysqlPort An override for the testing environment's MySQL port. + * @property {?number} phpmyadminPort An override for the development environment's phpMyAdmin port. * @property {?WPSource} coreSource An override for all environment's coreSource. * @property {?string} phpVersion An override for all environment's PHP version. * @property {?Object.} lifecycleScripts An override for various lifecycle scripts. @@ -40,6 +41,12 @@ module.exports = function getConfigFromEnvironmentVars( cacheDirectoryPath ) { testsMysqlPort: getPortFromEnvironmentVariable( 'WP_ENV_TESTS_MYSQL_PORT' ), + phpmyadminPort: getPortFromEnvironmentVariable( + 'WP_ENV_PHPMYADMIN_PORT' + ), + testsPhpmyadminPort: getPortFromEnvironmentVariable( + 'WP_ENV_TESTS_PHPMYADMIN_PORT' + ), lifecycleScripts: getLifecycleScriptOverrides(), }; diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index 1fc7e94925149..bddd7bc72aaee 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -46,14 +46,15 @@ const mergeConfigs = require( './merge-configs' ); * The environment-specific configuration options. (development/tests/etc) * * @typedef WPEnvironmentConfig - * @property {WPSource} coreSource The WordPress installation to load in the environment. - * @property {WPSource[]} pluginSources Plugins to load in the environment. - * @property {WPSource[]} themeSources Themes to load in the environment. - * @property {number} port The port to use. - * @property {number} mysqlPort The port to use for MySQL. Random if empty. - * @property {Object} config Mapping of wp-config.php constants to their desired values. - * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. - * @property {string|null} phpVersion Version of PHP to use in the environments, of the format 0.0. + * @property {WPSource} coreSource The WordPress installation to load in the environment. + * @property {WPSource[]} pluginSources Plugins to load in the environment. + * @property {WPSource[]} themeSources Themes to load in the environment. + * @property {number} port The port to use. + * @property {number} mysqlPort The port to use for MySQL. Random if empty. + * @property {number} phpmyadminPort The port to use for phpMyAdmin. If empty, disabled phpMyAdmin. + * @property {Object} config Mapping of wp-config.php constants to their desired values. + * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. + * @property {string|null} phpVersion Version of PHP to use in the environments, of the format 0.0. */ /** @@ -87,6 +88,7 @@ const DEFAULT_ENVIRONMENT_CONFIG = { port: 8888, testsPort: 8889, mysqlPort: null, + phpmyadminPort: null, mappings: {}, config: { FS_METHOD: 'direct', @@ -282,6 +284,11 @@ function getEnvironmentVarOverrides( cacheDirectoryPath ) { overrideConfig.env.development.mysqlPort = overrides.mysqlPort; } + if ( overrides.phpmyadminPort ) { + overrideConfig.env.development.phpmyadminPort = + overrides.phpmyadminPort; + } + if ( overrides.testsPort ) { overrideConfig.testsPort = overrides.testsPort; overrideConfig.env.tests.port = overrides.testsPort; @@ -455,6 +462,10 @@ async function parseEnvironmentConfig( parsedConfig.mysqlPort = config.mysqlPort; } + if ( config.phpmyadminPort !== undefined ) { + parsedConfig.phpmyadminPort = config.phpmyadminPort; + } + if ( config.phpVersion !== undefined ) { // Support null as a valid input. if ( config.phpVersion !== null ) { diff --git a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap index 6c3618f4724cb..6b671a6bc858e 100644 --- a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap +++ b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap @@ -31,6 +31,7 @@ exports[`Config Integration should load local and override configuration files 1 "mappings": {}, "mysqlPort": 23306, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 999, "themeSources": [], @@ -60,6 +61,7 @@ exports[`Config Integration should load local and override configuration files 1 "mappings": {}, "mysqlPort": 23307, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 456, "themeSources": [], @@ -106,6 +108,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "mappings": {}, "mysqlPort": 13306, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 123, "themeSources": [], @@ -135,6 +138,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "mappings": {}, "mysqlPort": 23307, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 8889, "themeSources": [], @@ -181,6 +185,7 @@ exports[`Config Integration should use default configuration 1`] = ` "mappings": {}, "mysqlPort": null, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 8888, "themeSources": [], @@ -210,6 +215,7 @@ exports[`Config Integration should use default configuration 1`] = ` "mappings": {}, "mysqlPort": null, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 8889, "themeSources": [], @@ -256,6 +262,7 @@ exports[`Config Integration should use environment variables over local and over "mappings": {}, "mysqlPort": 23306, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 12345, "testsPort": 61234, @@ -286,6 +293,7 @@ exports[`Config Integration should use environment variables over local and over "mappings": {}, "mysqlPort": 23307, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 61234, "testsPort": 61234, diff --git a/packages/env/lib/config/test/parse-config.js b/packages/env/lib/config/test/parse-config.js index 38e4db9860cb3..cc6e2c7a96bbc 100644 --- a/packages/env/lib/config/test/parse-config.js +++ b/packages/env/lib/config/test/parse-config.js @@ -22,6 +22,7 @@ const DEFAULT_CONFIG = { port: 8888, testsPort: 8889, mysqlPort: null, + phpmyadminPort: null, phpVersion: null, coreSource: { type: 'git', @@ -400,4 +401,57 @@ describe( 'parseConfig', () => { ) ); } ); + + it( 'should parse phpmyadmin configuration for a given environment', async () => { + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === '/test/gutenberg/.wp-env.json' ) { + return { + core: 'WordPress/WordPress#Test', + phpVersion: '1.0', + lifecycleScripts: { + afterStart: 'test', + }, + env: { + development: { + phpmyadminPort: 9001, + }, + }, + }; + } + } ); + + const parsed = await parseConfig( '/test/gutenberg', '/cache' ); + + const expected = { + development: { + ...DEFAULT_CONFIG.env.development, + phpmyadminPort: 9001, + }, + tests: DEFAULT_CONFIG.env.tests, + }; + expect( parsed.env ).toEqual( expected ); + } ); + + it( 'should ignore root-level configuration for phpmyadmin', async () => { + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === '/test/gutenberg/.wp-env.json' ) { + return { + core: 'WordPress/WordPress#Test', + phpVersion: '1.0', + lifecycleScripts: { + afterStart: 'test', + }, + phpmyadminPort: 9001, + }; + } + } ); + + const parsed = await parseConfig( '/test/gutenberg', '/cache' ); + + const expected = { + development: DEFAULT_CONFIG.env.development, + tests: DEFAULT_CONFIG.env.tests, + }; + expect( parsed.env ).toEqual( expected ); + } ); } ); diff --git a/packages/env/lib/validate-run-container.js b/packages/env/lib/validate-run-container.js index 77e68d2a3a4dc..fbb6670f6c150 100644 --- a/packages/env/lib/validate-run-container.js +++ b/packages/env/lib/validate-run-container.js @@ -10,6 +10,7 @@ const RUN_CONTAINERS = [ 'tests-wordpress', 'cli', 'tests-cli', + 'phpmyadmin', ]; /** diff --git a/schemas/json/wp-env.json b/schemas/json/wp-env.json index 491d1f8cf7301..8aa604ed41ed1 100644 --- a/schemas/json/wp-env.json +++ b/schemas/json/wp-env.json @@ -62,6 +62,10 @@ "description": "Mapping of WordPress directories to local directories to be mounted in the WordPress instance.", "type": "object", "default": {} + }, + "phpmyadminPort": { + "description": "The port number to access phpMyAdmin.", + "type": "integer" } } }, @@ -73,7 +77,8 @@ "themes", "port", "config", - "mappings" + "mappings", + "phpmyadminPort" ] } }, From ad0c1d444ae62e67d6a75ecaaeea8f62d23badba Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 9 Dec 2024 21:15:56 +0400 Subject: [PATCH 217/444] Replace remaining custom deep cloning with 'structuredClone' (#67707) * Replace remaining custom deep cloning with 'structuredClone' * Polyfill structuredClone for jsdom Co-authored-by: Mamaduka Co-authored-by: tyxla Co-authored-by: jsnajdr --- .../src/components/global-styles/use-global-styles-output.js | 2 +- packages/block-editor/src/hooks/style.js | 2 +- test/unit/config/global-mocks.js | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 7bdc95d222142..fabc65d143d1a 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -624,7 +624,7 @@ function pickStyleKeys( treeToPickFrom ) { // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [ key, - JSON.parse( JSON.stringify( style ) ), + structuredClone( style ), ] ); return Object.fromEntries( clonedEntries ); } diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index db2acd01665b6..5be2b1b3fd40a 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -245,7 +245,7 @@ export function omitStyle( style, paths, preserveReference = false ) { let newStyle = style; if ( ! preserveReference ) { - newStyle = JSON.parse( JSON.stringify( style ) ); + newStyle = structuredClone( style ); } if ( ! Array.isArray( paths ) ) { diff --git a/test/unit/config/global-mocks.js b/test/unit/config/global-mocks.js index ce64f03b514be..8db2c180fadf3 100644 --- a/test/unit/config/global-mocks.js +++ b/test/unit/config/global-mocks.js @@ -3,6 +3,7 @@ */ import { TextDecoder, TextEncoder } from 'node:util'; import { Blob as BlobPolyfill, File as FilePolyfill } from 'node:buffer'; +import 'core-js/stable/structured-clone'; jest.mock( '@wordpress/compose', () => { return { @@ -49,3 +50,6 @@ if ( ! global.TextEncoder ) { // Override jsdom built-ins with native node implementation. global.Blob = BlobPolyfill; global.File = FilePolyfill; + +// Polyfill structuredClone for jsdom. +global.structuredClone = structuredClone; From 95cbcdf5daacccccd181dfa7850e706c5a1e009c Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 9 Dec 2024 11:34:42 -0600 Subject: [PATCH 218/444] Inserter: Should receive focus on open (#67754) * Set default tab to 'blocks' if none passed * Add tests for focusing blocks tab and closing inserter on keypresses --- .../src/components/inserter/menu.js | 2 + .../site-editor/site-editor-inserter.spec.js | 53 +++++++++++++------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index ef260beb85906..019a37bffdde2 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -82,6 +82,8 @@ function InserterMenu( if ( isZoomOutMode ) { return 'patterns'; } + + return 'blocks'; } const [ selectedTab, setSelectedTab ] = useState( getInitialTab() ); diff --git a/test/e2e/specs/site-editor/site-editor-inserter.spec.js b/test/e2e/specs/site-editor/site-editor-inserter.spec.js index a730367d841bf..2364657613108 100644 --- a/test/e2e/specs/site-editor/site-editor-inserter.spec.js +++ b/test/e2e/specs/site-editor/site-editor-inserter.spec.js @@ -28,32 +28,51 @@ test.describe( 'Site Editor Inserter', () => { }, } ); - // eslint-disable-next-line playwright/expect-expect test( 'inserter toggle button should toggle global inserter', async ( { InserterUtils, + page, + editor, } ) => { await InserterUtils.openBlockLibrary(); await InserterUtils.closeBlockLibrary(); - } ); - // A test for https://github.com/WordPress/gutenberg/issues/43090. - test( 'should close the inserter when clicking on the toggle button', async ( { - editor, - InserterUtils, - } ) => { - const beforeBlocks = await editor.getBlocks(); + await test.step( 'should open the inserter via enter keypress on toggle button', async () => { + await InserterUtils.inserterButton.focus(); + await page.keyboard.press( 'Enter' ); + await expect( InserterUtils.blockLibrary ).toBeVisible(); + } ); - await InserterUtils.openBlockLibrary(); - await InserterUtils.expectActiveTab( 'Blocks' ); - await InserterUtils.blockLibrary - .getByRole( 'option', { name: 'Buttons' } ) - .click(); + await test.step( 'should set focus to the blocks tab when opening the inserter', async () => { + await expect( + InserterUtils.getBlockLibraryTab( 'Blocks' ) + ).toBeFocused(); + } ); + + await test.step( 'should close the inserter via escape keypress', async () => { + await page.keyboard.press( 'Escape' ); + await expect( InserterUtils.blockLibrary ).toBeHidden(); + } ); - await expect - .poll( editor.getBlocks ) - .toMatchObject( [ ...beforeBlocks, { name: 'core/buttons' } ] ); + await test.step( 'should focus inserter toggle button after closing the inserter via escape keypress', async () => { + await expect( InserterUtils.inserterButton ).toBeFocused(); + } ); - await InserterUtils.closeBlockLibrary(); + // A test for https://github.com/WordPress/gutenberg/issues/43090. + await test.step( 'should close the inserter when clicking on the toggle button', async () => { + const beforeBlocks = await editor.getBlocks(); + + await InserterUtils.openBlockLibrary(); + await InserterUtils.expectActiveTab( 'Blocks' ); + await InserterUtils.blockLibrary + .getByRole( 'option', { name: 'Buttons' } ) + .click(); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ ...beforeBlocks, { name: 'core/buttons' } ] ); + + await InserterUtils.closeBlockLibrary(); + } ); } ); test.describe( 'Inserter Zoom Level UX', () => { From f430cd73ff8eaf58aa9782d50891e8eb50a9ee54 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 9 Dec 2024 18:24:42 +0000 Subject: [PATCH 219/444] Fix: Dataviews remove primary field concept from some classes. (#67689) Co-authored-by: jorgefilipecosta Co-authored-by: youknowriad --- packages/dataviews/src/dataviews-layouts/grid/index.tsx | 8 +++----- packages/dataviews/src/dataviews-layouts/grid/style.scss | 2 +- packages/dataviews/src/dataviews-layouts/list/style.scss | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index 4a01609570202..e10ae7b591af5 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -82,11 +82,11 @@ function GridItem< Item >( { className: 'dataviews-view-grid__media', } ); - const clickablePrimaryItemProps = getClickableItemProps( { + const clickableTitleItemProps = getClickableItemProps( { item, isItemClickable, onClickItem, - className: 'dataviews-view-grid__primary-field dataviews-title-field', + className: 'dataviews-view-grid__title-field dataviews-title-field', } ); return ( @@ -128,9 +128,7 @@ function GridItem< Item >( { justify="space-between" className="dataviews-view-grid__title-actions" > -
    - { renderedTitleField } -
    +
    { renderedTitleField }
    diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index 70c94653371d1..e9fcb472dc318 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -15,7 +15,7 @@ padding: $grid-unit-10 0 $grid-unit-05; } - .dataviews-view-grid__primary-field { + .dataviews-view-grid__title-field { min-height: $grid-unit-30; // Preserve layout when there is no ellipsis button display: flex; align-items: center; diff --git a/packages/dataviews/src/dataviews-layouts/list/style.scss b/packages/dataviews/src/dataviews-layouts/list/style.scss index 8fe048cab77b5..65c28a90608a6 100644 --- a/packages/dataviews/src/dataviews-layouts/list/style.scss +++ b/packages/dataviews/src/dataviews-layouts/list/style.scss @@ -50,7 +50,7 @@ ul.dataviews-view-list { } &:not(.is-selected) { - .dataviews-view-list__primary-field { + .dataviews-view-list__title-field { color: $gray-900; } &:hover, @@ -59,7 +59,7 @@ ul.dataviews-view-list { color: var(--wp-admin-theme-color); background-color: #f8f8f8; - .dataviews-view-list__primary-field, + .dataviews-view-list__title-field, .dataviews-view-list__fields { color: var(--wp-admin-theme-color); } @@ -74,7 +74,7 @@ ul.dataviews-view-list { background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04); color: $gray-900; - .dataviews-view-list__primary-field, + .dataviews-view-list__title-field, .dataviews-view-list__fields { color: var(--wp-admin-theme-color); } @@ -106,7 +106,7 @@ ul.dataviews-view-list { } } } - .dataviews-view-list__primary-field { + .dataviews-view-list__title-field { flex: 1; min-height: $grid-unit-30; line-height: $grid-unit-30; From 7159a7857fb9106c4ecaa299220de91f0fff23c8 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 10 Dec 2024 16:01:26 +1100 Subject: [PATCH 220/444] Add stylebook screen for classic themes (#66851) Unlinked contributors: acketon. Co-authored-by: tellthemachines Co-authored-by: youknowriad Co-authored-by: ramonjd Co-authored-by: t-hamano Co-authored-by: jasmussen Co-authored-by: annezazu Co-authored-by: mtias Co-authored-by: carlomanf Co-authored-by: daveloodts Co-authored-by: cbirdsong Co-authored-by: fabiankaegy Co-authored-by: mrwweb Co-authored-by: ltrihan Co-authored-by: masteradhoc --- backport-changelog/6.8/7865.md | 3 + lib/block-editor-settings.php | 13 ++ lib/compat/wordpress-6.8/site-editor.php | 26 ++- .../src/components/maybe-editor/index.js | 58 +++++++ .../src/components/resizable-frame/index.js | 11 +- .../sidebar-navigation-screen-main/index.js | 105 +++++++----- .../index.js | 7 - .../src/components/site-editor-routes/home.js | 4 +- .../components/site-editor-routes/index.js | 2 + .../site-editor-routes/stylebook.js | 28 ++++ .../site-editor-routes/template-item.js | 6 +- .../site-editor-routes/template-part-item.js | 6 +- .../src/components/style-book/examples.tsx | 4 +- .../src/components/style-book/index.js | 155 ++++++++++++++---- .../src/components/style-book/style.scss | 4 + test/e2e/specs/site-editor/navigation.spec.js | 16 +- test/e2e/specs/site-editor/style-book.spec.js | 24 +++ 17 files changed, 375 insertions(+), 97 deletions(-) create mode 100644 backport-changelog/6.8/7865.md create mode 100644 packages/edit-site/src/components/maybe-editor/index.js create mode 100644 packages/edit-site/src/components/site-editor-routes/stylebook.js diff --git a/backport-changelog/6.8/7865.md b/backport-changelog/6.8/7865.md new file mode 100644 index 0000000000000..f7b23c944dc32 --- /dev/null +++ b/backport-changelog/6.8/7865.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7865 + +* https://github.com/WordPress/gutenberg/pull/66851 \ No newline at end of file diff --git a/lib/block-editor-settings.php b/lib/block-editor-settings.php index defd7cd391b16..6448eb2e52485 100644 --- a/lib/block-editor-settings.php +++ b/lib/block-editor-settings.php @@ -53,6 +53,13 @@ function gutenberg_get_block_editor_settings( $settings ) { $global_styles[] = $block_classes; } + // Get any additional css from the customizer and add it before global styles custom CSS. + $global_styles[] = array( + 'css' => wp_get_custom_css(), + '__unstableType' => 'user', + 'isGlobalStyles' => false, + ); + /* * Add the custom CSS as a separate stylesheet so any invalid CSS * entered by users does not break other global styles. @@ -74,6 +81,12 @@ function gutenberg_get_block_editor_settings( $settings ) { $block_classes['css'] = $actual_css; $global_styles[] = $block_classes; } + // Get any additional css from the customizer. + $global_styles[] = array( + 'css' => wp_get_custom_css(), + '__unstableType' => 'user', + 'isGlobalStyles' => false, + ); } $settings['styles'] = array_merge( $global_styles, get_block_editor_theme_styles() ); diff --git a/lib/compat/wordpress-6.8/site-editor.php b/lib/compat/wordpress-6.8/site-editor.php index cde108830b1d2..53d04c2e543f4 100644 --- a/lib/compat/wordpress-6.8/site-editor.php +++ b/lib/compat/wordpress-6.8/site-editor.php @@ -116,9 +116,33 @@ function gutenberg_redirect_site_editor_deprecated_urls() { * @return callable The default handler or a custom handler. */ function gutenberg_styles_wp_die_handler( $default_handler ) { - if ( ! wp_is_block_theme() && str_contains( $_SERVER['REQUEST_URI'], 'site-editor.php' ) && isset( $_GET['p'] ) ) { + if ( ! wp_is_block_theme() && str_contains( $_SERVER['REQUEST_URI'], 'site-editor.php' ) && current_user_can( 'edit_theme_options' ) ) { return '__return_false'; } return $default_handler; } add_filter( 'wp_die_handler', 'gutenberg_styles_wp_die_handler' ); + +/** + * Add a Styles submenu under the Appearance menu + * for Classic themes. + * + * @global array $submenu + */ +function gutenberg_add_styles_submenu_item() { + if ( ! wp_is_block_theme() && ( current_theme_supports( 'editor-styles' ) || wp_theme_has_theme_json() ) ) { + global $submenu; + + $styles_menu_item = array( + __( 'Design', 'gutenberg' ), + 'edit_theme_options', + 'site-editor.php', + ); + // If $submenu exists, insert the Styles submenu item at position 2. + if ( $submenu && isset( $submenu['themes.php'] ) ) { + // This might not work as expected if the submenu has already been modified. + array_splice( $submenu['themes.php'], 1, 1, array( $styles_menu_item ) ); + } + } +} +add_action( 'admin_init', 'gutenberg_add_styles_submenu_item' ); diff --git a/packages/edit-site/src/components/maybe-editor/index.js b/packages/edit-site/src/components/maybe-editor/index.js new file mode 100644 index 0000000000000..bee1c427c87b4 --- /dev/null +++ b/packages/edit-site/src/components/maybe-editor/index.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ + +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ + +import Editor from '../editor'; + +export function MaybeEditor( { showEditor = true } ) { + const { isBlockBasedTheme, siteUrl } = useSelect( ( select ) => { + const { getEntityRecord, getCurrentTheme } = select( coreStore ); + const siteData = getEntityRecord( 'root', '__unstableBase' ); + + return { + isBlockBasedTheme: getCurrentTheme()?.is_block_theme, + siteUrl: siteData?.home, + }; + }, [] ); + + // If theme is block based, return the Editor, otherwise return the site preview. + return isBlockBasedTheme || showEditor ? ( + + ) : ( + ); }; const Examples = memo( - ( { className, examples, category, label, isSelected, onSelect } ) => { - const categoryDefinition = category - ? getTopLevelStyleBookCategories().find( - ( _category ) => _category.slug === category - ) - : null; - - const filteredExamples = categoryDefinition - ? getExamplesByCategory( categoryDefinition, examples ) - : { examples }; - + ( { className, filteredExamples, label, isSelected, onSelect } ) => { return ( Date: Mon, 23 Dec 2024 10:04:59 +0800 Subject: [PATCH 439/444] Fix trailing spaces on navigation block classnames (#68161) Co-authored-by: kurudrive Co-authored-by: talldan Co-authored-by: luisherranz Co-authored-by: ramonjd --- packages/block-library/src/navigation/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 9a56e399fcfec..43ca833153427 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -539,8 +539,8 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $inner_blocks_html, $toggle_aria_label_open, $toggle_aria_label_close, - esc_attr( implode( ' ', $responsive_container_classes ) ), - esc_attr( implode( ' ', $open_button_classes ) ), + esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ), + esc_attr( trim( implode( ' ', $open_button_classes ) ) ), ( ! empty( $overlay_inline_styles ) ) ? "style=\"$overlay_inline_styles\"" : '', $toggle_button_content, $toggle_close_button_content, From 0fb8403108d1490d7a62453595bea76a7ddd9b2e Mon Sep 17 00:00:00 2001 From: timse201 Date: Mon, 23 Dec 2024 04:43:58 +0100 Subject: [PATCH 440/444] split upload into verbs and nouns (#68227) * split upload * split upload * split upload * split upload * split upload * Add missing imports for _x function --------- Co-authored-by: timse201 Co-authored-by: talldan --- .../block-editor/src/components/media-placeholder/index.js | 6 +++--- .../block-editor/src/components/media-replace-flow/index.js | 4 ++-- packages/block-library/src/video/tracks-editor.js | 2 +- .../components/global-styles/font-library-modal/index.js | 2 +- .../src/components/post-publish-panel/maybe-upload-media.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 0cbc6c8c26203..b19411893b86c 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -15,7 +15,7 @@ import { __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper, withFilters, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { useState, useEffect } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { keyboardReturn } from '@wordpress/icons'; @@ -482,7 +482,7 @@ export function MediaPlaceholder( { ) } onClick={ openFileDialog } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } { uploadMediaLibraryButton } { renderUrlSelectionUI() } @@ -512,7 +512,7 @@ export function MediaPlaceholder( { 'block-editor-media-placeholder__upload-button' ) } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } ) } onChange={ onUpload } diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index 0da15033a86bf..dd6000a5f6845 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -7,7 +7,7 @@ import clsx from 'clsx'; * WordPress dependencies */ import { useRef } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import { FormFileUpload, @@ -188,7 +188,7 @@ const MediaReplaceFlow = ( { openFileDialog(); } } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } ); } } diff --git a/packages/block-library/src/video/tracks-editor.js b/packages/block-library/src/video/tracks-editor.js index 33036a14f1fec..a0152885f5567 100644 --- a/packages/block-library/src/video/tracks-editor.js +++ b/packages/block-library/src/video/tracks-editor.js @@ -323,7 +323,7 @@ export default function TracksEditor( { tracks = [], onChange } ) { openFileDialog(); } } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } ); } } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index 27093e0ef1cbb..5661a002f71ec 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -28,7 +28,7 @@ const DEFAULT_TAB = { const UPLOAD_TAB = { id: 'upload-fonts', - title: __( 'Upload' ), + title: _x( 'Upload', 'noun' ), }; const tabsFromCollections = ( collections ) => diff --git a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js index 32ea69c425e0b..a92a479415434 100644 --- a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js +++ b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js @@ -9,7 +9,7 @@ import { __unstableAnimatePresence as AnimatePresence, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState } from '@wordpress/element'; import { isBlobURL } from '@wordpress/blob'; @@ -260,7 +260,7 @@ export default function MaybeUploadMediaPanel() { variant="primary" onClick={ uploadImages } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } ) }
    From 068660c154b3a4d8f11b53b9e0819b5f82bfcfb3 Mon Sep 17 00:00:00 2001 From: Eshaan Dabasiya <76681468+im3dabasia@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:55:24 +0530 Subject: [PATCH 441/444] fix: use dropdown menu props (#68015) Co-authored-by: im3dabasia Co-authored-by: fabiankaegy --- packages/block-library/src/navigation-submenu/edit.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index dbdbd23b13b2f..315169f2736ad 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -44,6 +44,7 @@ import { getColors, getNavigationChildBlockProps, } from '../navigation/edit/utils'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const ALLOWED_BLOCKS = [ 'core/navigation-link', @@ -153,6 +154,7 @@ export default function NavigationSubmenuEdit( { const isDraggingWithin = useIsDraggingWithin( listItemRef ); const itemLabelPlaceholder = __( 'Add text…' ); const ref = useRef(); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const { parentCount, @@ -394,6 +396,7 @@ export default function NavigationSubmenuEdit( { rel: '', } ); } } + dropdownMenuProps={ dropdownMenuProps } > Date: Mon, 23 Dec 2024 12:40:29 +0700 Subject: [PATCH 442/444] Stylebook: add the Appearance -> Design submenu through `admin_menu` action (#68174) * Stylebook: add the Appearance -> Design submenu through admin_menu action * Add backport reference Co-authored-by: fushar Co-authored-by: ramonjd --- backport-changelog/6.8/7865.md | 3 ++- lib/compat/wordpress-6.8/site-editor.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backport-changelog/6.8/7865.md b/backport-changelog/6.8/7865.md index f7b23c944dc32..b5de24b8ee63d 100644 --- a/backport-changelog/6.8/7865.md +++ b/backport-changelog/6.8/7865.md @@ -1,3 +1,4 @@ https://github.com/WordPress/wordpress-develop/pull/7865 -* https://github.com/WordPress/gutenberg/pull/66851 \ No newline at end of file +* https://github.com/WordPress/gutenberg/pull/66851 +* https://github.com/WordPress/gutenberg/pull/68174 diff --git a/lib/compat/wordpress-6.8/site-editor.php b/lib/compat/wordpress-6.8/site-editor.php index 53d04c2e543f4..9b2575676047d 100644 --- a/lib/compat/wordpress-6.8/site-editor.php +++ b/lib/compat/wordpress-6.8/site-editor.php @@ -145,4 +145,4 @@ function gutenberg_add_styles_submenu_item() { } } } -add_action( 'admin_init', 'gutenberg_add_styles_submenu_item' ); +add_action( 'admin_menu', 'gutenberg_add_styles_submenu_item' ); From 127eba0d098677fd90e007c293e5e7b3913cfe5b Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 23 Dec 2024 09:13:53 +0200 Subject: [PATCH 443/444] Storybook: Fix emotion/is-prop-valid warning (#68202) Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> --- package-lock.json | 27 ++++++++++++++------------- package.json | 1 + 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd901baca24a9..6471460ae535d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@babel/runtime-corejs3": "7.25.7", "@babel/traverse": "7.25.7", "@emotion/babel-plugin": "11.11.0", + "@emotion/is-prop-valid": "1.2.2", "@emotion/jest": "11.7.1", "@emotion/native": "11.0.0", "@geometricpanda/storybook-addon-badges": "2.0.5", @@ -4467,6 +4468,19 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, "node_modules/@emotion/jest": { "version": "11.7.1", "resolved": "https://registry.npmjs.org/@emotion/jest/-/jest-11.7.1.tgz", @@ -4618,19 +4632,6 @@ } } }, - "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/styled/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", diff --git a/package.json b/package.json index 792546d28582f..de73148bd6245 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@babel/runtime-corejs3": "7.25.7", "@babel/traverse": "7.25.7", "@emotion/babel-plugin": "11.11.0", + "@emotion/is-prop-valid": "1.2.2", "@emotion/jest": "11.7.1", "@emotion/native": "11.0.0", "@geometricpanda/storybook-addon-badges": "2.0.5", From 5bc955d182dc08c98dd07a5afcf1f54728ef0d19 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:32:24 +0200 Subject: [PATCH 444/444] postcss-plugins-preset: bump autoprefixer to 10.4.20 (#68237) * postcss-plugins-preset: bump autoprefixer to 10.4.20 * CHANGELOG Co-authored-by: tyxla Co-authored-by: gziolo --- package-lock.json | 93 ++++++++++++-------- packages/postcss-plugins-preset/CHANGELOG.md | 4 + packages/postcss-plugins-preset/package.json | 2 +- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6471460ae535d..eb2059e151f53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17514,43 +17514,6 @@ "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", "dev": true }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/autoprefixer/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, "node_modules/autosize": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.2.tgz", @@ -25666,6 +25629,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, "engines": { "node": "*" }, @@ -51968,7 +51932,7 @@ "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/base-styles": "*", - "autoprefixer": "^10.2.5" + "autoprefixer": "^10.4.20" }, "engines": { "node": ">=18.12.0", @@ -51978,6 +51942,59 @@ "postcss": "^8.0.0" } }, + "packages/postcss-plugins-preset/node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "packages/postcss-plugins-preset/node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "packages/postcss-plugins-preset/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", "version": "6.14.0", diff --git a/packages/postcss-plugins-preset/CHANGELOG.md b/packages/postcss-plugins-preset/CHANGELOG.md index b1425332402f5..f0e18614b0bdc 100644 --- a/packages/postcss-plugins-preset/CHANGELOG.md +++ b/packages/postcss-plugins-preset/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- The bundled `autoprefixer` dependency has been updated from requiring `^10.2.5` to requiring `^10.4.20` (see [#68237](https://github.com/WordPress/gutenberg/pull/68237)). + ## 5.14.0 (2024-12-11) ## 5.13.0 (2024-11-27) diff --git a/packages/postcss-plugins-preset/package.json b/packages/postcss-plugins-preset/package.json index caecfb0939863..aba3012f4cd6b 100644 --- a/packages/postcss-plugins-preset/package.json +++ b/packages/postcss-plugins-preset/package.json @@ -31,7 +31,7 @@ "main": "lib/index.js", "dependencies": { "@wordpress/base-styles": "*", - "autoprefixer": "^10.2.5" + "autoprefixer": "^10.4.20" }, "peerDependencies": { "postcss": "^8.0.0"