';
- }
-
- $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 = '
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.