From 478ea734e45b3e4b947b17f7bfd1d8ad80ed1670 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:18:49 +0100 Subject: [PATCH 01/34] Query Loop - Add accesibility markup at the end of the loop in all cases. (#55890) * Use tagName if exists * Added a test using string positions, we could refactor * Refactor to use Tag Processor --- packages/block-library/src/query/index.php | 8 +++- phpunit/blocks/render-query-test.php | 43 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 201ceed737a12a..b6a5733632ff44 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -44,7 +44,11 @@ function render_block_core_query( $attributes, $content, $block ) { $block->block_type->supports['interactivity'] = true; // Add a div to announce messages using `aria-live`. - $last_div_position = strripos( $content, '' ); + $html_tag = 'div'; + if ( ! empty( $attributes['tagName'] ) ) { + $html_tag = esc_attr( $attributes['tagName'] ); + } + $last_tag_position = strripos( $content, '' ); $content = substr_replace( $content, '
', - $last_div_position, + $last_tag_position, 0 ); } diff --git a/phpunit/blocks/render-query-test.php b/phpunit/blocks/render-query-test.php index 8a148e89be87bf..2d81bfdb513b33 100644 --- a/phpunit/blocks/render-query-test.php +++ b/phpunit/blocks/render-query-test.php @@ -131,6 +131,49 @@ public function test_rendering_query_with_enhanced_pagination_auto_disabled_when $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) ); } + + /** + * Tests that the `core/query` last tag is rendered with the tagName attribute + * if is defined, having a div as default. + */ + public function test_enhanced_query_markup_rendering_at_bottom_on_custom_html_element_tags() { + global $wp_query, $wp_the_query; + + $content = << + + + +HTML; + + // Set main query to single post. + $wp_query = new WP_Query( + array( + 'posts_per_page' => 1, + ) + ); + + $wp_the_query = $wp_query; + + $output = do_blocks( $content ); + + $p = new WP_HTML_Tag_Processor( $output ); + + $p->next_tag( 'span' ); + + // Test that there is a div added just after the last tag inside the aside. + $this->assertSame( $p->next_tag(), true ); + // Test that that div is the accesibility one. + $this->assertSame( 'screen-reader-text', $p->get_attribute( 'class' ) ); + $this->assertSame( 'context.core.query.message', $p->get_attribute( 'data-wp-text' ) ); + $this->assertSame( 'polite', $p->get_attribute( 'aria-live' ) ); + } + /** * Tests that the `core/query` block adds an extra attribute to disable the * enhanced pagination in the browser when a post content block is found inside. From 827cc8317f9479afd5cb80550f4ee89ee2aea88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:39:09 +0100 Subject: [PATCH 02/34] DataViews: make items per page an even number (#55906) --- packages/edit-site/src/components/dataviews/view-actions.js | 2 +- .../edit-site/src/components/sidebar-dataviews/default-views.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/dataviews/view-actions.js b/packages/edit-site/src/components/dataviews/view-actions.js index 035ab6e36facf2..cb6aa022cf9a50 100644 --- a/packages/edit-site/src/components/dataviews/view-actions.js +++ b/packages/edit-site/src/components/dataviews/view-actions.js @@ -86,7 +86,7 @@ function ViewTypeMenu( { view, onChangeView } ) { ); } -const PAGE_SIZE_VALUES = [ 5, 20, 50 ]; +const PAGE_SIZE_VALUES = [ 20, 50, 100 ]; function PageSizeMenu( { view, onChangeView } ) { return ( Date: Tue, 7 Nov 2023 11:15:45 +0100 Subject: [PATCH 03/34] Fix Inaccurate description of the Show icon button setting in Nav block (#55429) * Navigation: Fix Inaccurate description of the Show icon button setting * Update overlay-menu-preview.js Co-authored-by: Dave Smith --------- Co-authored-by: Dave Smith --- .../block-library/src/navigation/edit/overlay-menu-preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/overlay-menu-preview.js b/packages/block-library/src/navigation/edit/overlay-menu-preview.js index adb1377a604ede..8ea07351821c89 100644 --- a/packages/block-library/src/navigation/edit/overlay-menu-preview.js +++ b/packages/block-library/src/navigation/edit/overlay-menu-preview.js @@ -20,7 +20,7 @@ export default function OverlayMenuPreview( { setAttributes, hasIcon, icon } ) { __nextHasNoMarginBottom label={ __( 'Show icon button' ) } help={ __( - 'Configure the visual appearance of the button opening the overlay menu.' + 'Configure the visual appearance of the button that toggles the overlay menu.' ) } onChange={ ( value ) => setAttributes( { hasIcon: value } ) } checked={ hasIcon } From 11df44a7aa2d705958e740cabbd0865798fdc2ec Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 7 Nov 2023 12:28:11 +0200 Subject: [PATCH 04/34] [Data views]: Make used taxonomy private (#55918) --- lib/experimental/data-views.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/data-views.php b/lib/experimental/data-views.php index e0346184ffc21c..e9fb2134f3b39c 100644 --- a/lib/experimental/data-views.php +++ b/lib/experimental/data-views.php @@ -40,7 +40,7 @@ function _gutenberg_register_data_views_post_type() { 'wp_dataviews_type', array( 'wp_dataviews' ), array( - 'public' => true, + 'public' => false, 'hierarchical' => false, 'labels' => array( 'name' => __( 'Dataview types', 'gutenberg' ), From 3e4c0530d80737e46da1682d4855c202a0bb26c8 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 7 Nov 2023 12:45:53 +0200 Subject: [PATCH 05/34] [Site editor]: Add edit page slug field (#55767) * [Site editor]: Add edit page slug field * address feedback * use `getEditedEntityRecord` * fallback to generated slug and use proper permalink * clean slug on blur to allow empty spaces * rename function * Combine selectors; there's no need to create two subscriptions to the same store * Avoid making the two HTTP requests for the same record * Remove redundant target blank * Handle case when the dropdown is closed without triggering a blur event * address feedback * update design by using InputControl * remove prefix * add form in order to close on Enter --------- Co-authored-by: George Mamadashvili --- .../page-panels/page-slug.js | 161 ++++++++++++++++++ .../page-panels/page-summary.js | 2 + .../sidebar-edit-mode/page-panels/style.scss | 7 + 3 files changed, 170 insertions(+) create mode 100644 packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js new file mode 100644 index 00000000000000..d6ffa1991333ec --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js @@ -0,0 +1,161 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { + safeDecodeURIComponent, + filterURLForDisplay, + cleanForSlug, +} from '@wordpress/url'; +import { useState, useMemo } from '@wordpress/element'; +import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { + __experimentalInputControl as InputControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalText as Text, + Dropdown, + Button, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; + +export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; + +function getPostPermalink( record, isEditable ) { + if ( ! record?.permalink_template ) { + return; + } + const slug = record?.slug || record?.generated_slug; + const [ prefix, suffix ] = record.permalink_template.split( + PERMALINK_POSTNAME_REGEX + ); + const permalink = isEditable ? prefix + slug + suffix : record.link; + return filterURLForDisplay( safeDecodeURIComponent( permalink ) ); +} + +export default function PageSlug( { postType, postId } ) { + const { editEntityRecord } = useDispatch( coreStore ); + const { record, savedSlug } = useSelect( + ( select ) => { + const { getEntityRecord, getEditedEntityRecord } = + select( coreStore ); + const savedRecord = getEntityRecord( 'postType', postType, postId ); + return { + record: getEditedEntityRecord( 'postType', postType, postId ), + savedSlug: savedRecord?.slug || savedRecord?.generated_slug, + }; + }, + [ postType, postId ] + ); + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); + const [ forceEmptyField, setForceEmptyField ] = useState( false ); + const isEditable = + PERMALINK_POSTNAME_REGEX.test( record?.permalink_template ) && + record?._links?.[ 'wp:action-publish' ]; + const popoverProps = useMemo( + () => ( { + // Anchor the popover to the middle of the entire row so that it doesn't + // move around when the label changes. + anchor: popoverAnchor, + 'aria-label': __( 'Change slug' ), + placement: 'bottom-end', + } ), + [ popoverAnchor ] + ); + if ( ! record || ! isEditable ) { + return null; + } + const recordSlug = safeDecodeURIComponent( + record?.slug || record?.generated_slug + ); + const permaLink = getPostPermalink( record, isEditable ); + const onSlugChange = ( newValue ) => { + editEntityRecord( 'postType', postType, postId, { + slug: newValue, + } ); + }; + return ( + + + { __( 'URL' ) } + + { + if ( forceEmptyField ) { + onSlugChange( cleanForSlug( savedSlug ) ); + setForceEmptyField( false ); + } + } } + renderToggle={ ( { onToggle } ) => ( + + ) } + renderContent={ ( { onClose } ) => { + return ( + <> + + +
+ { + onSlugChange( 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 ) => { + onSlugChange( + cleanForSlug( + event.target.value || + savedSlug + ) + ); + if ( forceEmptyField ) { + setForceEmptyField( false ); + } + } } + /> + +
+ + ); + } } + /> +
+ ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js index c4dafeab6cb372..25b69985bcbd6e 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js @@ -8,6 +8,7 @@ import { __experimentalVStack as VStack } from '@wordpress/components'; import PageStatus from './page-status'; import PublishDate from './publish-date'; import EditTemplate from './edit-template'; +import PageSlug from './page-slug'; export default function PageSummary( { status, @@ -32,6 +33,7 @@ export default function PageSummary( { postType={ postType } /> + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index 5501fe49e5876b..e1a8e4acb72273 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -85,3 +85,10 @@ min-width: 240px; } } + +.edit-site-page-panels-edit-slug__dropdown { + .components-popover__content { + min-width: 320px; + padding: $grid-unit-20; + } +} From 38726abc4dcd3aaeb3eed4c352a615316746fee2 Mon Sep 17 00:00:00 2001 From: Pooja Killekar <41000648+pooja-muchandikar@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:09:55 +0530 Subject: [PATCH 06/34] Migrate Child Block Test to Playwright (#55199) * Migrate Child Block Test to Playwright * Fix Stylelint failing and modified the test case * Fix failed CI * Update locators * Address Feedback * Address feedbacks * Address feedbacks * Update test case * Update test case --- .../specs/editor/plugins/child-blocks.test.js | 66 ------------- .../specs/editor/plugins/child-blocks.spec.js | 97 +++++++++++++++++++ 2 files changed, 97 insertions(+), 66 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/plugins/child-blocks.test.js create mode 100644 test/e2e/specs/editor/plugins/child-blocks.spec.js diff --git a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js deleted file mode 100644 index c7ca368003397e..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - closeGlobalBlockInserter, - createNewPost, - deactivatePlugin, - getAllBlockInserterItemTitles, - insertBlock, - openGlobalBlockInserter, -} from '@wordpress/e2e-test-utils'; - -describe( 'Child Blocks', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-child-blocks' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-child-blocks' ); - } ); - - it( 'are hidden from the global block inserter', async () => { - await openGlobalBlockInserter(); - await expect( await getAllBlockInserterItemTitles() ).not.toContain( - 'Child Blocks Child' - ); - } ); - - it( 'shows up in a parent block', async () => { - await insertBlock( 'Child Blocks Unrestricted Parent' ); - await closeGlobalBlockInserter(); - await page.waitForSelector( - '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' - ); - await page.click( - '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' - ); - await openGlobalBlockInserter(); - const inserterItemTitles = await getAllBlockInserterItemTitles(); - expect( inserterItemTitles ).toContain( 'Child Blocks Child' ); - expect( inserterItemTitles.length ).toBeGreaterThan( 20 ); - } ); - - it( 'display in a parent block with allowedItems', async () => { - await insertBlock( 'Child Blocks Restricted Parent' ); - await closeGlobalBlockInserter(); - await page.waitForSelector( - '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' - ); - await page.click( - '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' - ); - await openGlobalBlockInserter(); - const allowedBlocks = await getAllBlockInserterItemTitles(); - expect( allowedBlocks.sort() ).toEqual( [ - 'Child Blocks Child', - 'Image', - 'Paragraph', - ] ); - } ); -} ); diff --git a/test/e2e/specs/editor/plugins/child-blocks.spec.js b/test/e2e/specs/editor/plugins/child-blocks.spec.js new file mode 100644 index 00000000000000..b3073b70a5409a --- /dev/null +++ b/test/e2e/specs/editor/plugins/child-blocks.spec.js @@ -0,0 +1,97 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Child Blocks', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-child-blocks' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-child-blocks' ); + } ); + + test( 'are hidden from the global block inserter', async ( { page } ) => { + const blockInserter = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockInserter.click(); + await expect( blockLibrary ).toBeVisible(); + expect( blockLibrary.getByRole( 'option' ) ).not.toContain( [ + 'Child Blocks Child', + ] ); + } ); + + test( 'shows up in a parent block', async ( { page, editor } ) => { + await editor.insertBlock( { + name: 'test/child-blocks-unrestricted-parent', + } ); + + await page + .getByRole( 'document', { + name: 'Block: Child Blocks Unrestricted Parent', + } ) + .getByRole( 'button', { + name: 'Add default block', + } ) + .click(); + + const blockInserter = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockInserter.click(); + await expect( blockLibrary ).toBeVisible(); + await expect( blockLibrary.getByRole( 'option' ) ).toContainText( [ + 'Child Blocks Child', + ] ); + expect( + await blockLibrary.getByRole( 'option' ).count() + ).toBeGreaterThan( 10 ); + } ); + + test( 'display in a parent block with allowedItems', async ( { + page, + editor, + } ) => { + await editor.insertBlock( { + name: 'test/child-blocks-restricted-parent', + } ); + + await page + .getByRole( 'document', { + name: 'Block: Child Blocks Restricted Parent', + } ) + .getByRole( 'button', { + name: 'Add default block', + } ) + .click(); + + const blockInserter = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockInserter.click(); + await expect( blockLibrary ).toBeVisible(); + await expect( blockLibrary.getByRole( 'option' ) ).toHaveText( [ + 'Paragraph', + 'Child Blocks Child', + 'Image', + ] ); + } ); +} ); From 137ad68e41b0ed2e48d7d70f3428cc4245a856a4 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:55:30 +0900 Subject: [PATCH 07/34] Edit Post: Fix pattern modal reopening when making the title empty again (#55873) * Fix pattern modal reopening when making the title empty again * Use boolean value --- .../components/start-page-options/index.js | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/edit-post/src/components/start-page-options/index.js b/packages/edit-post/src/components/start-page-options/index.js index 02473fd4eaa148..77264d27a5e7df 100644 --- a/packages/edit-post/src/components/start-page-options/index.js +++ b/packages/edit-post/src/components/start-page-options/index.js @@ -3,7 +3,7 @@ */ import { Modal } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useState, useMemo } from '@wordpress/element'; import { store as blockEditorStore, __experimentalBlockPatternsList as BlockPatternsList, @@ -62,19 +62,11 @@ function PatternSelection( { blockPatterns, onChoosePattern } ) { ); } -function StartPageOptionsModal() { - const [ modalState, setModalState ] = useState( 'initial' ); +function StartPageOptionsModal( { onClose } ) { const startPatterns = useStartPatterns(); const hasStartPattern = startPatterns.length > 0; - const shouldOpenModal = hasStartPattern && modalState === 'initial'; - useEffect( () => { - if ( shouldOpenModal ) { - setModalState( 'open' ); - } - }, [ shouldOpenModal ] ); - - if ( modalState !== 'open' ) { + if ( ! hasStartPattern ) { return null; } @@ -83,12 +75,12 @@ function StartPageOptionsModal() { className="edit-post-start-page-options__modal" title={ __( 'Choose a pattern' ) } isFullScreen - onRequestClose={ () => setModalState( 'closed' ) } + onRequestClose={ onClose } >
setModalState( 'closed' ) } + onChoosePattern={ onClose } />
@@ -96,6 +88,7 @@ function StartPageOptionsModal() { } export default function StartPageOptions() { + const [ isClosed, setIsClosed ] = useState( false ); const shouldEnableModal = useSelect( ( select ) => { const { isCleanNewPost } = select( editorStore ); const { isEditingTemplate, isFeatureActive } = select( editPostStore ); @@ -107,9 +100,9 @@ export default function StartPageOptions() { ); }, [] ); - if ( ! shouldEnableModal ) { + if ( ! shouldEnableModal || isClosed ) { return null; } - return ; + return setIsClosed( true ) } />; } From 837374ecb19c3342bb4e88073c6d7ee00f35b694 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Tue, 7 Nov 2023 12:12:54 +0000 Subject: [PATCH 08/34] Perf Tests: Stabilise the Site Editor metrics (#55922) --- .../src/admin/visit-site-editor.ts | 4 +- test/performance/specs/site-editor.spec.js | 73 +++++-------------- 2 files changed, 22 insertions(+), 55 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index da21f17aade117..5883a2b92a5bcc 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -47,7 +47,9 @@ export async function visitSiteEditor( * loading is done. */ await this.page - .locator( '.edit-site-canvas-loader' ) + // Spinner was used instead of the progress bar in an earlier version of + // the site editor. + .locator( '.edit-site-canvas-loader, .edit-site-canvas-spinner' ) // Bigger timeout is needed for larger entities, for example the large // post html fixture that we load for performance tests, which often // doesn't make it under the default 10 seconds. diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index bd1a5f0b87cc01..38bcceb14edd61 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -58,19 +58,13 @@ test.describe( 'Site Editor Performance', () => { } ); test.describe( 'Loading', () => { - let draftURL = null; + let draftId = null; - test( 'Setup the test page', async ( { page, admin, perfUtils } ) => { + test( 'Setup the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); await perfUtils.loadBlocksForLargePost(); - await perfUtils.saveDraft(); - await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), - postType: 'page', - } ); - - draftURL = page.url(); + draftId = await perfUtils.saveDraft(); } ); const samples = 10; @@ -78,15 +72,18 @@ test.describe( 'Site Editor Performance', () => { const iterations = samples + throwaway; for ( let i = 1; i <= iterations; i++ ) { test( `Run the test (${ i } of ${ iterations })`, async ( { - page, + admin, perfUtils, metrics, } ) => { // Go to the test draft. - await page.goto( draftURL ); - const canvas = await perfUtils.getCanvas(); + await admin.visitSiteEditor( { + postId: draftId, + postType: 'page', + } ); // Wait for the first block. + const canvas = await perfUtils.getCanvas(); await canvas.locator( '.wp-block' ).first().waitFor(); // Get the durations. @@ -109,44 +106,25 @@ test.describe( 'Site Editor Performance', () => { } ); test.describe( 'Typing', () => { - let draftURL = null; - - test( 'Setup the test post', async ( { - page, - admin, - editor, - perfUtils, - } ) => { + let draftId = null; + + test( 'Setup the test post', async ( { admin, editor, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); await perfUtils.loadBlocksForLargePost(); await editor.insertBlock( { name: 'core/paragraph' } ); - await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); + } ); + + test( 'Run the test', async ( { admin, perfUtils, metrics } ) => { + // Go to the test draft. await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), + postId: draftId, postType: 'page', } ); - draftURL = page.url(); - } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); - await perfUtils.disableAutosave(); - - // Wait for the loader overlay to disappear. This is necessary - // because the overlay is still visible for a while after the editor - // canvas is ready, and we don't want it to affect the typing - // timings. - await page - .locator( - // Spinner was used instead of the progress bar in an earlier version of the site editor. - '.edit-site-canvas-loader, .edit-site-canvas-spinner' - ) - .waitFor( { state: 'hidden' } ); - - const canvas = await perfUtils.getCanvas(); - // Enter edit mode (second click is needed for the legacy edit mode). + const canvas = await perfUtils.getCanvas(); await canvas.locator( 'body' ).click(); await canvas .getByRole( 'document', { name: /Block:( Post)? Content/ } ) @@ -210,19 +188,6 @@ test.describe( 'Site Editor Performance', () => { path: '/wp_template', } ); - // Wait for the loader overlay to disappear. This is necessary - // because the overlay is still visible for a while after the editor - // canvas is ready, and we don't want it to affect the typing - // timings. - await page - .locator( - // Spinner was used instead of the progress bar in an earlier version of the site editor. - '.edit-site-canvas-loader, .edit-site-canvas-spinner' - ) - .waitFor( { state: 'hidden' } ); - // Additional time to ensure the browser is completely idle. - // eslint-disable-next-line playwright/no-wait-for-timeout - // Start tracing. await metrics.startTracing(); From 76356caba6e39bfc8726069efb416ead6b3e3558 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 7 Nov 2023 13:24:16 +0100 Subject: [PATCH 09/34] Core Data: Move the template lookup to core-data selectors/resolvers (#55883) --- docs/reference-guides/data/data-core.md | 26 +++++++++++++++++++ packages/core-data/README.md | 26 +++++++++++++++++++ packages/core-data/src/actions.js | 16 ++++++++++++ packages/core-data/src/reducer.js | 21 +++++++++++++++ packages/core-data/src/resolvers.js | 11 ++++++++ packages/core-data/src/selectors.ts | 22 ++++++++++++++++ .../start-template-options/index.js | 26 ++++++++++--------- packages/edit-site/src/store/actions.js | 20 ++++++++++---- 8 files changed, 151 insertions(+), 17 deletions(-) diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 8a190869f99e78..ea97ce28e4d85c 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -150,6 +150,19 @@ _Returns_ - `undefined< 'edit' >`: Current user object. +### getDefaultTemplateId + +Returns the default template use to render a given query. + +_Parameters_ + +- _state_ `State`: Data state. +- _query_ `TemplateQuery`: Query. + +_Returns_ + +- `string`: The default template id for the given query. + ### getEditedEntityRecord Returns the specified entity record, merged with its edits. @@ -648,6 +661,19 @@ _Returns_ - `Object`: Action object. +### receiveDefaultTemplateId + +Returns an action object used to set the template for a given query. + +_Parameters_ + +- _query_ `Object`: The lookup query. +- _templateId_ `string`: The resolved template id. + +_Returns_ + +- `Object`: Action object. + ### receiveEntityRecords Returns an action object used in signalling that entity records have been received. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index a20e86e9695a26..ef5d9c1197f099 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -205,6 +205,19 @@ _Returns_ - `Object`: Action object. +### receiveDefaultTemplateId + +Returns an action object used to set the template for a given query. + +_Parameters_ + +- _query_ `Object`: The lookup query. +- _templateId_ `string`: The resolved template id. + +_Returns_ + +- `Object`: Action object. + ### receiveEntityRecords Returns an action object used in signalling that entity records have been received. @@ -444,6 +457,19 @@ _Returns_ - `undefined< 'edit' >`: Current user object. +### getDefaultTemplateId + +Returns the default template use to render a given query. + +_Parameters_ + +- _state_ `State`: Data state. +- _query_ `TemplateQuery`: Query. + +_Returns_ + +- `string`: The default template id for the given query. + ### getEditedEntityRecord Returns the specified entity record, merged with its edits. diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index c4b19819ed7a41..9e7277f35a62a7 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -908,3 +908,19 @@ export function receiveNavigationFallbackId( fallbackId ) { fallbackId, }; } + +/** + * Returns an action object used to set the template for a given query. + * + * @param {Object} query The lookup query. + * @param {string} templateId The resolved template id. + * + * @return {Object} Action object. + */ +export function receiveDefaultTemplateId( query, templateId ) { + return { + type: 'RECEIVE_DEFAULT_TEMPLATE', + query, + templateId, + }; +} diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 68c0cc233d7b68..a21623d8ba89d3 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -572,6 +572,26 @@ export function themeGlobalStyleRevisions( state = {}, action ) { return state; } +/** + * Reducer managing the template lookup per query. + * + * @param {Record} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Record} Updated state. + */ +export function defaultTemplates( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_DEFAULT_TEMPLATE': + return { + ...state, + [ JSON.stringify( action.query ) ]: action.templateId, + }; + } + + return state; +} + export default combineReducers( { terms, users, @@ -592,4 +612,5 @@ export default combineReducers( { blockPatternCategories, userPatternCategories, navigationFallbackId, + defaultTemplates, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 5fc7cd14f35c0b..cd2a65a60b0139 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -707,3 +707,14 @@ export const getNavigationFallbackId = ] ); } }; + +export const getDefaultTemplateId = + ( query ) => + async ( { dispatch } ) => { + const template = await apiFetch( { + path: addQueryArgs( '/wp/v2/templates/lookup', query ), + } ); + if ( template ) { + dispatch.receiveDefaultTemplateId( query, template.id ); + } + }; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index b6b36fad2ee934..2a046941611c7d 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -46,6 +46,7 @@ export interface State { users: UserState; navigationFallbackId: EntityRecordKey; userPatternCategories: Array< UserPatternCategory >; + defaultTemplates: Record< string, string >; } type EntityRecordKey = string | number; @@ -81,6 +82,12 @@ interface UserState { byId: Record< EntityRecordKey, ET.User< 'edit' > >; } +type TemplateQuery = { + slug?: string; + is_custom?: boolean; + ignore_empty?: boolean; +}; + export interface UserPatternCategory { id: number; name: string; @@ -1351,3 +1358,18 @@ export function getCurrentThemeGlobalStylesRevisions( return state.themeGlobalStyleRevisions[ currentGlobalStylesId ]; } + +/** + * Returns the default template use to render a given query. + * + * @param state Data state. + * @param query Query. + * + * @return The default template id for the given query. + */ +export function getDefaultTemplateId( + state: State, + query: TemplateQuery +): string { + return state.defaultTemplates[ JSON.stringify( query ) ]; +} diff --git a/packages/edit-site/src/components/start-template-options/index.js b/packages/edit-site/src/components/start-template-options/index.js index fe4179a338504b..771f380db05a3d 100644 --- a/packages/edit-site/src/components/start-template-options/index.js +++ b/packages/edit-site/src/components/start-template-options/index.js @@ -3,7 +3,7 @@ */ import { Modal, Flex, FlexItem, Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useState, useMemo } from '@wordpress/element'; import { __experimentalBlockPatternsList as BlockPatternsList, store as blockEditorStore, @@ -13,8 +13,6 @@ import { useAsyncList } from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; import { parse } from '@wordpress/blocks'; import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data'; -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -23,18 +21,22 @@ import { store as editSiteStore } from '../../store'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; function useFallbackTemplateContent( slug, isCustom = false ) { - const [ templateContent, setTemplateContent ] = useState( '' ); - - useEffect( () => { - apiFetch( { - path: addQueryArgs( '/wp/v2/templates/lookup', { + return useSelect( + ( select ) => { + const { getEntityRecord, getDefaultTemplateId } = + select( coreStore ); + const templateId = getDefaultTemplateId( { slug, is_custom: isCustom, ignore_empty: true, - } ), - } ).then( ( { content } ) => setTemplateContent( content.raw ) ); - }, [ isCustom, slug ] ); - return templateContent; + } ); + return templateId + ? getEntityRecord( 'postType', TEMPLATE_POST_TYPE, templateId ) + ?.content?.raw + : undefined; + }, + [ slug, isCustom ] + ); } function useStartPatterns( fallbackContent ) { diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index ce698a757f6bb3..30ee9e6aab01af 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -266,12 +266,22 @@ export const setPage = ( page ) => async ( { dispatch, registry } ) => { let template; - const getDefaultTemplate = async ( slug ) => - apiFetch( { - path: addQueryArgs( '/wp/v2/templates/lookup', { + const getDefaultTemplate = async ( slug ) => { + const templateId = await registry + .resolveSelect( coreStore ) + .getDefaultTemplateId( { slug: `page-${ slug }`, - } ), - } ); + } ); + return templateId + ? await registry + .resolveSelect( coreStore ) + .getEntityRecord( + 'postType', + TEMPLATE_POST_TYPE, + templateId + ) + : undefined; + }; if ( page.path ) { template = await registry From 7ce6b179ab049be8b1e5648e5cd2a6eb65d54736 Mon Sep 17 00:00:00 2001 From: scruffian Date: Tue, 7 Nov 2023 12:30:55 +0000 Subject: [PATCH 10/34] move the logic for inner block rendering to a separate function --- .../class-wp-navigation-block-renderer.php | 611 ++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php new file mode 100644 index 00000000000000..2d7cb67b4882a2 --- /dev/null +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -0,0 +1,611 @@ +render(); + $p = new WP_HTML_Tag_Processor( $inner_block_content ); + if ( $p->next_tag( + array( + 'name' => 'LI', + 'class_name' => 'has-child', + ) + ) ) { + $has_submenus = true; + } + } + return $has_submenus; + } + + /** + * Determine whether to load the view script. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return bool Returns whether or not to load the view script. + */ + private static function should_load_view_script( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; + } + + /** + * Returns the markup for a single inner block. + * + * @param WP_Block $inner_block The inner block. + * @return string Returns the markup for a single inner block. + */ + private static function get_markup_for_inner_block( $inner_block ) { + $inner_block_content = $inner_block->render(); + if ( ! empty( $inner_block_content ) ) { + if ( in_array( $inner_block->name, static::$needs_list_item_wrapper, true ) ) { + return '
  • ' . $inner_block_content . '
  • '; + } + + return $inner_block_content; + } + } + + /** + * Returns the html for the inner blocks of the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the html for the inner blocks of the navigation block. + */ + private static function get_inner_blocks_html( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $container_attributes = get_block_wrapper_attributes( + array( + 'class' => 'wp-block-navigation__container ' . $class, + 'style' => $style, + ) + ); + + $inner_blocks_html = ''; + $is_list_open = false; + + foreach ( $inner_blocks as $inner_block ) { + $is_list_item = in_array( $inner_block->name, static::$list_item_nav_blocks, true ); + + if ( $is_list_item && ! $is_list_open ) { + $is_list_open = true; + $inner_blocks_html .= sprintf( + '
      ', + $container_attributes + ); + } + + if ( ! $is_list_item && $is_list_open ) { + $is_list_open = false; + $inner_blocks_html .= '
    '; + } + + $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); + } + + if ( $is_list_open ) { + $inner_blocks_html .= ''; + } + + // Add directives to the submenu if needed. + if ( $has_submenus && $should_load_view_script ) { + $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); + $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); + } + + return $inner_blocks_html; + } + + /** + * Gets the inner blocks for the navigation block from the navigation post. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_navigation_post( $attributes ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return ''; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $parsed_blocks = parse_blocks( $navigation_post->post_content ); + + // 'parse_blocks' includes a null block with '\n\n' as the content when + // it encounters whitespace. This code strips it. + $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + + // TODO - this uses the full navigation block attributes for the + // context which could be refined. + return new WP_Block_List( $compacted_blocks, $attributes ); + } + } + + /** + * Gets the inner blocks for the navigation block from the fallback. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_fallback( $attributes ) { + $fallback_blocks = block_core_navigation_get_fallback_blocks(); + + // Fallback my have been filtered so do basic test for validity. + if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { + return ''; + } + + return new WP_Block_List( $fallback_blocks, $attributes ); + } + + /** + * Gets the inner blocks for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks( $attributes, $block ) { + $inner_blocks = $block->inner_blocks; + + // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. + if ( array_key_exists( 'navigationMenuId', $attributes ) ) { + $attributes['ref'] = $attributes['navigationMenuId']; + } + + // If: + // - the gutenberg plugin is active + // - `__unstableLocation` is defined + // - we have menu items at the defined location + // - we don't have a relationship to a `wp_navigation` Post (via `ref`). + // ...then create inner blocks from the classic menu assigned to that location. + if ( + defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && + array_key_exists( '__unstableLocation', $attributes ) && + ! array_key_exists( 'ref', $attributes ) && + ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) + ) { + $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); + } + + // Load inner blocks from the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); + } + + // If there are no inner blocks then fallback to rendering an appropriate fallback. + if ( empty( $inner_blocks ) ) { + $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); + } + + /** + * Filter navigation block $inner_blocks. + * Allows modification of a navigation block menu items. + * + * @since 6.1.0 + * + * @param \WP_Block_List $inner_blocks + */ + $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); + + $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); + if ( $post_ids ) { + _prime_post_caches( $post_ids, false, false ); + } + + return $inner_blocks; + } + + /** + * Gets the name of the current navigation, if it has one. + * + * @param array $attributes The block attributes. + * @param array $seen_menu_names The list of seen menu names, passed by reference so they can be updated. + * @return string Returns the name of the navigation. + */ + private static function get_navigation_name( $attributes, &$seen_menu_names ) { + $navigation_name = $attributes['ariaLabel'] ?? ''; + + // Load the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return $navigation_name; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $navigation_name = $navigation_post->post_title; + + // This is used to count the number of times a navigation name has been seen, + // so that we can ensure every navigation has a unique id. + if ( isset( $seen_menu_names[ $navigation_name ] ) ) { + ++$seen_menu_names[ $navigation_name ]; + } else { + $seen_menu_names[ $navigation_name ] = 1; + } + } + } + + return $navigation_name; + } + + /** + * Returns the layout class for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the layout class for the navigation block. + */ + private static function get_layout_class( $attributes ) { + $layout_justification = array( + 'left' => 'items-justified-left', + 'right' => 'items-justified-right', + 'center' => 'items-justified-center', + 'space-between' => 'items-justified-space-between', + ); + + $layout_class = ''; + if ( + isset( $attributes['layout']['justifyContent'] ) && + isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) + ) { + $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; + } + if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { + $layout_class .= ' is-vertical'; + } + + if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { + $layout_class .= ' no-wrap'; + } + return $layout_class; + } + + /** + * Return classes for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the classes for the navigation block. + */ + private static function get_classes( $attributes ) { + // Restore legacy classnames for submenu positioning. + $layout_class = static::get_layout_class( $attributes ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $is_responsive_menu = static::is_responsive( $attributes ); + + // Manually add block support text decoration as CSS class. + $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; + $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); + + $classes = array_merge( + $colors['css_classes'], + $font_sizes['css_classes'], + $is_responsive_menu ? array( 'is-responsive' ) : array(), + $layout_class ? array( $layout_class ) : array(), + $text_decoration ? array( $text_decoration_class ) : array() + ); + return implode( ' ', $classes ); + } + + /** + * Get styles for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the styles for the navigation block. + */ + private static function get_styles( $attributes ) { + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; + return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; + } + + /** + * Get the responsive container markup + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @param string $inner_blocks_html The markup for the inner blocks. + * @return string Returns the container markup. + */ + private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $modal_unique_id = wp_unique_id( 'modal-' ); + + $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + + $responsive_container_classes = array( + 'wp-block-navigation__responsive-container', + $is_hidden_by_default ? 'hidden-by-default' : '', + implode( ' ', $colors['overlay_css_classes'] ), + ); + $open_button_classes = array( + 'wp-block-navigation__responsive-container-open', + $is_hidden_by_default ? 'always-shown' : '', + ); + + $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; + $toggle_button_icon = ''; + if ( isset( $attributes['icon'] ) ) { + if ( 'menu' === $attributes['icon'] ) { + $toggle_button_icon = ''; + } + } + $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); + $toggle_close_button_icon = ''; + $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); + $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. + $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. + + // Add Interactivity API directives to the markup if needed. + $open_button_directives = ''; + $responsive_container_directives = ''; + $responsive_dialog_directives = ''; + $close_button_directives = ''; + if ( $should_load_view_script ) { + $open_button_directives = ' + data-wp-on--click="actions.core.navigation.openMenuOnClick" + data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + '; + $responsive_container_directives = ' + data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" + data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" + data-wp-effect="effects.core.navigation.initMenu" + data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" + tabindex="-1" + '; + $responsive_dialog_directives = ' + data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" + data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" + data-wp-bind--role="selectors.core.navigation.roleAttribute" + data-wp-effect="effects.core.navigation.focusFirstElement" + '; + $close_button_directives = ' + data-wp-on--click="actions.core.navigation.closeMenuOnClick" + '; + } + + return sprintf( + ' +
    +
    +
    + +
    + %2$s +
    +
    +
    +
    ', + esc_attr( $modal_unique_id ), + $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( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), + $toggle_button_content, + $toggle_close_button_content, + $open_button_directives, + $responsive_container_directives, + $responsive_dialog_directives, + $close_button_directives + ); + } + + /** + * Get the wrapper attributes + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks A list of inner blocks. + * @param string $nav_menu_name The name of the navigation menu. + * @return string Returns the navigation block markup. + */ + private static function get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ) { + $should_load_view_script = static::should_load_view_script( $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, + ) + ); + + if ( $is_responsive_menu ) { + $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); + $wrapper_attributes .= ' ' . $nav_element_directives; + } + + return $wrapper_attributes; + } + + /** + * Get the nav element directives + * + * @param bool $should_load_view_script Whether or not the view script should be loaded. + * @return string the directives for the navigation element. + */ + private static function get_nav_element_directives( $should_load_view_script ) { + if ( ! $should_load_view_script ) { + return ''; + } + // When adding to this array be mindful of security concerns. + $nav_element_context = wp_json_encode( + array( + 'core' => array( + 'navigation' => array( + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), + ), + ), + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP + ); + return ' + data-wp-interactive + data-wp-context=\'' . $nav_element_context . '\' + '; + } + + /** + * Handle view script loading. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @param WP_Block_List $inner_blocks The list of inner blocks. + */ + private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + + $view_js_file = 'wp-block-navigation-view'; + + // If the script already exists, there is no point in removing it from viewScript. + if ( ! wp_script_is( $view_js_file ) ) { + $script_handles = $block->block_type->view_script_handles; + + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); + } + } + } + + /** + * Returns the markup for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the navigation wrapper markup. + */ + private static function get_wrapper_markup( $attributes, $inner_blocks ) { + $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); + if ( static::is_responsive( $attributes ) ) { + return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); + } + return $inner_blocks_html; + } + + /** + * Renders the navigation block. + * + * @param array $attributes The block attributes. + * @param string $content The saved content. + * @param WP_Block $block The parsed block. + * @return string Returns the navigation block markup. + */ + public static function render( $attributes, $content, $block ) { + static $seen_menu_names = array(); + + $nav_menu_name = static::get_navigation_name( $attributes, $seen_menu_names ); + + /** + * Deprecated: + * The rgbTextColor and rgbBackgroundColor attributes + * have been deprecated in favor of + * customTextColor and customBackgroundColor ones. + * Move the values from old attrs to the new ones. + */ + if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { + $attributes['customTextColor'] = $attributes['rgbTextColor']; + } + + if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { + $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; + } + + unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); + + $inner_blocks = static::get_inner_blocks( $attributes, $block ); + // Prevent navigation blocks referencing themselves from rendering. + if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { + return ''; + } + + // If the menu name has been used previously then append an ID + // to the name to ensure uniqueness across a given post. + if ( isset( $seen_menu_names[ $nav_menu_name ] ) && $seen_menu_names[ $nav_menu_name ] > 1 ) { + $count = $seen_menu_names[ $nav_menu_name ]; + $nav_menu_name = $nav_menu_name . ' ' . ( $count ); + } + + $wrapper_attributes = static::get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ); + + static::handle_view_script_loading( $attributes, $block, $inner_blocks ); + + return sprintf( + '', + $wrapper_attributes, + static::get_wrapper_markup( $attributes, $inner_blocks ) + ); + } +} From a9f307bf4f9a111b82b31062a4c3a8c34a1b881a Mon Sep 17 00:00:00 2001 From: scruffian Date: Tue, 7 Nov 2023 12:37:02 +0000 Subject: [PATCH 11/34] Revert "move the logic for inner block rendering to a separate function" This reverts commit 7ce6b179ab049be8b1e5648e5cd2a6eb65d54736. --- .../class-wp-navigation-block-renderer.php | 611 ------------------ 1 file changed, 611 deletions(-) delete mode 100644 lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php deleted file mode 100644 index 2d7cb67b4882a2..00000000000000 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ /dev/null @@ -1,611 +0,0 @@ -render(); - $p = new WP_HTML_Tag_Processor( $inner_block_content ); - if ( $p->next_tag( - array( - 'name' => 'LI', - 'class_name' => 'has-child', - ) - ) ) { - $has_submenus = true; - } - } - return $has_submenus; - } - - /** - * Determine whether to load the view script. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not to load the view script. - */ - private static function should_load_view_script( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; - } - - /** - * Returns the markup for a single inner block. - * - * @param WP_Block $inner_block The inner block. - * @return string Returns the markup for a single inner block. - */ - private static function get_markup_for_inner_block( $inner_block ) { - $inner_block_content = $inner_block->render(); - if ( ! empty( $inner_block_content ) ) { - if ( in_array( $inner_block->name, static::$needs_list_item_wrapper, true ) ) { - return '
  • ' . $inner_block_content . '
  • '; - } - - return $inner_block_content; - } - } - - /** - * Returns the html for the inner blocks of the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the html for the inner blocks of the navigation block. - */ - private static function get_inner_blocks_html( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $container_attributes = get_block_wrapper_attributes( - array( - 'class' => 'wp-block-navigation__container ' . $class, - 'style' => $style, - ) - ); - - $inner_blocks_html = ''; - $is_list_open = false; - - foreach ( $inner_blocks as $inner_block ) { - $is_list_item = in_array( $inner_block->name, static::$list_item_nav_blocks, true ); - - if ( $is_list_item && ! $is_list_open ) { - $is_list_open = true; - $inner_blocks_html .= sprintf( - '
      ', - $container_attributes - ); - } - - if ( ! $is_list_item && $is_list_open ) { - $is_list_open = false; - $inner_blocks_html .= '
    '; - } - - $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); - } - - if ( $is_list_open ) { - $inner_blocks_html .= ''; - } - - // Add directives to the submenu if needed. - if ( $has_submenus && $should_load_view_script ) { - $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); - $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); - } - - return $inner_blocks_html; - } - - /** - * Gets the inner blocks for the navigation block from the navigation post. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_navigation_post( $attributes ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return ''; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $parsed_blocks = parse_blocks( $navigation_post->post_content ); - - // 'parse_blocks' includes a null block with '\n\n' as the content when - // it encounters whitespace. This code strips it. - $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); - - // TODO - this uses the full navigation block attributes for the - // context which could be refined. - return new WP_Block_List( $compacted_blocks, $attributes ); - } - } - - /** - * Gets the inner blocks for the navigation block from the fallback. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_fallback( $attributes ) { - $fallback_blocks = block_core_navigation_get_fallback_blocks(); - - // Fallback my have been filtered so do basic test for validity. - if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { - return ''; - } - - return new WP_Block_List( $fallback_blocks, $attributes ); - } - - /** - * Gets the inner blocks for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks( $attributes, $block ) { - $inner_blocks = $block->inner_blocks; - - // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. - if ( array_key_exists( 'navigationMenuId', $attributes ) ) { - $attributes['ref'] = $attributes['navigationMenuId']; - } - - // If: - // - the gutenberg plugin is active - // - `__unstableLocation` is defined - // - we have menu items at the defined location - // - we don't have a relationship to a `wp_navigation` Post (via `ref`). - // ...then create inner blocks from the classic menu assigned to that location. - if ( - defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && - array_key_exists( '__unstableLocation', $attributes ) && - ! array_key_exists( 'ref', $attributes ) && - ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) - ) { - $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); - } - - // Load inner blocks from the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); - } - - // If there are no inner blocks then fallback to rendering an appropriate fallback. - if ( empty( $inner_blocks ) ) { - $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); - } - - /** - * Filter navigation block $inner_blocks. - * Allows modification of a navigation block menu items. - * - * @since 6.1.0 - * - * @param \WP_Block_List $inner_blocks - */ - $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - - $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); - if ( $post_ids ) { - _prime_post_caches( $post_ids, false, false ); - } - - return $inner_blocks; - } - - /** - * Gets the name of the current navigation, if it has one. - * - * @param array $attributes The block attributes. - * @param array $seen_menu_names The list of seen menu names, passed by reference so they can be updated. - * @return string Returns the name of the navigation. - */ - private static function get_navigation_name( $attributes, &$seen_menu_names ) { - $navigation_name = $attributes['ariaLabel'] ?? ''; - - // Load the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return $navigation_name; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $navigation_name = $navigation_post->post_title; - - // This is used to count the number of times a navigation name has been seen, - // so that we can ensure every navigation has a unique id. - if ( isset( $seen_menu_names[ $navigation_name ] ) ) { - ++$seen_menu_names[ $navigation_name ]; - } else { - $seen_menu_names[ $navigation_name ] = 1; - } - } - } - - return $navigation_name; - } - - /** - * Returns the layout class for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the layout class for the navigation block. - */ - private static function get_layout_class( $attributes ) { - $layout_justification = array( - 'left' => 'items-justified-left', - 'right' => 'items-justified-right', - 'center' => 'items-justified-center', - 'space-between' => 'items-justified-space-between', - ); - - $layout_class = ''; - if ( - isset( $attributes['layout']['justifyContent'] ) && - isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) - ) { - $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; - } - if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { - $layout_class .= ' is-vertical'; - } - - if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { - $layout_class .= ' no-wrap'; - } - return $layout_class; - } - - /** - * Return classes for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the classes for the navigation block. - */ - private static function get_classes( $attributes ) { - // Restore legacy classnames for submenu positioning. - $layout_class = static::get_layout_class( $attributes ); - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); - $is_responsive_menu = static::is_responsive( $attributes ); - - // Manually add block support text decoration as CSS class. - $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; - $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); - - $classes = array_merge( - $colors['css_classes'], - $font_sizes['css_classes'], - $is_responsive_menu ? array( 'is-responsive' ) : array(), - $layout_class ? array( $layout_class ) : array(), - $text_decoration ? array( $text_decoration_class ) : array() - ); - return implode( ' ', $classes ); - } - - /** - * Get styles for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the styles for the navigation block. - */ - private static function get_styles( $attributes ) { - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); - $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; - return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; - } - - /** - * Get the responsive container markup - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @param string $inner_blocks_html The markup for the inner blocks. - * @return string Returns the container markup. - */ - private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $colors = block_core_navigation_build_css_colors( $attributes ); - $modal_unique_id = wp_unique_id( 'modal-' ); - - $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - - $responsive_container_classes = array( - 'wp-block-navigation__responsive-container', - $is_hidden_by_default ? 'hidden-by-default' : '', - implode( ' ', $colors['overlay_css_classes'] ), - ); - $open_button_classes = array( - 'wp-block-navigation__responsive-container-open', - $is_hidden_by_default ? 'always-shown' : '', - ); - - $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; - $toggle_button_icon = ''; - if ( isset( $attributes['icon'] ) ) { - if ( 'menu' === $attributes['icon'] ) { - $toggle_button_icon = ''; - } - } - $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); - $toggle_close_button_icon = ''; - $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); - $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. - $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. - - // Add Interactivity API directives to the markup if needed. - $open_button_directives = ''; - $responsive_container_directives = ''; - $responsive_dialog_directives = ''; - $close_button_directives = ''; - if ( $should_load_view_script ) { - $open_button_directives = ' - data-wp-on--click="actions.core.navigation.openMenuOnClick" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - '; - $responsive_container_directives = ' - data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" - data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" - data-wp-effect="effects.core.navigation.initMenu" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" - tabindex="-1" - '; - $responsive_dialog_directives = ' - data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" - data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" - data-wp-bind--role="selectors.core.navigation.roleAttribute" - data-wp-effect="effects.core.navigation.focusFirstElement" - '; - $close_button_directives = ' - data-wp-on--click="actions.core.navigation.closeMenuOnClick" - '; - } - - return sprintf( - ' -
    -
    -
    - -
    - %2$s -
    -
    -
    -
    ', - esc_attr( $modal_unique_id ), - $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( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), - $toggle_button_content, - $toggle_close_button_content, - $open_button_directives, - $responsive_container_directives, - $responsive_dialog_directives, - $close_button_directives - ); - } - - /** - * Get the wrapper attributes - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks A list of inner blocks. - * @param string $nav_menu_name The name of the navigation menu. - * @return string Returns the navigation block markup. - */ - private static function get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ) { - $should_load_view_script = static::should_load_view_script( $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, - ) - ); - - if ( $is_responsive_menu ) { - $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); - $wrapper_attributes .= ' ' . $nav_element_directives; - } - - return $wrapper_attributes; - } - - /** - * Get the nav element directives - * - * @param bool $should_load_view_script Whether or not the view script should be loaded. - * @return string the directives for the navigation element. - */ - private static function get_nav_element_directives( $should_load_view_script ) { - if ( ! $should_load_view_script ) { - return ''; - } - // When adding to this array be mindful of security concerns. - $nav_element_context = wp_json_encode( - array( - 'core' => array( - 'navigation' => array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - ), - ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP - ); - return ' - data-wp-interactive - data-wp-context=\'' . $nav_element_context . '\' - '; - } - - /** - * Handle view script loading. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @param WP_Block_List $inner_blocks The list of inner blocks. - */ - private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - - $view_js_file = 'wp-block-navigation-view'; - - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - } - - /** - * Returns the markup for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the navigation wrapper markup. - */ - private static function get_wrapper_markup( $attributes, $inner_blocks ) { - $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); - if ( static::is_responsive( $attributes ) ) { - return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); - } - return $inner_blocks_html; - } - - /** - * Renders the navigation block. - * - * @param array $attributes The block attributes. - * @param string $content The saved content. - * @param WP_Block $block The parsed block. - * @return string Returns the navigation block markup. - */ - public static function render( $attributes, $content, $block ) { - static $seen_menu_names = array(); - - $nav_menu_name = static::get_navigation_name( $attributes, $seen_menu_names ); - - /** - * Deprecated: - * The rgbTextColor and rgbBackgroundColor attributes - * have been deprecated in favor of - * customTextColor and customBackgroundColor ones. - * Move the values from old attrs to the new ones. - */ - if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { - $attributes['customTextColor'] = $attributes['rgbTextColor']; - } - - if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { - $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; - } - - unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); - - $inner_blocks = static::get_inner_blocks( $attributes, $block ); - // Prevent navigation blocks referencing themselves from rendering. - if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { - return ''; - } - - // If the menu name has been used previously then append an ID - // to the name to ensure uniqueness across a given post. - if ( isset( $seen_menu_names[ $nav_menu_name ] ) && $seen_menu_names[ $nav_menu_name ] > 1 ) { - $count = $seen_menu_names[ $nav_menu_name ]; - $nav_menu_name = $nav_menu_name . ' ' . ( $count ); - } - - $wrapper_attributes = static::get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ); - - static::handle_view_script_loading( $attributes, $block, $inner_blocks ); - - return sprintf( - '', - $wrapper_attributes, - static::get_wrapper_markup( $attributes, $inner_blocks ) - ); - } -} From e94446acb3071db0b5321e29def411ea58be3043 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 7 Nov 2023 17:03:55 +0400 Subject: [PATCH 12/34] Playwright Utils: Fix 'clickBlockOptionsMenuItem' helper (#55923) --- .../src/editor/click-block-options-menu-item.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts b/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts index 2c473352c6123d..e8c02fb11dcb71 100644 --- a/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts +++ b/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts @@ -12,8 +12,7 @@ import type { Editor } from './index'; export async function clickBlockOptionsMenuItem( this: Editor, label: string ) { await this.clickBlockToolbarButton( 'Options' ); await this.page - .locator( - `role=menu[name="Options"i] >> role=menuitem[name="${ label }"i]` - ) + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: label } ) .click(); } From 3cdfd9d3525307ac6e491694bd3c203557371f4a Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 7 Nov 2023 17:21:03 +0400 Subject: [PATCH 13/34] Migrate 'Meta boxes' e2e tests to Playwright (#55915) * Migrate 'Meta boxes' e2e tests to Playwright * Remove old test file --- .../specs/editor/plugins/meta-boxes.test.js | 137 ------------------ .../specs/editor/plugins/meta-boxes.spec.js | 123 ++++++++++++++++ 2 files changed, 123 insertions(+), 137 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js create mode 100644 test/e2e/specs/editor/plugins/meta-boxes.spec.js diff --git a/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js b/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js deleted file mode 100644 index 3a75f9656c71e5..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - findSidebarPanelToggleButtonWithTitle, - insertBlock, - openDocumentSettingsSidebar, - publishPost, - saveDraft, -} from '@wordpress/e2e-test-utils'; - -describe( 'Meta boxes', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-plugin-meta-box' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); - } ); - - it( 'Should save the post', async () => { - // Save should not be an option for new empty post. - expect( await page.$( '.editor-post-save-draft' ) ).toBe( null ); - - // Add title to enable valid non-empty post save. - await page.type( '.editor-post-title__input', 'Hello Meta' ); - expect( await page.$( '.editor-post-save-draft' ) ).not.toBe( null ); - - await saveDraft(); - - // After saving, affirm that the button returns to Save Draft. - await page.waitForSelector( '.editor-post-save-draft' ); - } ); - - it( 'Should render dynamic blocks when the meta box uses the excerpt for front end rendering', async () => { - // Publish a post so there's something for the latest posts dynamic block to render. - await page.type( '.editor-post-title__input', 'A published post' ); - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Hello there!' ); - await publishPost(); - - // Publish a post with the latest posts dynamic block. - await createNewPost(); - await page.type( '.editor-post-title__input', 'Dynamic block test' ); - await insertBlock( 'Latest Posts' ); - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Check the dynamic block appears. - const latestPostsBlock = await page.waitForSelector( - '.wp-block-latest-posts' - ); - - expect( - await latestPostsBlock.evaluate( ( block ) => block.textContent ) - ).toContain( 'A published post' ); - - expect( - await latestPostsBlock.evaluate( ( block ) => block.textContent ) - ).toContain( 'Dynamic block test' ); - } ); - - it( 'Should render the excerpt in meta based on post content if no explicit excerpt exists', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Excerpt from content.' ); - await page.type( '.editor-post-title__input', 'A published post' ); - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Retrieve the excerpt used as meta. - const metaExcerpt = await page.evaluate( () => { - return document - .querySelector( 'meta[property="gutenberg:hello"]' ) - .getAttribute( 'content' ); - } ); - - expect( metaExcerpt ).toEqual( 'Excerpt from content.' ); - } ); - - it( 'Should render the explicitly set excerpt in meta instead of the content based one', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Excerpt from content.' ); - await page.type( '.editor-post-title__input', 'A published post' ); - - // Open the excerpt panel. - await openDocumentSettingsSidebar(); - const excerptButton = - await findSidebarPanelToggleButtonWithTitle( 'Excerpt' ); - if ( excerptButton ) { - await excerptButton.click( 'button' ); - } - - await page.waitForSelector( '.editor-post-excerpt textarea' ); - - await page.type( - '.editor-post-excerpt textarea', - 'Explicitly set excerpt.' - ); - - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Retrieve the excerpt used as meta. - const metaExcerpt = await page.evaluate( () => { - return document - .querySelector( 'meta[property="gutenberg:hello"]' ) - .getAttribute( 'content' ); - } ); - - expect( metaExcerpt ).toEqual( 'Explicitly set excerpt.' ); - } ); -} ); diff --git a/test/e2e/specs/editor/plugins/meta-boxes.spec.js b/test/e2e/specs/editor/plugins/meta-boxes.spec.js new file mode 100644 index 00000000000000..b901201ff6c1de --- /dev/null +++ b/test/e2e/specs/editor/plugins/meta-boxes.spec.js @@ -0,0 +1,123 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Meta boxes', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-plugin-meta-box' ); + await requestUtils.deleteAllPosts(); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); + } ); + + test( 'Should save the post', async ( { editor, page } ) => { + const saveDraft = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save draft' } ); + + // Save should not be an option for new empty post. + await expect( saveDraft ).toBeDisabled(); + + // Add title to enable valid non-empty post save. + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello Meta' ); + + await expect( saveDraft ).toBeEnabled(); + + await editor.saveDraft(); + + // After saving, affirm that the button returns to Save Draft. + await expect( saveDraft ).toBeEnabled(); + } ); + + test( 'Should render dynamic blocks when the meta box uses the excerpt for front end rendering', async ( { + admin, + editor, + page, + } ) => { + // Publish a post so there's something for the latest posts dynamic block to render. + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'A published post' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Hello there!' ); + await editor.publishPost(); + + // Publish a post with the latest posts dynamic block. + await admin.createNewPost(); + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Dynamic block test' ); + await editor.insertBlock( { name: 'core/latest-posts' } ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + + await expect( + page.locator( '.wp-block-latest-posts > li' ) + ).toContainText( [ 'A published post', 'Dynamic block test' ] ); + } ); + + test( 'Should render the excerpt in meta based on post content if no explicit excerpt exists', async ( { + editor, + page, + } ) => { + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'A published post' ); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page.keyboard.type( 'Excerpt from content.' ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + + await expect( + page.locator( 'meta[property="gutenberg:hello"]' ) + ).toHaveAttribute( 'content', 'Excerpt from content.' ); + } ); + + test( 'Should render the explicitly set excerpt in meta instead of the content based one', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page.keyboard.type( 'Excerpt from content.' ); + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'A published post' ); + + const documentSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + const excerptButton = documentSettings.getByRole( 'button', { + name: 'Excerpt', + } ); + + // eslint-disable-next-line playwright/no-conditional-in-test + if ( + ( await excerptButton.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await excerptButton.click(); + } + + await documentSettings + .getByRole( 'textbox', { name: 'Write an Excerpt' } ) + .fill( 'Explicitly set excerpt.' ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + + await expect( + page.locator( 'meta[property="gutenberg:hello"]' ) + ).toHaveAttribute( 'content', 'Explicitly set excerpt.' ); + } ); +} ); From 9b1cbc60955123efc0f7b946867257a77ff7e5d1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 7 Nov 2023 13:22:16 +0000 Subject: [PATCH 14/34] Dataviews: Add icon for the side by side view. (#55925) --- .../src/components/sidebar-dataviews/dataview-item.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 45e3a9d50f3f6f..1d24ff8d5ef3b4 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js +++ b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { page, columns } from '@wordpress/icons'; +import { page, columns, pullRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; /** @@ -13,7 +13,7 @@ import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); function getDataViewIcon( type ) { - const icons = { list: page, grid: columns }; + const icons = { list: page, grid: columns, 'side-by-side': pullRight }; return icons[ type ]; } From d3274f615af500e2707b0c023abf7a7fa2baf7d7 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 7 Nov 2023 13:58:05 +0000 Subject: [PATCH 15/34] Fix: 404 link in get-started-with-create-block docs. (#55932) --- docs/getting-started/devenv/get-started-with-create-block.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/devenv/get-started-with-create-block.md b/docs/getting-started/devenv/get-started-with-create-block.md index a01c08a4ce2f44..3a2c6607b82cff 100644 --- a/docs/getting-started/devenv/get-started-with-create-block.md +++ b/docs/getting-started/devenv/get-started-with-create-block.md @@ -1,6 +1,6 @@ # Get started with create-block -Custom blocks for the Block Editor in WordPress are typically registered using plugins and are defined through a specific set of files. The [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package is an officially supported tool to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts.md)) with no configuration required. +Custom blocks for the Block Editor in WordPress are typically registered using plugins and are defined through a specific set of files. The [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package is an officially supported tool to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/)) with no configuration required. The package is designed to help developers quickly set up a block development environment following WordPress best practices. From a82dedf8f9238f7f250f75ef5931b16c8dd1f19c Mon Sep 17 00:00:00 2001 From: Siobhan Bamber Date: Tue, 7 Nov 2023 14:26:40 +0000 Subject: [PATCH 16/34] [RNMobile] Enable rendering a block's SVG icon directly from an XML string (#55742) Enable rendering a block's SVG icon directly from an XML string. --- packages/primitives/src/svg/index.native.js | 1 + test/native/setup.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/primitives/src/svg/index.native.js b/packages/primitives/src/svg/index.native.js index c8c735283c05a0..719f4d233cc3af 100644 --- a/packages/primitives/src/svg/index.native.js +++ b/packages/primitives/src/svg/index.native.js @@ -26,6 +26,7 @@ export { LinearGradient, Stop, Line, + SvgXml, } from 'react-native-svg'; const AnimatedSvg = Animated.createAnimatedComponent( diff --git a/test/native/setup.js b/test/native/setup.js index 00fb95070d84d7..53ab28f861a1ef 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -151,6 +151,7 @@ jest.mock( 'react-native-svg', () => { G: () => 'G', Polygon: () => 'Polygon', Rect: () => 'Rect', + SvgXml: jest.fn(), }; } ); From 7b89a5e4ab93269af100364367893d65b1295605 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 7 Nov 2023 10:13:58 -0500 Subject: [PATCH 17/34] test: E2E test setup script creates Android emulator (#55898) * test: E2E test script detects or create Android emulators Simplify E2E test environment setup by creating the required Android emualtors if they are not present. * docs: Simplify E2E test setup steps The setup script now manages these previously documented steps. * test: Clarify cache cleared log * test: Improve consistency in E2E test setup script logging Reduce verbosity and improve consistency of log messages. * test: Disable Android emulator creation on CI servers The `avdmanager` command is unavailable, also the CI server generally provides its own emulator. * test: E2E test setup script throws error for missing `avdmanager` This CLI is required for creating emulators. * test: Replace static device references in Android emulator setup These values need to update if the `device-config.json` updates. * feat: Increase consistency in E2E script log messages --- .../__device-tests__/README.md | 25 ++--------- .../react-native-editor/bin/test-e2e-setup.sh | 41 ++++++++++++++++--- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/README.md b/packages/react-native-editor/__device-tests__/README.md index f4a4fcf89aed6c..e917a297a491c7 100644 --- a/packages/react-native-editor/__device-tests__/README.md +++ b/packages/react-native-editor/__device-tests__/README.md @@ -4,28 +4,9 @@ The Mobile Gutenberg (MG) project maintains a suite of automated end-to-end (E2E ## Setup -Before setting up the E2E test environment, the required iOS and Android dependencies must be installed. - -> **Note** -> The required dependencies change overtime. We do our best to update the scripts documented below, but it is best to review the [Appium capabilities](https://github.com/WordPress/gutenberg/blob/trunk/packages/react-native-editor/__device-tests__/helpers/caps.js) configuration to identify the currently required `deviceName` and `platformVersion` for each of the iOS and Android platforms. - -### iOS - -- Complete the [React Native Getting Started](https://reactnative.dev/docs/environment-setup) guide, which covers installing and setting up Xcode. -- Open [Xcode settings](https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#Install-and-manage-Simulator-runtimes-in-settings) to install the iOS 16.2 simulator runtime. - -### Android - -- Complete the [React Native Getting Started](https://reactnative.dev/docs/environment-setup) guide, which covers installing and setting up Android Studio and the Android SDK. -- Open Android Studio and [create an emulator](https://developer.android.com/studio/run/managing-avds) for a Pixel 3 XL running Android 11.0 with the “Enable Device Frame” option disabled. - -### Test Environment - -After installing the iOS and Android dependencies, the MG project provides a script to set up the testing environment, verifying necessary dependencies are available. - -```shell -npm run native test:e2e:setup -``` +1. Complete the [React Native Getting Started](https://reactnative.dev/docs/environment-setup) guide for both iOS and Android, which covers setting up Xcode, Android Studio, the Android SDK. +1. Open [Xcode settings](https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#Install-and-manage-Simulator-runtimes-in-settings) to install the iOS 16.2 simulator runtime. +1. `npm run native test:e2e:setup` ## Running Tests diff --git a/packages/react-native-editor/bin/test-e2e-setup.sh b/packages/react-native-editor/bin/test-e2e-setup.sh index 083748391bf737..ed896a03eacb85 100755 --- a/packages/react-native-editor/bin/test-e2e-setup.sh +++ b/packages/react-native-editor/bin/test-e2e-setup.sh @@ -1,4 +1,4 @@ -#!/bin/bash -eu +#!/bin/bash -e set -o pipefail @@ -24,14 +24,14 @@ function log_error() { output=$($APPIUM_CMD driver list --installed --json) if echo "$output" | grep -q 'uiautomator2'; then - log_info "UiAutomator2 is installed, skipping installation." + log_info "UiAutomator2 available." else log_info "UiAutomator2 not found, installing..." $APPIUM_CMD driver install uiautomator2 fi if echo "$output" | grep -q 'xcuitest'; then - log_info "XCUITest is installed, skipping installation." + log_info "XCUITest available." else log_info "XCUITest not found, installing..." $APPIUM_CMD driver install xcuitest @@ -54,7 +54,7 @@ function detect_or_create_simulator() { local simulators=$(xcrun simctl list devices -j | jq -r --arg runtime "$runtime_name" '.devices | to_entries[] | select(.key | contains($runtime)) | .value[] | .name + "," + .udid') if ! echo "$simulators" | grep -q "$simulator_name"; then - log_info "$simulator_name ($runtime_name_display) not available, creating..." + log_info "$simulator_name ($runtime_name_display) not found, creating..." xcrun simctl create "$simulator_name" "$simulator_name" "com.apple.CoreSimulator.SimRuntime.$runtime_name" > /dev/null log_success "$simulator_name ($runtime_name_display) created." else @@ -69,6 +69,37 @@ IOS_DEVICE_TABLET_NAME=$(jq -r '.ios.local.deviceTabletName' "$CONFIG_FILE") detect_or_create_simulator "$IOS_DEVICE_NAME" detect_or_create_simulator "$IOS_DEVICE_TABLET_NAME" +function detect_or_create_emulator() { + if [[ "${CI}" ]]; then + log_info "Detected CI server, skipping Android emulator creation." + return + fi + + if [[ -z $(command -v avdmanager) ]]; then + log_error "avdmanager not found! Please install the Android SDK command-line tools.\n https://developer.android.com/tools/" + exit 1; + fi + + local emulator_name=$1 + local emulator_id=$(echo "$emulator_name" | sed 's/ /_/g; s/\./_/g') + local device_id=$(echo "$emulator_id" | awk -F '_' '{print tolower($1)"_"tolower($2)"_"tolower($3)}') + local runtime_api=$(echo "$emulator_id" | awk -F '_' '{print $NF}') + local emulator=$(emulator -list-avds | grep "$emulator_id") + + if [[ -z $emulator ]]; then + log_info "$emulator_name not found, creating..." + avdmanager create avd -n "$emulator_id" -k "system-images;android-$runtime_api;google_apis;arm64-v8a" -d "$device_id" > /dev/null + log_success "$emulator_name created." + else + log_info "$emulator_name available." + fi +} + +ANDROID_DEVICE_NAME=$(jq -r '.android.local.deviceName' "$CONFIG_FILE") + +# Create the required Android emulators, if they don't exist +detect_or_create_emulator $ANDROID_DEVICE_NAME + # Mitigate conflicts between development server caches and E2E tests npm run clean:runtime > /dev/null -log_info 'Runtime cache cleaned.' +log_info 'Runtime cache cleared.' From ee3027b0bbe1bd9d12c904ff5fd98ff44370f25d Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 7 Nov 2023 19:24:36 +0400 Subject: [PATCH 18/34] Data: Fix ESLint warnings for the 'useSelect' hook (#55916) * Data: Fix ESLint warnings for the 'useSelect' hook * Provide reason via comment --- packages/data/src/components/use-select/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 62da6a005c0d0b..84cf7ed617f185 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -193,7 +193,14 @@ function useStaticSelect( storeName ) { function useMappingSelect( suspense, mapSelect, deps ) { const registry = useRegistry(); const isAsync = useAsyncMode(); - const store = useMemo( () => Store( registry, suspense ), [ registry ] ); + const store = useMemo( + () => Store( registry, suspense ), + [ registry, suspense ] + ); + + // These are "pass-through" dependencies from the parent hook, + // and the parent should catch any hook rule violations. + // eslint-disable-next-line react-hooks/exhaustive-deps const selector = useCallback( mapSelect, deps ); const { subscribe, getValue } = store( selector, isAsync ); const result = useSyncExternalStore( subscribe, getValue, getValue ); From 659c960d8736ca248e7856007ddac8bd74cd8935 Mon Sep 17 00:00:00 2001 From: Joan <47475754+joanrodas@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:49:53 +0100 Subject: [PATCH 19/34] Update Link Control labels to use gray-900 (#55867) --- .../block-editor/src/components/link-control/style.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index 8883af42ee2ca6..fae3f03786dfe5 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -61,6 +61,10 @@ $preview-image-height: 140px; .block-editor-link-control__field { margin: $grid-unit-20; // allow margin collapse for vertical spacing. + .components-base-control__label { + color: $gray-900; + } + input[type="text"], // Specificity overide of URLInput defaults. &.block-editor-url-input input[type="text"].block-editor-url-input__input { @@ -419,6 +423,10 @@ $preview-image-height: 140px; .components-base-control__field { display: flex; // don't allow label to wrap under checkbox. + + .components-checkbox-control__label { + color: $gray-900; + } } // Cancel left margin inherited from WP Admin Forms CSS. From eba64cad7457345e514b1cba1b7523bc45b72e6d Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:59:59 +0900 Subject: [PATCH 20/34] Font Library: Fix font installation failure (#55893) * Font Library: Fix font installation failure * Made 'has_upload_director' private --------- Co-authored-by: Jason Crist --- .../class-wp-rest-font-library-controller.php | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php index 19dfcaab49533c..9655178d706679 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php @@ -344,6 +344,18 @@ public function update_font_library_permissions_check() { return true; } + /** + * Checks whether the font directory exists or not. + * + * @since 6.5.0 + * + * @return bool Whether the font directory exists. + */ + private function has_upload_directory() { + $upload_dir = WP_Font_Library::get_fonts_dir(); + return is_dir( $upload_dir ); + } + /** * Checks whether the user has write permissions to the temp and fonts directories. * @@ -418,12 +430,29 @@ public function install_fonts( $request ) { $response_status = 400; } - if ( $this->needs_write_permission( $fonts_to_install ) && ! $this->has_write_permission() ) { - $errors[] = new WP_Error( - 'cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ) - ); - $response_status = 500; + if ( $this->needs_write_permission( $fonts_to_install ) ) { + $upload_dir = WP_Font_Library::get_fonts_dir(); + if ( ! $this->has_upload_directory() ) { + if ( ! wp_mkdir_p( $upload_dir ) ) { + $errors[] = new WP_Error( + 'cannot_create_fonts_folder', + sprintf( + /* translators: %s: Directory path. */ + __( 'Error: Unable to create directory %s.', 'gutenberg' ), + esc_html( $upload_dir ) + ) + ); + $response_status = 500; + } + } + + if ( $this->has_upload_directory() && ! $this->has_write_permission() ) { + $errors[] = new WP_Error( + 'cannot_write_fonts_folder', + __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ) + ); + $response_status = 500; + } } if ( ! empty( $errors ) ) { From 25ce1288e4ad6e11da8d3a1fb327b6b08c5721a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor?= <27339341+priethor@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:26:21 +0100 Subject: [PATCH 21/34] Update enforce-pr-labels.yml (#55900) --- .github/workflows/enforce-pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enforce-pr-labels.yml b/.github/workflows/enforce-pr-labels.yml index 320ef1375029c3..4ef163694b947a 100644 --- a/.github/workflows/enforce-pr-labels.yml +++ b/.github/workflows/enforce-pr-labels.yml @@ -14,5 +14,5 @@ jobs: count: 1 labels: '[Type] Automated Testing, [Type] Breaking Change, [Type] Bug, [Type] Build Tooling, [Type] Code Quality, [Type] Copy, [Type] Developer Documentation, [Type] Enhancement, [Type] Experimental, [Type] Feature, [Type] New API, [Type] Task, [Type] Performance, [Type] Project Management, [Type] Regression, [Type] Security, [Type] WP Core Ticket, Backport from WordPress Core' add_comment: true - message: "**Warning: Type of PR label error**\n\n To merge this PR, it requires {{ errorString }} {{ count }} label indicating the type of PR. Other labels are optional and not being checked here. \n- **Type-related labels to choose from**: {{ provided }}.\n- **Labels found**: {{ applied }}.\n\nRead more about [Type labels in Gutenberg](https://github.com/WordPress/gutenberg/labels?q=type)." + message: "**Warning: Type of PR label mismatch**\n\n To merge this PR, it requires {{ errorString }} {{ count }} label indicating the type of PR. Other labels are optional and not being checked here. \n- **Type-related labels to choose from**: {{ provided }}.\n- **Labels found**: {{ applied }}.\n\nRead more about [Type labels in Gutenberg](https://github.com/WordPress/gutenberg/labels?q=type). Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task." exit_type: failure From 3daa76a992922dc55780b334ed71940505974528 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:48:34 -0500 Subject: [PATCH 22/34] Tabs: improve focus behavior (#55287) --- packages/components/CHANGELOG.md | 4 ++ packages/components/src/tabs/README.md | 7 ++ .../src/tabs/stories/index.story.tsx | 10 ++- packages/components/src/tabs/styles.ts | 16 +++++ packages/components/src/tabs/tabpanel.tsx | 13 ++-- packages/components/src/tabs/test/index.tsx | 66 ++++++++++++++++++- packages/components/src/tabs/types.ts | 10 ++- 7 files changed, 118 insertions(+), 8 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 72edad0df4e10e..8dd8d4b552d9d8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - Migrate `Divider` from `reakit` to `ariakit` ([#55622](https://github.com/WordPress/gutenberg/pull/55622)) +### Experimental + +- `Tabs`: Add `focusable` prop to the `Tabs.TabPanel` sub-component ([#55287](https://github.com/WordPress/gutenberg/pull/55287)) + ### Enhancements - `ToggleGroupControl`: Add opt-in prop for 40px default size ([#55789](https://github.com/WordPress/gutenberg/pull/55789)). diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index 6907f385fda371..8fb4e0e73caec2 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -240,3 +240,10 @@ The class name to apply to the tabpanel. Custom CSS styles for the tab. - Required: No + +###### `focusable`: `boolean` + +Determines whether or not the tabpanel element should be focusable. If `false`, pressing the tab key will skip over the tabpanel, and instead focus on the first focusable element in the panel (if there is one). + +- Required: No +- Default: `true` diff --git a/packages/components/src/tabs/stories/index.story.tsx b/packages/components/src/tabs/stories/index.story.tsx index 3b6ba022f6d91b..08e29589881707 100644 --- a/packages/components/src/tabs/stories/index.story.tsx +++ b/packages/components/src/tabs/stories/index.story.tsx @@ -42,8 +42,16 @@ const Template: StoryFn< typeof Tabs > = ( props ) => {

    Selected tab: Tab 2

    - +

    Selected tab: Tab 3

    +

    + This tabpanel has its focusable prop set to + false, so it won't get a tab stop. +
    + Instead, the [Tab] key will move focus to the first + focusable element within the panel. +

    +
    ); diff --git a/packages/components/src/tabs/styles.ts b/packages/components/src/tabs/styles.ts index 091ba608fb6ecd..cb735f3177662a 100644 --- a/packages/components/src/tabs/styles.ts +++ b/packages/components/src/tabs/styles.ts @@ -101,3 +101,19 @@ export const Tab = styled( Ariakit.Tab )` } } `; + +export const TabPanel = styled( Ariakit.TabPanel )` + &:focus { + box-shadow: none; + outline: none; + } + + &:focus-visible { + border-radius: 2px; + box-shadow: 0 0 0 var( --wp-admin-border-width-focus ) + ${ COLORS.theme.accent }; + // Windows high contrast mode. + outline: 2px solid transparent; + outline-offset: 0; + } +`; diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index fb62fc91912331..b5339141a56eca 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -1,8 +1,6 @@ /** * External dependencies */ -// eslint-disable-next-line no-restricted-imports -import * as Ariakit from '@ariakit/react'; /** * WordPress dependencies @@ -14,12 +12,16 @@ import { forwardRef, useContext } from '@wordpress/element'; * Internal dependencies */ import type { TabPanelProps } from './types'; +import { TabPanel as StyledTabPanel } from './styles'; import warning from '@wordpress/warning'; import { TabsContext } from './context'; export const TabPanel = forwardRef< HTMLDivElement, TabPanelProps >( - function TabPanel( { children, id, className, style }, ref ) { + function TabPanel( + { children, id, className, style, focusable = true }, + ref + ) { const context = useContext( TabsContext ); if ( ! context ) { warning( '`Tabs.TabPanel` must be wrapped in a `Tabs` component.' ); @@ -28,7 +30,8 @@ export const TabPanel = forwardRef< HTMLDivElement, TabPanelProps >( const { store, instanceId } = context; return ( - ( className={ className } > { children } - + ); } ); diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index a89e680e244d8c..67b7bf588e74e8 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -26,6 +26,9 @@ type Tab = { icon?: IconType; disabled?: boolean; }; + tabpanel?: { + focusable?: boolean; + }; }; const TABS: Tab[] = [ @@ -83,7 +86,11 @@ const UncontrolledTabs = ( { ) ) } { tabs.map( ( tabObj ) => ( - + { tabObj.content } ) ) } @@ -184,6 +191,63 @@ describe( 'Tabs', () => { ); } ); } ); + describe( 'Focus Behavior', () => { + it( 'should focus on the related TabPanel when pressing the Tab key', async () => { + const user = userEvent.setup(); + + render( ); + + const selectedTabPanel = await screen.findByRole( 'tabpanel' ); + + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await user.keyboard( '[Tab]' ); + expect( + await screen.findByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + + // By default the tabpanel should receive focus + await user.keyboard( '[Tab]' ); + expect( selectedTabPanel ).toHaveFocus(); + } ); + it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { + const user = userEvent.setup(); + + const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => + tabObj.id === 'alpha' + ? { + ...tabObj, + content: ( + <> + Selected Tab: Alpha + + + ), + tabpanel: { focusable: false }, + } + : tabObj + ); + + render( + + ); + + const alphaButton = await screen.findByRole( 'button', { + name: /alpha button/i, + } ); + + // Tab should initially focus the first tab in the tablist, which + // is Alpha. + await user.keyboard( '[Tab]' ); + 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 user.keyboard( '[Tab]' ); + expect( alphaButton ).toHaveFocus(); + } ); + } ); describe( 'Tab Attributes', () => { it( "should apply the tab's `className` to the tab button", async () => { diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 88e25eb5a3863c..9874fe6cb6ccfa 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -128,7 +128,7 @@ export type TabPanelProps = { */ children?: React.ReactNode; /** - * A unique identifier for the TabPanel, which is used to generate a unique `id` for the underlying element. + * A unique identifier for the tabpanel, which is used to generate a unique `id` for the underlying element. */ id: string; /** @@ -139,4 +139,12 @@ export type TabPanelProps = { * Custom CSS styles for the rendered `TabPanel` component. */ style?: React.CSSProperties; + /** + * Determines whether or not the tabpanel element should be focusable. + * If `false`, pressing the tab key will skip over the tabpanel, and instead + * focus on the first focusable element in the panel (if there is one). + * + * @default true + */ + focusable?: boolean; }; From 6257737da3c2e2582d7163fb7338fa93f27b4746 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Tue, 7 Nov 2023 08:51:40 -0800 Subject: [PATCH 23/34] `TextControl`: Add opt-in prop for 40px default size (#55471) * `TextControl`: Add opt-in prop for 40px default size * Update changelog * Update snapshots * Remove unnecessary additions and update snapshots * Update default size to 32px --- packages/components/CHANGELOG.md | 1 + packages/components/src/text-control/index.tsx | 6 +++++- packages/components/src/text-control/style.scss | 5 +++++ packages/components/src/text-control/types.ts | 6 ++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8dd8d4b552d9d8..439c45a312d966 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,6 +13,7 @@ ### Enhancements - `ToggleGroupControl`: Add opt-in prop for 40px default size ([#55789](https://github.com/WordPress/gutenberg/pull/55789)). +- `TextControl`: Add opt-in prop for 40px default size ([#55471](https://github.com/WordPress/gutenberg/pull/55471)). ## 25.11.0 (2023-11-02) diff --git a/packages/components/src/text-control/index.tsx b/packages/components/src/text-control/index.tsx index 31b1462a3b3a43..30298357c3c01d 100644 --- a/packages/components/src/text-control/index.tsx +++ b/packages/components/src/text-control/index.tsx @@ -2,6 +2,7 @@ * External dependencies */ import type { ChangeEvent, ForwardedRef } from 'react'; +import classnames from 'classnames'; /** * WordPress dependencies @@ -22,6 +23,7 @@ function UnforwardedTextControl( ) { const { __nextHasNoMarginBottom, + __next40pxDefaultSize = false, label, hideLabelFromVision, value, @@ -46,7 +48,9 @@ function UnforwardedTextControl( className={ className } > Date: Tue, 7 Nov 2023 19:15:47 +0200 Subject: [PATCH 24/34] [Data views]: Fix pagination on manual input (#55940) --- .../edit-site/src/components/dataviews/pagination.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/dataviews/pagination.js b/packages/edit-site/src/components/dataviews/pagination.js index 806d4bf7987cc2..7948cf01ecfc21 100644 --- a/packages/edit-site/src/components/dataviews/pagination.js +++ b/packages/edit-site/src/components/dataviews/pagination.js @@ -83,16 +83,17 @@ function Pagination( { min={ 1 } max={ totalPages } onChange={ ( value ) => { + const _value = +value; if ( - ! value || - value < 1 || - value > totalPages + ! _value || + _value < 1 || + _value > totalPages ) { return; } onChangeView( { ...view, - page: value, + page: _value, } ); } } step="1" From 1b15cda2b43860e7736ebaf5ca39e5b81a49b2ff Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:41:22 -0500 Subject: [PATCH 25/34] Tabs: Update subcomponents to accept full HTML element props (#55860) --- packages/components/CHANGELOG.md | 1 + packages/components/src/tabs/README.md | 37 ---------------- packages/components/src/tabs/tab.tsx | 12 +++--- packages/components/src/tabs/tablist.tsx | 41 +++++++++--------- packages/components/src/tabs/tabpanel.tsx | 48 ++++++++++----------- packages/components/src/tabs/test/index.tsx | 21 +++------ packages/components/src/tabs/types.ts | 33 -------------- 7 files changed, 57 insertions(+), 136 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 439c45a312d966..fb5e36f1fb6e61 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,7 @@ ### Experimental - `Tabs`: Add `focusable` prop to the `Tabs.TabPanel` sub-component ([#55287](https://github.com/WordPress/gutenberg/pull/55287)) +- `Tabs`: Update sub-components to accept relevant HTML element props ([#55860](https://github.com/WordPress/gutenberg/pull/55860)) ### Enhancements diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index 8fb4e0e73caec2..423216e940584d 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -159,19 +159,6 @@ The children elements, which should be a series of `Tabs.TabPanel` components. - Required: No -###### `className`: `string` - -The class name to apply to the tablist. - -- Required: No -- Default: '' - -###### `style`: `React.CSSProperties` - -Custom CSS styles for the tablist. - -- Required: No - #### Tab ##### Props @@ -182,24 +169,12 @@ The id of the tab, which is prepended with the `Tabs` instance ID. - Required: Yes -###### `style`: `React.CSSProperties` - -Custom CSS styles for the tab. - -- Required: No - ###### `children`: `React.ReactNode` The children elements, generally the text to display on the tab. - Required: No -###### `className`: `string` - -The class name to apply to the tab. - -- Required: No - ###### `disabled`: `boolean` Determines if the tab button should be disabled. @@ -229,18 +204,6 @@ The id of the tabpanel, which is combined with the `Tabs` instance ID and the su - Required: Yes -###### `className`: `string` - -The class name to apply to the tabpanel. - -- Required: No - -###### `style`: `React.CSSProperties` - -Custom CSS styles for the tab. - -- Required: No - ###### `focusable`: `boolean` Determines whether or not the tabpanel element should be focusable. If `false`, pressing the tab key will skip over the tabpanel, and instead focus on the first focusable element in the panel (if there is one). diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index 75b3df1c1ba01e..03e5d80871c56a 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -11,11 +11,12 @@ import type { TabProps } from './types'; import warning from '@wordpress/warning'; import { TabsContext } from './context'; import { Tab as StyledTab } from './styles'; +import type { WordPressComponentProps } from '../context'; -export const Tab = forwardRef< HTMLButtonElement, TabProps >( function Tab( - { children, id, className, disabled, render, style }, - ref -) { +export const Tab = forwardRef< + HTMLButtonElement, + WordPressComponentProps< TabProps, 'button', false > +>( function Tab( { children, id, disabled, render, ...otherProps }, ref ) { const context = useContext( TabsContext ); if ( ! context ) { warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' ); @@ -28,10 +29,9 @@ export const Tab = forwardRef< HTMLButtonElement, TabProps >( function Tab( ref={ ref } store={ store } id={ instancedTabId } - className={ className } - style={ style } disabled={ disabled } render={ render } + { ...otherProps } > { children } diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx index 02255fefd20827..7a53115910796c 100644 --- a/packages/components/src/tabs/tablist.tsx +++ b/packages/components/src/tabs/tablist.tsx @@ -16,25 +16,26 @@ import { forwardRef } from '@wordpress/element'; import type { TabListProps } from './types'; import { useTabsContext } from './context'; import { TabListWrapper } from './styles'; +import type { WordPressComponentProps } from '../context'; -export const TabList = forwardRef< HTMLDivElement, TabListProps >( - function TabList( { children, className, style }, ref ) { - const context = useTabsContext(); - if ( ! context ) { - warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' ); - return null; - } - const { store } = context; - return ( - } - > - { children } - - ); +export const TabList = forwardRef< + HTMLDivElement, + WordPressComponentProps< TabListProps, 'div', false > +>( function TabList( { children, ...otherProps }, ref ) { + const context = useTabsContext(); + if ( ! context ) { + warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' ); + return null; } -); + const { store } = context; + return ( + } + { ...otherProps } + > + { children } + + ); +} ); diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index b5339141a56eca..f477d1d3b4b437 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -16,30 +16,28 @@ import { TabPanel as StyledTabPanel } from './styles'; import warning from '@wordpress/warning'; import { TabsContext } from './context'; +import type { WordPressComponentProps } from '../context'; -export const TabPanel = forwardRef< HTMLDivElement, TabPanelProps >( - function TabPanel( - { children, id, className, style, focusable = true }, - ref - ) { - const context = useContext( TabsContext ); - if ( ! context ) { - warning( '`Tabs.TabPanel` must be wrapped in a `Tabs` component.' ); - return null; - } - const { store, instanceId } = context; - - return ( - - { children } - - ); +export const TabPanel = forwardRef< + HTMLDivElement, + WordPressComponentProps< TabPanelProps, 'div', false > +>( function TabPanel( { children, id, focusable = true, ...otherProps }, ref ) { + const context = useContext( TabsContext ); + if ( ! context ) { + warning( '`Tabs.TabPanel` must be wrapped in a `Tabs` component.' ); + return null; } -); + const { store, instanceId } = context; + + return ( + + { children } + + ); +} ); diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index 67b7bf588e74e8..d2a035e436c194 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -7,7 +7,6 @@ import userEvent from '@testing-library/user-event'; /** * WordPress dependencies */ -import { wordpress, category, media } from '@wordpress/icons'; import { useState } from '@wordpress/element'; /** @@ -15,7 +14,6 @@ import { useState } from '@wordpress/element'; */ import Tabs from '..'; import type { TabsProps } from '../types'; -import type { IconType } from '../../icon'; type Tab = { id: string; @@ -23,7 +21,6 @@ type Tab = { content: React.ReactNode; tab: { className?: string; - icon?: IconType; disabled?: boolean; }; tabpanel?: { @@ -36,19 +33,19 @@ const TABS: Tab[] = [ id: 'alpha', title: 'Alpha', content: 'Selected tab: Alpha', - tab: { className: 'alpha-class', icon: wordpress }, + tab: { className: 'alpha-class' }, }, { id: 'beta', title: 'Beta', content: 'Selected tab: Beta', - tab: { className: 'beta-class', icon: category }, + tab: { className: 'beta-class' }, }, { id: 'gamma', title: 'Gamma', content: 'Selected tab: Gamma', - tab: { className: 'gamma-class', icon: media }, + tab: { className: 'gamma-class' }, }, ]; @@ -58,17 +55,15 @@ const TABS_WITH_DELTA: Tab[] = [ id: 'delta', title: 'Delta', content: 'Selected tab: Delta', - tab: { className: 'delta-class', icon: media }, + tab: { className: 'delta-class' }, }, ]; const UncontrolledTabs = ( { tabs, - showTabIcons = false, ...props }: Omit< TabsProps, 'children' > & { tabs: Tab[]; - showTabIcons?: boolean; } ) => { return ( @@ -79,9 +74,8 @@ const UncontrolledTabs = ( { id={ tabObj.id } className={ tabObj.tab.className } disabled={ tabObj.tab.disabled } - icon={ showTabIcons ? tabObj.tab.icon : undefined } > - { showTabIcons ? null : tabObj.title } + { tabObj.title } ) ) } @@ -100,11 +94,9 @@ const UncontrolledTabs = ( { const ControlledTabs = ( { tabs, - showTabIcons = false, ...props }: Omit< TabsProps, 'children' > & { tabs: Tab[]; - showTabIcons?: boolean; } ) => { const [ selectedTabId, setSelectedTabId ] = useState< string | undefined | null @@ -126,9 +118,8 @@ const ControlledTabs = ( { id={ tabObj.id } className={ tabObj.tab.className } disabled={ tabObj.tab.disabled } - icon={ showTabIcons ? tabObj.tab.icon : undefined } > - { showTabIcons ? null : tabObj.title } + { tabObj.title } ) ) } diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 9874fe6cb6ccfa..8b071937410919 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -4,11 +4,6 @@ // eslint-disable-next-line no-restricted-imports import type * as Ariakit from '@ariakit/react'; -/** - * Internal dependencies - */ -import type { IconType } from '../icon'; - export type TabsContextProps = | { /** @@ -78,14 +73,6 @@ export type TabListProps = { * The children elements, which should be a series of `Tabs.TabPanel` components. */ children?: React.ReactNode; - /** - * The class name to apply to the tablist. - */ - className?: string; - /** - * Custom CSS styles for the rendered tablist. - */ - style?: React.CSSProperties; }; export type TabProps = { @@ -93,22 +80,10 @@ export type TabProps = { * The id of the tab, which is prepended with the `Tabs` instanceId. */ id: string; - /** - * Custom CSS styles for the tab. - */ - style?: React.CSSProperties; /** * The children elements, generally the text to display on the tab. */ children?: React.ReactNode; - /** - * The class name to apply to the tab button. - */ - className?: string; - /** - * The icon used for the tab button. - */ - icon?: IconType; /** * Determines if the tab button should be disabled. * @@ -131,14 +106,6 @@ export type TabPanelProps = { * A unique identifier for the tabpanel, which is used to generate a unique `id` for the underlying element. */ id: string; - /** - * The class name to apply to the tabpanel. - */ - className?: string; - /** - * Custom CSS styles for the rendered `TabPanel` component. - */ - style?: React.CSSProperties; /** * Determines whether or not the tabpanel element should be focusable. * If `false`, pressing the tab key will skip over the tabpanel, and instead From cad8faebd613d5e821fe7f1eaf568fa4b56b6f88 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 8 Nov 2023 14:38:05 +1100 Subject: [PATCH 26/34] Global Style Revisions: ensure consistent back button behaviour (#55881) * When applying a revision the route should switch to the global styles panel. At the moment, it's using `goBack` which means the behaviour is inconsistent depending on the previous action. * Update packages/edit-site/src/components/global-styles/screen-revisions/index.js Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> * Update packages/edit-site/src/components/global-styles/screen-revisions/index.js Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --------- Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --- .../src/components/global-styles/screen-revisions/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 46a7a8c74ab692..d0c256b6905ec2 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -32,7 +32,7 @@ const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( ); function ScreenRevisions() { - const { goBack } = useNavigator(); + const { goTo } = useNavigator(); const { user: userConfig, setUserConfig } = useContext( GlobalStylesContext ); const { blocks, editorCanvasContainerView } = useSelect( ( select ) => { @@ -58,13 +58,13 @@ function ScreenRevisions() { useEffect( () => { if ( editorCanvasContainerView !== 'global-styles-revisions' ) { - goBack(); + goTo( '/' ); // Return to global styles main panel. setEditorCanvasContainerView( editorCanvasContainerView ); } }, [ editorCanvasContainerView ] ); const onCloseRevisions = () => { - goBack(); + goTo( '/' ); // Return to global styles main panel. }; const restoreRevision = ( revision ) => { From 48c74db0427bba509d15c327009ca3bcba9cf408 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 8 Nov 2023 09:50:14 +0200 Subject: [PATCH 27/34] Use type=submit for form-submit buttons (#55690) --- packages/block-library/src/form-submit-button/edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/form-submit-button/edit.js b/packages/block-library/src/form-submit-button/edit.js index f8d7a65c6877a6..4b22b26fd4755c 100644 --- a/packages/block-library/src/form-submit-button/edit.js +++ b/packages/block-library/src/form-submit-button/edit.js @@ -14,6 +14,7 @@ const TEMPLATE = [ { text: __( 'Submit' ), tagName: 'button', + type: 'submit', }, ], ], From e4c8ef8456bfe4c15ebd505dcb2ccf572eeab071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:50:05 +0100 Subject: [PATCH 28/34] DataViews: simplify filters API (#55917) --- .../src/components/dataviews/README.md | 70 ++++++----------- .../src/components/dataviews/filters.js | 78 ++++++++----------- .../src/components/dataviews/in-filter.js | 9 ++- .../src/components/dataviews/view-list.js | 16 ++-- .../src/components/page-pages/index.js | 4 +- 5 files changed, 70 insertions(+), 107 deletions(-) diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/edit-site/src/components/dataviews/README.md index 4b978f1ee78833..64cae92d4e874c 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/edit-site/src/components/dataviews/README.md @@ -59,21 +59,38 @@ Example: - `sort.field`: field used for sorting the dataset. - `sort.direction`: the direction to use for sorting, one of `asc` or `desc`. - `search`: the text search applied to the dataset. -- `filters`: the filters applied to the dataset. See filters section. +- `filters`: the filters applied to the dataset. Each item describes: + - `field`: which field this filter is bound to. + - `operator`: which type of filter it is. Only `in` available at the moment. + - `vaule`: the actual value selected by the user. - `visibleFilters`: the `id` of the filters that are visible in the UI. - `hiddenFields`: the `id` of the fields that are hidden in the UI. - `layout`: ... -Note that it's the consumer's responsibility to provide the data and make sure the dataset corresponds to the view's config (sort, pagination, filters, etc.). +### View <=> data -Example: +The view is a representation of the visible state of the dataset. Note, however, that it's the consumer's responsibility to work with the data provider to make sure the user options defined through the view's config (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. ```js function MyCustomPageList() { const [ view, setView ] = useState( { type: 'list', + perPage: 5, page: 1, - "...": "..." + sort: { + field: 'date', + direction: 'desc', + }, + search: '', + filters: [ + { field: 'author', operator: 'in', value: 2 }, + { field: 'status', operator: 'in', value: 'publish,draft' } + ], + visibleFilters: [ 'author', 'status' ], + hiddenFields: [ 'date', 'featured-image' ], + layout: {}, } ); const queryArgs = useMemo( () => { @@ -143,7 +160,7 @@ Example: { value: 1, label: 'Admin' } { value: 2, label: 'User' } ] - filters: [ 'enumeration' ], + filters: [ 'in' ], } ] ``` @@ -153,45 +170,4 @@ Example: - `getValue`: function that returns the value of the field. - `render`: function that renders the field. - `elements`: the set of valid values for the field's value. -- `filters`: what filters are available for the user to use. See filters section. - -## Filters - -Filters describe the conditions a record should match to be listed as part of the dataset. Filters are provided per field. - -```js -const field = [ - { - id: 'author', - filters: [ 'enumeration' ], - } -]; - - -``` - -A filter is an object that may contain the following properties: - -- `type`: the type of filter. Only `enumeration` is supported at the moment. -- `elements`: for filters of type `enumeration`, the list of options to show. A one-dimensional array of object with value/label keys, as in `[ { value: 1, label: "Value name" } ]`. - - `value`: what's serialized into the view's filters. - - `label`: nice-looking name for users. - -As a convenience, field's filter can provide abbreviated versions for the filter. All of following examples result in the same filter: - -```js -const field = [ - { - id: 'author', - header: __( 'Author' ), - elements: authors, - filters: [ - 'enumeration', - { type: 'enumeration' }, - { type: 'enumeration', elements: authors }, - ], - } -]; -``` +- `filters`: what filter operators are available for the user to use over this field. Only `in` available at the moment. diff --git a/packages/edit-site/src/components/dataviews/filters.js b/packages/edit-site/src/components/dataviews/filters.js index 0e7466f3737420..655bd837322934 100644 --- a/packages/edit-site/src/components/dataviews/filters.js +++ b/packages/edit-site/src/components/dataviews/filters.js @@ -6,67 +6,55 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import InFilter from './in-filter'; +import { default as InFilter, OPERATOR_IN } from './in-filter'; +const VALID_OPERATORS = [ OPERATOR_IN ]; export default function Filters( { fields, view, onChangeView } ) { - const filterIndex = {}; + const filtersRegistered = []; fields.forEach( ( field ) => { if ( ! field.filters ) { return; } field.filters.forEach( ( filter ) => { - const id = field.id; - if ( 'string' === typeof filter ) { - filterIndex[ id ] = { - id, + if ( VALID_OPERATORS.some( ( operator ) => operator === filter ) ) { + filtersRegistered.push( { + field: field.id, name: field.header, - type: filter, - }; - } - - if ( 'object' === typeof filter ) { - filterIndex[ id ] = { - id, - name: field.header, - type: filter.type, - }; - } - - if ( 'enumeration' === filterIndex[ id ]?.type ) { - const elements = [ - { - value: '', - label: __( 'All' ), - }, - ...( field.elements || [] ), - ]; - filterIndex[ id ] = { - ...filterIndex[ id ], - elements, - }; + operator: filter, + elements: [ + { + value: '', + label: __( 'All' ), + }, + ...( field.elements || [] ), + ], + } ); } } ); } ); - return view.visibleFilters?.map( ( filterName ) => { - const filter = filterIndex[ filterName ]; + return view.visibleFilters?.map( ( fieldName ) => { + const visibleFiltersForField = filtersRegistered.filter( + ( f ) => f.field === fieldName + ); - if ( ! filter ) { + if ( visibleFiltersForField.length === 0 ) { return null; } - if ( filter.type === 'enumeration' ) { - return ( - - ); - } - - return null; + return visibleFiltersForField.map( ( filter ) => { + if ( OPERATOR_IN === filter.operator ) { + return ( + + ); + } + return null; + } ); } ); } diff --git a/packages/edit-site/src/components/dataviews/in-filter.js b/packages/edit-site/src/components/dataviews/in-filter.js index 826e94de652de3..9abd9a2ee21f10 100644 --- a/packages/edit-site/src/components/dataviews/in-filter.js +++ b/packages/edit-site/src/components/dataviews/in-filter.js @@ -6,11 +6,11 @@ import { SelectControl, } from '@wordpress/components'; -const OPERATOR_IN = 'in'; +export const OPERATOR_IN = 'in'; export default ( { filter, view, onChangeView } ) => { const valueFound = view.filters.find( - ( f ) => f.field === filter.id && f.operator === OPERATOR_IN + ( f ) => f.field === filter.field && f.operator === OPERATOR_IN ); const activeValue = @@ -32,11 +32,12 @@ export default ( { filter, view, onChangeView } ) => { options={ filter.elements } onChange={ ( value ) => { const filters = view.filters.filter( - ( f ) => f.field !== filter.id || f.operator !== OPERATOR_IN + ( f ) => + f.field !== filter.field || f.operator !== OPERATOR_IN ); if ( value !== '' ) { filters.push( { - field: filter.id, + field: filter.field, operator: OPERATOR_IN, value, } ); diff --git a/packages/edit-site/src/components/dataviews/view-list.js b/packages/edit-site/src/components/dataviews/view-list.js index cddf316562f346..3793701a52f12e 100644 --- a/packages/edit-site/src/components/dataviews/view-list.js +++ b/packages/edit-site/src/components/dataviews/view-list.js @@ -72,13 +72,11 @@ function HeaderMenu( { dataView, header } ) { if ( header.column.columnDef.filters?.length > 0 && header.column.columnDef.filters.some( - ( f ) => - ( 'string' === typeof f && f === 'enumeration' ) || - ( 'object' === typeof f && f.type === 'enumeration' ) + ( f ) => 'string' === typeof f && f === 'in' ) ) { filter = { - id: header.column.columnDef.id, + field: header.column.columnDef.id, elements: [ { value: '', @@ -149,7 +147,7 @@ function HeaderMenu( { dataView, header } ) { { isFilterable && ( } @@ -169,7 +167,7 @@ function HeaderMenu( { dataView, header } ) { ( f ) => Object.keys( f )[ 0 ].split( ':' - )[ 0 ] === filter.id + )[ 0 ] === filter.field ); // Set the empty item as active if the filter is not set. @@ -204,7 +202,7 @@ function HeaderMenu( { dataView, header } ) { )[ 0 ].split( ':' ); return ( field !== - filter.id || + filter.field || operator !== 'in' ); } @@ -218,8 +216,8 @@ function HeaderMenu( { dataView, header } ) { dataView.setColumnFilters( [ ...otherFilters, { - [ filter.id + ':in' ]: - element.value, + [ filter.field + + ':in' ]: element.value, }, ] ); } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 529b22f01ca530..4aa670853e97e4 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -231,7 +231,7 @@ export default function PagePages() { ); }, - filters: [ 'enumeration' ], + filters: [ 'in' ], elements: authors?.map( ( { id, name } ) => ( { value: id, @@ -244,7 +244,7 @@ export default function PagePages() { getValue: ( { item } ) => statuses?.find( ( { slug } ) => slug === item.status ) ?.name ?? item.status, - filters: [ 'enumeration' ], + filters: [ 'in' ], elements: statuses?.map( ( { slug, name } ) => ( { value: slug, From 9ca6c8f3a531c4a7423d69f245fa1a9bfe0e4d07 Mon Sep 17 00:00:00 2001 From: JorgeVilchez95 <99050272+JorgeVilchez95@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:21:17 +0100 Subject: [PATCH 29/34] "Detach" text change in template options (#55870) --- .../components/template-part-converter/convert-to-regular.js | 2 +- test/e2e/specs/site-editor/template-part.spec.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/template-part-converter/convert-to-regular.js b/packages/edit-site/src/components/template-part-converter/convert-to-regular.js index b1534ad88a999c..4ca21f42fa944a 100644 --- a/packages/edit-site/src/components/template-part-converter/convert-to-regular.js +++ b/packages/edit-site/src/components/template-part-converter/convert-to-regular.js @@ -26,7 +26,7 @@ export default function ConvertToRegularBlocks( { clientId, onClose } ) { onClose(); } } > - { __( 'Detach blocks from template part' ) } + { __( 'Detach' ) } ); } diff --git a/test/e2e/specs/site-editor/template-part.spec.js b/test/e2e/specs/site-editor/template-part.spec.js index d1c215ec2a4949..ff4610a57ba27b 100644 --- a/test/e2e/specs/site-editor/template-part.spec.js +++ b/test/e2e/specs/site-editor/template-part.spec.js @@ -211,9 +211,7 @@ test.describe( 'Template Part', () => { // Detach the paragraph from the header template part. await editor.selectBlocks( templatePartWithParagraph ); - await editor.clickBlockOptionsMenuItem( - 'Detach blocks from template part' - ); + await editor.clickBlockOptionsMenuItem( 'Detach' ); // There should be a paragraph but no header template part. await expect( paragraph ).toBeVisible(); From fd42f04e3d83de7df990bfb558d49f4a15c0a057 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 8 Nov 2023 12:21:41 +0000 Subject: [PATCH 30/34] Dataviews: Add: custom views header indication. (#55926) --- .../custom-dataviews-list.js | 41 +++++++++++-------- .../components/sidebar-dataviews/style.scss | 8 ++++ packages/edit-site/src/style.scss | 1 + 3 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 packages/edit-site/src/components/sidebar-dataviews/style.scss diff --git a/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js b/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js index f9b0cddb7d8e1e..a8aef191c445e1 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js +++ b/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js @@ -3,8 +3,12 @@ */ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalHeading as Heading, +} from '@wordpress/components'; import { useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -70,20 +74,25 @@ export function useCustomDataViews( type ) { export default function CustomDataViewsList( { type, activeView, isCustom } ) { const customDataViews = useCustomDataViews( type ); return ( - - { customDataViews.map( ( customViewRecord ) => { - return ( - - ); - } ) } - - + <> +
    + { __( 'Custom Views' ) } +
    + + { customDataViews.map( ( customViewRecord ) => { + return ( + + ); + } ) } + + + ); } diff --git a/packages/edit-site/src/components/sidebar-dataviews/style.scss b/packages/edit-site/src/components/sidebar-dataviews/style.scss new file mode 100644 index 00000000000000..3e8812267b076d --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/style.scss @@ -0,0 +1,8 @@ +.edit-site-sidebar-navigation-screen-dataviews__group-header { + margin-top: $grid-unit-40; + h2 { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + } +} diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index dadbf48d06e64d..0b49f48a3e5845 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -39,6 +39,7 @@ @import "./components/sidebar-navigation-screen-pattern/style.scss"; @import "./components/sidebar-navigation-screen-patterns/style.scss"; @import "./components/sidebar-navigation-screen-template/style.scss"; +@import "./components/sidebar-dataviews/style.scss"; @import "./components/site-hub/style.scss"; @import "./components/sidebar-navigation-screen-navigation-menus/style.scss"; @import "./components/site-icon/style.scss"; From f8cfba61d55b02e75858ea3a3dd08ac7e702d221 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:44:48 +0100 Subject: [PATCH 31/34] Server directive processing: Process only root blocks (#55739) * Test that we can identify the real root blocks * Add some tests, fix yoda condition and linting issues * Refactor to static function filter * Small nitpicks * Update test with a pattern * Refactor and use test for counting root blocks * Simplify tests * Fix e2e tests --------- Co-authored-by: Luis Herranz --- .../directive-processing.php | 84 ++++++++------- .../e2e-tests/plugins/interactive-blocks.php | 4 +- .../directive-processing-test.php | 102 ++++++++++++++---- 3 files changed, 134 insertions(+), 56 deletions(-) diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 41223c08158869..eae731e2438913 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -8,37 +8,9 @@ */ /** - * Process directives in each block. - * - * @param string $block_content The block content. - * @param array $block The full block. - * - * @return string Filtered block content. - */ -function gutenberg_interactivity_process_directives_in_root_blocks( $block_content, $block ) { - // Don't process inner blocks or root blocks that don't contain directives. - if ( ! WP_Directive_Processor::is_root_block( $block ) || strpos( $block_content, 'data-wp-' ) === false ) { - return $block_content; - } - - // TODO: Add some directive/components registration mechanism. - $directives = array( - 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', - ); - - $tags = new WP_Directive_Processor( $block_content ); - $tags = gutenberg_interactivity_process_directives( $tags, 'data-wp-', $directives ); - return $tags->get_updated_html(); -} -add_filter( 'render_block', 'gutenberg_interactivity_process_directives_in_root_blocks', 10, 2 ); - -/** - * Mark the inner blocks with a temporary property so we can discard them later, - * and process only the root blocks. + * Process the Interactivity API directives using the root blocks of the + * outermost rendering, ignoring the root blocks of inner blocks like Patterns, + * Template Parts or Content. * * @param array $parsed_block The parsed block. * @param array $source_block The source block. @@ -46,16 +18,56 @@ function gutenberg_interactivity_process_directives_in_root_blocks( $block_conte * * @return array The parsed block. */ -function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block, $parent_block ) { - if ( ! isset( $parent_block ) ) { +function gutenberg_interactivity_process_directives( $parsed_block, $source_block, $parent_block ) { + static $is_inside_root_block = false; + static $process_directives_in_root_blocks = null; + + if ( ! isset( $process_directives_in_root_blocks ) ) { + /** + * Process directives in each root block. + * + * @param string $block_content The block content. + * @param array $block The full block. + * + * @return string Filtered block content. + */ + $process_directives_in_root_blocks = static function ( $block_content, $block ) use ( &$is_inside_root_block ) { + + if ( WP_Directive_Processor::is_root_block( $block ) ) { + + $directives = array( + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + ); + + $tags = new WP_Directive_Processor( $block_content ); + $tags = gutenberg_interactivity_process_rendered_html( $tags, 'data-wp-', $directives ); + $is_inside_root_block = false; + return $tags->get_updated_html(); + + } + + return $block_content; + }; + add_filter( 'render_block', $process_directives_in_root_blocks, 10, 2 ); + } + + if ( ! isset( $parent_block ) && ! $is_inside_root_block ) { WP_Directive_Processor::add_root_block( $parsed_block ); + $is_inside_root_block = true; } + return $parsed_block; } -add_filter( 'render_block_data', 'gutenberg_interactivity_mark_inner_blocks', 10, 3 ); +add_filter( 'render_block_data', 'gutenberg_interactivity_process_directives', 10, 3 ); + /** - * Process directives. + * Traverses the HTML searching for Interactivity API directives and processing + * them. * * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. * @param string $prefix Attribute prefix. @@ -64,7 +76,7 @@ function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block * @return WP_Directive_Processor The modified instance of the * WP_Directive_Processor. */ -function gutenberg_interactivity_process_directives( $tags, $prefix, $directives ) { +function gutenberg_interactivity_process_rendered_html( $tags, $prefix, $directives ) { $context = new WP_Directive_Context(); $tag_stack = array(); diff --git a/packages/e2e-tests/plugins/interactive-blocks.php b/packages/e2e-tests/plugins/interactive-blocks.php index a6bd468493840d..956508a11361e4 100644 --- a/packages/e2e-tests/plugins/interactive-blocks.php +++ b/packages/e2e-tests/plugins/interactive-blocks.php @@ -39,8 +39,8 @@ function () { // HTML is not correct or malformed. if ( 'true' === $_GET['disable_directives_ssr'] ) { remove_filter( - 'render_block', - 'gutenberg_interactivity_process_directives_in_root_blocks' + 'render_block_data', + 'gutenberg_interactivity_process_directives' ); } } diff --git a/phpunit/experimental/interactivity-api/directive-processing-test.php b/phpunit/experimental/interactivity-api/directive-processing-test.php index 44838b4ce68d3e..97dddba3c263f7 100644 --- a/phpunit/experimental/interactivity-api/directive-processing-test.php +++ b/phpunit/experimental/interactivity-api/directive-processing-test.php @@ -28,10 +28,10 @@ function gutenberg_test_process_directives_helper_increment( $store ) { } /** - * Tests for the gutenberg_interactivity_process_directives function. + * Tests for the gutenberg_interactivity_process_rendered_html function. * * @group interactivity-api - * @covers gutenberg_interactivity_process_directives + * @covers gutenberg_interactivity_process_rendered_html */ class Tests_Process_Directives extends WP_UnitTestCase { public function test_correctly_call_attribute_directive_processor_on_closing_tag() { @@ -40,19 +40,19 @@ public function test_correctly_call_attribute_directive_processor_on_closing_tag $test_helper = $this->createMock( Helper_Class::class ); $test_helper->expects( $this->exactly( 2 ) ) - ->method( 'process_foo_test' ) - ->with( - $this->callback( - function ( $p ) { - return 'DIV' === $p->get_tag() && ( - // Either this is a closing tag... - $p->is_tag_closer() || - // ...or it is an open tag, and has the directive attribute set. - ( ! $p->is_tag_closer() && 'abc' === $p->get_attribute( 'foo-test' ) ) - ); - } - ) - ); + ->method( 'process_foo_test' ) + ->with( + $this->callback( + function ( $p ) { + return 'DIV' === $p->get_tag() && ( + // Either this is a closing tag... + $p->is_tag_closer() || + // ...or it is an open tag, and has the directive attribute set. + ( ! $p->is_tag_closer() && 'abc' === $p->get_attribute( 'foo-test' ) ) + ); + } + ) + ); $directives = array( 'foo-test' => array( $test_helper, 'process_foo_test' ), @@ -60,13 +60,13 @@ function ( $p ) { $markup = '
    Example:
    This is a test>
    Here is a nested div
    '; $tags = new WP_HTML_Tag_Processor( $markup ); - gutenberg_interactivity_process_directives( $tags, 'foo-', $directives ); + gutenberg_interactivity_process_rendered_html( $tags, 'foo-', $directives ); } public function test_directives_with_double_hyphen_processed_correctly() { $test_helper = $this->createMock( Helper_Class::class ); $test_helper->expects( $this->atLeastOnce() ) - ->method( 'process_foo_test' ); + ->method( 'process_foo_test' ); $directives = array( 'foo-test' => array( $test_helper, 'process_foo_test' ), @@ -74,10 +74,76 @@ public function test_directives_with_double_hyphen_processed_correctly() { $markup = '
    '; $tags = new WP_HTML_Tag_Processor( $markup ); - gutenberg_interactivity_process_directives( $tags, 'foo-', $directives ); + gutenberg_interactivity_process_rendered_html( $tags, 'foo-', $directives ); + } + + public function test_interactivity_process_directives_in_root_blocks() { + $pattern_content = + '' . + '

    Pattern Content Block 1

    ' . + '' . + '' . + '

    Pattern Content Block 2

    ' . + ''; + register_block_pattern( + 'core/interactivity-pattern', + array( + 'title' => 'Interactivity Pattern', + 'content' => $pattern_content, + ) + ); + + $providers = $this->data_only_root_blocks_are_processed(); + foreach ( $providers as $provider ) { + do_blocks( $provider['page_content'] ); + $this->assertSame( $provider['root_blocks'], count( WP_Directive_Processor::$root_blocks ) ); + + } + } + + /** + * Data provider . + * + * @return array + **/ + public function data_only_root_blocks_are_processed() { + + return array( + array( + 'root_blocks' => 2, + 'page_content' => + '' . + '
    + ' . + '

    The XYZ Doohickey Company was founded in 1971, and has been providing' . + 'quality doohickeys to the public ever since. Located in Gotham City, XYZ employs' . + 'over 2,000 people and does all kinds of awesome things for the Gotham community.

    ' . + ' +
    ' . + '' . + '' . + '
    + ' . + '

    The XYZ Doohickey Company was founded in 1971, and has been providing' . + 'quality doohickeys to the public ever since. Located in Gotham City, XYZ employs' . + 'over 2,000 people and does all kinds of awesome things for the Gotham community.

    ' . + ' +
    ' . + '', + ), + array( + 'root_blocks' => 2, + 'page_content' => + '' . + '

    Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

    ' . + '' . + '', + ), + ); } } + /** * Tests for the gutenberg_interactivity_evaluate_reference function. * From 08f0b7aebae8efe47f5d5b97ca02d952482eae96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:56:37 +0100 Subject: [PATCH 32/34] DataViews: document actions (#55959) --- .../edit-site/src/components/dataviews/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/edit-site/src/components/dataviews/README.md index 64cae92d4e874c..7a5abc8ad485da 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/edit-site/src/components/dataviews/README.md @@ -171,3 +171,15 @@ Example: - `render`: function that renders the field. - `elements`: the set of valid values for the field's value. - `filters`: what filter operators are available for the user to use over this field. Only `in` available at the moment. + +## Actions + +Array of operations that can be performed upon each record. Each action is an object with the following properties: + +- `id`: string, required. Unique identifier of the action. For example, `move-to-trash`. +- `label`: string, required. User facing description of the action. For example, `Move to Trash`. +- `isPrimary`: boolean, optional. Whether the action should be listed inline (primary) or in hidden in the more actions menu (secondary). +- `icon`: icon to show for primary actions. It's required for a primary action, otherwise the action would be considered secondary. +- `isEligible`: function, optional. Whether the action can be performed for a given record. If not present, the action is considered to be eligible for all items. It takes the given record as input. +- `isDestructive`: boolean, optional. Whether the action can delete data, in which case the UI would communicate it via red color. +- `perform`: function, required. Function that takes the record as input and performs the required action. From a522e392cfe9271d7e09878abc4278ed4c398a00 Mon Sep 17 00:00:00 2001 From: flootr Date: Wed, 8 Nov 2023 14:06:02 +0100 Subject: [PATCH 33/34] `DisclosureContent`: migrate from `reakit` to `@ariakit/react` (#55639) * `DisclosureContent`: migrate from `reakit` to `@ariakit/react` * add changelog entry * include `WordPressComponentProps` & spread props * move changelog entry to UNRELEASED * adjust comment to mention `Ariakit` rather than `reakit` * remove link to `ariakit` docs & comment about future plans --- packages/components/CHANGELOG.md | 1 + packages/components/src/disclosure/index.js | 11 ----- packages/components/src/disclosure/index.tsx | 44 ++++++++++++++++++++ packages/components/src/disclosure/types.tsx | 10 +++++ 4 files changed, 55 insertions(+), 11 deletions(-) delete mode 100644 packages/components/src/disclosure/index.js create mode 100644 packages/components/src/disclosure/index.tsx create mode 100644 packages/components/src/disclosure/types.tsx diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index fb5e36f1fb6e61..5d05abc7026b22 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,7 @@ ### Internal - Migrate `Divider` from `reakit` to `ariakit` ([#55622](https://github.com/WordPress/gutenberg/pull/55622)) +- Migrate `DisclosureContent` from `reakit` to `ariakit` and TypeScript ([#55639](https://github.com/WordPress/gutenberg/pull/55639)) ### Experimental diff --git a/packages/components/src/disclosure/index.js b/packages/components/src/disclosure/index.js deleted file mode 100644 index 5458ba053eef6a..00000000000000 --- a/packages/components/src/disclosure/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Accessible Disclosure component that controls visibility of a section of - * content. It follows the WAI-ARIA Disclosure Pattern. - * - * @see https://reakit.io/docs/disclosure/ - * - * The plan is to build own API that accounts for future breaking changes - * in Reakit (https://github.com/WordPress/gutenberg/pull/28085). - */ -/* eslint-disable-next-line no-restricted-imports */ -export { DisclosureContent } from 'reakit'; diff --git a/packages/components/src/disclosure/index.tsx b/packages/components/src/disclosure/index.tsx new file mode 100644 index 00000000000000..5bacfcabc349a6 --- /dev/null +++ b/packages/components/src/disclosure/index.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DisclosureContentProps } from './types'; +import type { WordPressComponentProps } from '../context'; + +/** + * Accessible Disclosure component that controls visibility of a section of + * content. It follows the WAI-ARIA Disclosure Pattern. + */ +const UnforwardedDisclosureContent = ( + { + visible, + children, + ...props + }: WordPressComponentProps< DisclosureContentProps, 'div', false >, + ref: React.ForwardedRef< any > +) => { + const disclosure = Ariakit.useDisclosureStore( { open: visible } ); + + return ( + + { children } + + ); +}; + +export const DisclosureContent = forwardRef( UnforwardedDisclosureContent ); +export default DisclosureContent; diff --git a/packages/components/src/disclosure/types.tsx b/packages/components/src/disclosure/types.tsx new file mode 100644 index 00000000000000..6a0a746bb6397f --- /dev/null +++ b/packages/components/src/disclosure/types.tsx @@ -0,0 +1,10 @@ +export type DisclosureContentProps = { + /** + * If set to `true` the content will be shown, otherwise it's hidden. + */ + visible?: boolean; + /** + * The content to display within the component. + */ + children: React.ReactNode; +}; From e50aabe77c228b02c2d8b02cb1be4454db370cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:11:58 +0100 Subject: [PATCH 34/34] DataViews: add control to reset all filters at once (#55955) * Add reset filters button * Set proper spacing/alignment for control * Fix lint * Also reset search * Disable reset button if no filters active * Properly manage keys * Managed undefined visibleFilters --- .../src/components/dataviews/filters.js | 58 ++++++++++++------- .../src/components/dataviews/reset-filters.js | 26 +++++++++ 2 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 packages/edit-site/src/components/dataviews/reset-filters.js diff --git a/packages/edit-site/src/components/dataviews/filters.js b/packages/edit-site/src/components/dataviews/filters.js index 655bd837322934..e34ba84040a95d 100644 --- a/packages/edit-site/src/components/dataviews/filters.js +++ b/packages/edit-site/src/components/dataviews/filters.js @@ -7,6 +7,8 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { default as InFilter, OPERATOR_IN } from './in-filter'; +import ResetFilters from './reset-filters'; + const VALID_OPERATORS = [ OPERATOR_IN ]; export default function Filters( { fields, view, onChangeView } ) { @@ -34,27 +36,41 @@ export default function Filters( { fields, view, onChangeView } ) { } ); } ); - return view.visibleFilters?.map( ( fieldName ) => { - const visibleFiltersForField = filtersRegistered.filter( - ( f ) => f.field === fieldName - ); + const visibleFilters = view.visibleFilters + ?.map( ( fieldName ) => { + const visibleFiltersForField = filtersRegistered.filter( + ( f ) => f.field === fieldName + ); - if ( visibleFiltersForField.length === 0 ) { - return null; - } - - return visibleFiltersForField.map( ( filter ) => { - if ( OPERATOR_IN === filter.operator ) { - return ( - - ); + if ( visibleFiltersForField.length === 0 ) { + return null; } - return null; - } ); - } ); + + return visibleFiltersForField.map( ( filter ) => { + if ( OPERATOR_IN === filter.operator ) { + return ( + + ); + } + return null; + } ); + } ) + .filter( Boolean ); + + if ( visibleFilters?.length > 0 ) { + visibleFilters.push( + + ); + } + + return visibleFilters; } diff --git a/packages/edit-site/src/components/dataviews/reset-filters.js b/packages/edit-site/src/components/dataviews/reset-filters.js new file mode 100644 index 00000000000000..68b5c17590c977 --- /dev/null +++ b/packages/edit-site/src/components/dataviews/reset-filters.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { BaseControl, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +export default ( { view, onChangeView } ) => { + return ( + + + + ); +};