diff --git a/backport-changelog/6.8/8212.md b/backport-changelog/6.8/8212.md index 30483af9e7b71..2a0019eae2809 100644 --- a/backport-changelog/6.8/8212.md +++ b/backport-changelog/6.8/8212.md @@ -2,3 +2,4 @@ https://github.com/WordPress/wordpress-develop/pull/8212 * https://github.com/WordPress/gutenberg/pull/68926 * https://github.com/WordPress/gutenberg/pull/69142 +* https://github.com/WordPress/gutenberg/pull/69241 diff --git a/backport-changelog/6.8/8261.md b/backport-changelog/6.8/8261.md new file mode 100644 index 0000000000000..d125d122cb3a4 --- /dev/null +++ b/backport-changelog/6.8/8261.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/8261 + +* https://github.com/WordPress/gutenberg/pull/68521 diff --git a/backport-changelog/6.8/8274.md b/backport-changelog/6.8/8274.md new file mode 100644 index 0000000000000..f841a6f010d6d --- /dev/null +++ b/backport-changelog/6.8/8274.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/8274 + +* https://github.com/WordPress/gutenberg/pull/69096 diff --git a/changelog.txt b/changelog.txt index 3a8e2bafd9567..68e13890ff280 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,191 +1,14 @@ == Changelog == -= 20.3.0-rc.2 = - -## Changelog - -### Enhancements - -- Disable hover animation on preview frame for classic themes. ([68976](https://github.com/WordPress/gutenberg/pull/68976)) - -#### Block Library -- Added discord in social links. ([68848](https://github.com/WordPress/gutenberg/pull/68848)) -- Featured Image block: Use resolution tool component. ([68471](https://github.com/WordPress/gutenberg/pull/68471)) -- Query block: Add option to ignore sticky posts behavior. ([69057](https://github.com/WordPress/gutenberg/pull/69057)) -- RSS: Border & Spacing support. ([66411](https://github.com/WordPress/gutenberg/pull/66411)) - -#### Global Styles -- Disable Clear button if there's no shadow. ([69092](https://github.com/WordPress/gutenberg/pull/69092)) -- Duotone Settings: Add `reset` button and improve toggle rendering in FiltersPanel. ([68672](https://github.com/WordPress/gutenberg/pull/68672)) -- Shadow Panel: Add reset button. ([68981](https://github.com/WordPress/gutenberg/pull/68981)) - -#### Post Editor -- Editor: Add loading state to the 'PageAttributesParent' component. ([69062](https://github.com/WordPress/gutenberg/pull/69062)) -- Editor: Add loading state to the 'PostAuthorCombobox' component. ([68991](https://github.com/WordPress/gutenberg/pull/68991)) -- Editor: Display error message when loading current post fails. ([68999](https://github.com/WordPress/gutenberg/pull/68999)) - -#### Block Editor -- Quick Inserter: Restore pattern search and insertion. ([69028](https://github.com/WordPress/gutenberg/pull/69028)) -- Update keyboard shortcuts to use `primaryShift+backspace` for block deletion. ([69074](https://github.com/WordPress/gutenberg/pull/69074)) - -#### Design Tools -- Archives: Add Color Support. ([68685](https://github.com/WordPress/gutenberg/pull/68685)) -- Categories: Add Color Support. ([68686](https://github.com/WordPress/gutenberg/pull/68686)) - -#### Components -- ComboboxControl: Add an `isLoading` prop to show a loading spinner. ([68990](https://github.com/WordPress/gutenberg/pull/68990)) - += 20.0.2 = ### Bug Fixes -- Exclude Iterator helpers from polyfills. ([69070](https://github.com/WordPress/gutenberg/pull/69070)) -- Fix Dependabot ignore statements. ([69144](https://github.com/WordPress/gutenberg/pull/69144)) - #### Block Editor -- Block popover: Fix scrolling over. ([68075](https://github.com/WordPress/gutenberg/pull/68075)) -- Display root appender when default block is disabled. ([68951](https://github.com/WordPress/gutenberg/pull/68951)) -- Fix regression for root appender logic. ([68994](https://github.com/WordPress/gutenberg/pull/68994)) -- Inserter: Remove block default icon from no results message. ([68693](https://github.com/WordPress/gutenberg/pull/68693)) -- Rename `aspect` property to `ratio` to carry the `defaultAspect` in `AspectRatioDropdown`. ([69085](https://github.com/WordPress/gutenberg/pull/69085)) -- Writing Flow: Restore early return for no block selection in tab nav hook. ([69079](https://github.com/WordPress/gutenberg/pull/69079)) - -#### Site Editor -- Edit Site: Fix Fields package private APIs error. ([68964](https://github.com/WordPress/gutenberg/pull/68964)) -- Fix: Site Editor should display a 404 message. ([69009](https://github.com/WordPress/gutenberg/pull/69009)) -- Use the same editor component for all routes. ([69093](https://github.com/WordPress/gutenberg/pull/69093)) +- Fix 'isBlockVisibleInTheInserter' selector helper performance ([68898](https://github.com/WordPress/gutenberg/pull/68898)) -#### Global Styles -- Fix: Additional CSS button not working after back navigation. ([68954](https://github.com/WordPress/gutenberg/pull/68954)) -- Fix: Missing 'No blocks found.' message for block search in editor. ([69036](https://github.com/WordPress/gutenberg/pull/69036)) - -#### Block Library -- Query Block: Fix 'parents' argument validation. ([68983](https://github.com/WordPress/gutenberg/pull/68983)) -- Social Links: Fix appender size in non-iframe editor. ([68215](https://github.com/WordPress/gutenberg/pull/68215)) - -#### REST API -- Add support for the 'ignore_sticky_posts' argument. ([68970](https://github.com/WordPress/gutenberg/pull/68970)) -- Fix: Prevent Errors in Header Processing and Encode URLs Properly. ([67780](https://github.com/WordPress/gutenberg/pull/67780)) - -#### Icons -- Fix the background, arrowUpLeft, keyboardReturn and square icons. ([69076](https://github.com/WordPress/gutenberg/pull/69076)) - -#### Font Library -- Refactor font variant components to use useId for checkbox IDs. ([69050](https://github.com/WordPress/gutenberg/pull/69050)) - -#### Block Directory -- Remove block icon from InstalledBlocksPrePublishPanel. ([69046](https://github.com/WordPress/gutenberg/pull/69046)) - -#### DataViews -- Fixed: Empty Pattern Overlap in Pattern Title in Dataviews Table Layout. ([68997](https://github.com/WordPress/gutenberg/pull/68997)) - -#### Media -- Add optional chain to sizes indexing of media details in edit-site. ([68995](https://github.com/WordPress/gutenberg/pull/68995)) - -#### CSS & Styling -- Enabled Full height in Additional CSS. ([68993](https://github.com/WordPress/gutenberg/pull/68993)) - -#### Block hooks -- Fix truncation of post content. ([68926](https://github.com/WordPress/gutenberg/pull/68926)) - -#### Interactivity API -- iAPI Router: Fix CSS rule order in some constructed style sheets. ([68923](https://github.com/WordPress/gutenberg/pull/68923)) -- iAPI Router: Revert "Handle styles assets on region-based navigation" ([69222](https://github.com/WordPress/gutenberg/pull/69222)) - - -### Accessibility - -#### Components -- Font Size Picker: Remove Custom option from FontSizePickerSelect dropdown. ([69038](https://github.com/WordPress/gutenberg/pull/69038)) - -#### Global Styles -- Add missing list role to the list of blocks in the global Styles. ([69027](https://github.com/WordPress/gutenberg/pull/69027)) - -#### Block API -- Block support: Preserve aria-label value in comment delimiter. ([69002](https://github.com/WordPress/gutenberg/pull/69002)) - -#### Block Editor -- [Block Editor]: A11y - Add and Update missing reduce-motion mixing. ([68417](https://github.com/WordPress/gutenberg/pull/68417)) - - -### Performance - -#### Post Editor -- Editor: Don't use selector shortcuts for the taxonomy queries. ([68998](https://github.com/WordPress/gutenberg/pull/68998)) -- Editor: Optimize 'PostAuthorCheck' component data selection. ([69105](https://github.com/WordPress/gutenberg/pull/69105)) - -#### Style Book -- Improve StyleBook resize responsiveness for Classic Theme. ([68980](https://github.com/WordPress/gutenberg/pull/68980)) - - -### Documentation - -- Added Missing Global Documentation. ([69104](https://github.com/WordPress/gutenberg/pull/69104)) -- Changed Inline Document Order. ([68992](https://github.com/WordPress/gutenberg/pull/68992)) -- wp-env: Add lifecycleScripts to the schema. ([68724](https://github.com/WordPress/gutenberg/pull/68724)) - - -### Code Quality - -- Core Data: Add type for term entity. ([69151](https://github.com/WordPress/gutenberg/pull/69151)) -- iAPI Router: Add missing changelog entry for #68923. ([68945](https://github.com/WordPress/gutenberg/pull/68945)) - -#### Block Library -- E2E: Add regression test for spacer block in themes without spacing units. ([68913](https://github.com/WordPress/gutenberg/pull/68913)) -- Navigation Link Block: Use stable variable for underline color styling. ([68953](https://github.com/WordPress/gutenberg/pull/68953)) -- Regenerate block fixtures. ([68982](https://github.com/WordPress/gutenberg/pull/68982)) -- Social Links: Remove redundant reduce-motion mixin. ([69000](https://github.com/WordPress/gutenberg/pull/69000)) - -#### Site Editor -- Fast follow: Redirections of deprecated site editor URLs. ([68971](https://github.com/WordPress/gutenberg/pull/68971)) -- Quality: Remove unused props and styles from SidebarNavigationScreen. ([68972](https://github.com/WordPress/gutenberg/pull/68972)) - -#### Block Editor -- Inserter: Remove unused no-results-icon styles. ([69018](https://github.com/WordPress/gutenberg/pull/69018)) - - -### Tools - -- PR Template: Suggest linking the issue. ([68924](https://github.com/WordPress/gutenberg/pull/68924)) -- Relocate changelog file for WP#6910 to 6.9 backports. ([69068](https://github.com/WordPress/gutenberg/pull/69068)) - -#### Build Tooling -- Fix installing svn during deploys. ([69047](https://github.com/WordPress/gutenberg/pull/69047)) -- Remove `react-native` dependabot group. ([69118](https://github.com/WordPress/gutenberg/pull/69118)) - -#### Testing -- e2e: Fix "add new" selector. ([69111](https://github.com/WordPress/gutenberg/pull/69111)) - - -### Various - -- Remove react-native dependabot group - Take 2. ([69122](https://github.com/WordPress/gutenberg/pull/69122)) - -#### Plugin -- Code Quality: Delete unused function from PHP Sync Issue generation script. ([68947](https://github.com/WordPress/gutenberg/pull/68947)) -- npm scripts: Use `node -p` instead of `echo`. ([68946](https://github.com/WordPress/gutenberg/pull/68946)) - - -## First-time contributors - -The following PRs were merged by first-time contributors: - -- @benazeer-ben: RSS: Border & Spacing support. ([66411](https://github.com/WordPress/gutenberg/pull/66411)) -- @grgar: Add optional chain to sizes indexing of media details in edit-site. ([68995](https://github.com/WordPress/gutenberg/pull/68995)) -- @Gulamdastgir-Momin: Added discord in social links. ([68848](https://github.com/WordPress/gutenberg/pull/68848)) -- @Juzar10: Fix: Prevent Errors in Header Processing and Encode URLs Properly. ([67780](https://github.com/WordPress/gutenberg/pull/67780)) -- @singhakanshu00: Disable hover animation on preview frame for classic themes. ([68976](https://github.com/WordPress/gutenberg/pull/68976)) -- @srtfisher: wp-env: Add lifecycleScripts to the schema. ([68724](https://github.com/WordPress/gutenberg/pull/68724)) - - -## Contributors - -The following contributors merged PRs in this release: - -@adamsilverstein @afercia @akasunil @benazeer-ben @carolinan @DAreRodz @desrosj @ellatrix @grgar @Gulamdastgir-Momin @himanshupathak95 @Infinite-Null @joemcgill @Juzar10 @Mamaduka @Mayank-Tripathi32 @ockham @peterwilsoncc @Rishit30G @SainathPoojary @shail-mehta @shimotmk @singhakanshu00 @srtfisher @swissspidy @t-hamano @torounit @yogeshbhutkar - - -= 20.3.0-rc.1 = += 20.3.0 = ## Changelog @@ -274,6 +97,7 @@ The following contributors merged PRs in this release: #### Interactivity API - iAPI Router: Fix CSS rule order in some constructed style sheets. ([68923](https://github.com/WordPress/gutenberg/pull/68923)) +- iAPI Router: Revert "Handle styles assets on region-based navigation" ([69222](https://github.com/WordPress/gutenberg/pull/69222)) ### Accessibility diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index f1b5999ae3147..f37316e3dcc52 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -29,7 +29,7 @@ The WordPress project, and Gutenberg in particular, iterates quickly. Staying up ## Additional resources -For more resources on block development and extending the Block Editor, review the additional sections here in the Block Editor Handbook. Further practical examples are also available in the [block-development-examples](https://github.com/wptrainingteam/block-development-examples) GitHub repository. +For more resources on block development and extending the Block Editor, review the additional sections here in the Block Editor Handbook. Further practical examples are also available in the [block-development-examples](https://github.com/WordPress/block-development-examples) GitHub repository. If you are looking for more educational content, check out [Learn WordPress](https://learn.wordpress.org/), where you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/). Here is a selection of current offerings: diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 4cd7c0b36fe86..717626af5437e 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -75,7 +75,7 @@ wp.blocks.registerBlockVariation( For scripts that need to run in the Block Editor, make sure you use the [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) function. -Refer to [Enqueueing assets in the Editor](/docs/how-to-guides/enqueueing-assets-in-the-editor.md) for more information. You can also visit the [block-development-example](https://github.com/wptrainingteam/block-theme-examples/blob/master/example-block-variation/functions.php) GitHub repository for more practical examples. +Refer to [Enqueueing assets in the Editor](/docs/how-to-guides/enqueueing-assets-in-the-editor.md) for more information.
Open your browser's dev tools and try running wp.data.select('core/editor').getBlocks() in the console when editing a post or when using the Site Editor. This command will return all available blocks. diff --git a/docs/how-to-guides/plugin-sidebar-0.md b/docs/how-to-guides/plugin-sidebar-0.md index 76ef54bb9d308..495aa0a1d6b8b 100644 --- a/docs/how-to-guides/plugin-sidebar-0.md +++ b/docs/how-to-guides/plugin-sidebar-0.md @@ -42,7 +42,7 @@ Add the following code to a JavaScript file called `plugin-sidebar.js` and save } )( window.wp, window.React ); ``` -For this code to work, those utilities need to be available in the browser, so you must specify `wp-plugins`, `wp-edit-post`, and `react` as dependencies of your script. +For this code to work, those utilities need to be available in the browser, so you must specify `wp-plugins`, `wp-editor`, and `react` as dependencies of your script. Here is the PHP code to register your script and specify the dependencies: diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md index 359b36b4ad205..e0b7653321413 100644 --- a/docs/how-to-guides/themes/global-settings-and-styles.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -1034,7 +1034,7 @@ h3 { {% end %} ##### Element pseudo selectors -Pseudo selectors `:hover`, `:focus`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. +Pseudo selectors `:hover`, `:focus`, `:focus-visible`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. ```json "elements": { diff --git a/docs/reference-guides/block-api/block-variations.md b/docs/reference-guides/block-api/block-variations.md index 8a0c6b1dd5bd6..8c223e54eea29 100644 --- a/docs/reference-guides/block-api/block-variations.md +++ b/docs/reference-guides/block-api/block-variations.md @@ -60,6 +60,44 @@ wp.blocks.registerBlockVariation( 'core/embed', { } ); ``` +## Registering block variations in PHP + +Block variations can also be registered from PHP using the `get_block_type_variations` filter hook. This approach is particularly useful when you need to dynamically generate variations based on registered post types, taxonomies, or other WordPress data. + +Here's an example of how to register a custom variation for the `core/image` block: + +```php +function my_custom_image_variation( $variations, $block_type ) { + // Only modify variations for the image block + if ( 'core/image' !== $block_type->name ) { + return $variations; + } + + // Add a custom variation + $variations[] = array( + 'name' => 'wide-image', + 'title' => __( 'Wide image', 'textdomain' ), + 'description' => __( 'A wide image', 'textdomain' ), + 'scope' => array( 'inserter' ), + 'isDefault' => false, + 'attributes' => array( + 'align' => 'wide', // Identifies the link type as custom + ), + ); + + return $variations; +} +add_filter( 'get_block_type_variations', 'my_custom_image_variation', 10, 2 ); +``` + +The `get_block_type_variations` filter is called when variations are requested for a block type. It receives two parameters: +- `$variations`: An array of currently registered variations for the block type +- `$block_type`: The full block type object + +Note that variations registered through PHP will be merged with any variations registered through JavaScript using `registerBlockVariation()`. + +
Check the How to register block variations with PHP blog post for more info about this
+ ## Removing a block variation Block variations can also be easily removed. To do so, use `wp.blocks.unregisterBlockVariation()`. This function accepts the name of the block and the `name` of the variation that should be unregistered. diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md index bbbb565684c57..bf2c1370ebcde 100644 --- a/docs/reference-guides/interactivity-api/api-reference.md +++ b/docs/reference-guides/interactivity-api/api-reference.md @@ -873,6 +873,8 @@ const { state } = store( 'myPlugin', { } ); ``` +You may want to add multiple such `yield` points in your action if it is doing a lot of work. + As mentioned above with [`wp-on`](#wp-on), [`wp-on-window`](#wp-on-window), and [`wp-on-document`](#wp-on-document), an async action should be used whenever the `async` versions of the aforementioned directives cannot be used due to the action requiring synchronous access to the `event` object. Synchronous access is required whenever the action needs to call `event.preventDefault()`, `event.stopPropagation()`, or `event.stopImmediatePropagation()`. To ensure that the action code does not contribute to a long task, you may manually yield to the main thread after calling the synchronous event API. For example: ```js @@ -885,16 +887,17 @@ function splitTask() { store( 'myPlugin', { actions: { - handleClick: function* ( event ) { + handleClick: withSyncEvent( function* ( event ) { event.preventDefault(); yield splitTask(); doTheWork(); - }, + } ), }, } ); ``` -You may want to add multiple such `yield` points in your action if it is doing a lot of work. +You may notice the use of the [`withSyncEvent()`](#withsyncevent) utility function in this example. This is necessary due to an ongoing effort to handle store actions asynchronously by default, unless they require synchronous event access (which this example does due to the call to `event.preventDefault()`). Otherwise a deprecation warning will be triggered, and in a future release the behavior will change accordingly. + #### Side Effects @@ -1253,6 +1256,43 @@ store( 'mySliderPlugin', { } ); ``` +### withSyncEvent() + +Actions that require synchronous access to the `event` object need to use the `withSyncEvent()` function to annotate their handler callback. This is necessary due to an ongoing effort to handle store actions asynchronously by default, unless they require synchronous event access. Therefore, as of Gutenberg 20.4 / WordPress 6.8 all actions that require synchronous event access need to use the `withSyncEvent()` function. Otherwise a deprecation warning will be triggered, and in a future release the behavior will change accordingly. + +Only very specific event methods and properties require synchronous access, so it is advised to only use `withSyncEvent()` when necessary. The following event methods and properties require synchronous access: + +* `event.currentTarget` +* `event.preventDefault()` +* `event.stopImmediatePropagation()` +* `event.stopPropagation()` + +Here is an example, where one action requires synchronous event access while the other actions do not: + +```js +// store +import { store, withSyncEvent } from '@wordpress/interactivity'; + +store( 'myPlugin', { + actions: { + // `event.preventDefault()` requires synchronous event access. + preventNavigation: withSyncEvent( ( event ) => { + event.preventDefault(); + } ), + + // `event.target` does not require synchronous event access. + logTarget: ( event ) => { + console.log( 'event target => ', event.target ); + }, + + // Not using `event` at all does not require synchronous event access. + logSomething: () => { + console.log( 'something' ); + }, + }, +} ); +``` + ## Server functions The Interactivity API comes with handy functions that allow you to initialize and reference configuration options on the server. This is necessary to feed the initial data that the Server Directive Processing will use to modify the HTML markup before it's send to the browser. It is also a great way to leverage many of WordPress's APIs, like nonces, AJAX, and translations. diff --git a/gutenberg.php b/gutenberg.php index 1ef51115ec932..5814650073768 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.6 * Requires PHP: 7.2 - * Version: 20.3.0-rc.2 + * Version: 20.3.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/aria-label.php b/lib/block-supports/aria-label.php new file mode 100644 index 0000000000000..062ff027bf594 --- /dev/null +++ b/lib/block-supports/aria-label.php @@ -0,0 +1,63 @@ +attributes ) { + $block_type->attributes = array(); + } + + if ( ! array_key_exists( 'ariaLabel', $block_type->attributes ) ) { + $block_type->attributes['ariaLabel'] = array( + 'type' => 'string', + ); + } +} + +/** + * Add the aria-label to the output. + * + * @param WP_Block_Type $block_type Block Type. + * @param array $block_attributes Block attributes. + * + * @return array Block aria-label. + */ +function gutenberg_apply_aria_label_support( $block_type, $block_attributes ) { + if ( ! $block_attributes ) { + return array(); + } + + $has_aria_label_support = block_has_support( $block_type, array( 'ariaLabel' ), false ); + if ( ! $has_aria_label_support ) { + return array(); + } + + $has_aria_label = array_key_exists( 'ariaLabel', $block_attributes ); + if ( ! $has_aria_label ) { + return array(); + } + return array( 'aria-label' => $block_attributes['ariaLabel'] ); +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'aria-label', + array( + 'register_attribute' => 'gutenberg_register_aria_label_support', + 'apply' => 'gutenberg_apply_aria_label_support', + ) +); diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index e3186d2d37032..0679dd4283acf 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -563,7 +563,7 @@ class WP_Theme_JSON_Gutenberg { /** * Defines which pseudo selectors are enabled for which elements. * - * The order of the selectors should be: link, any-link, visited, hover, focus, active. + * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active. * This is to ensure the user action (hover, focus and active) styles have a higher * specificity than the visited styles, which in turn have a higher specificity than * the unvisited styles. @@ -573,10 +573,11 @@ class WP_Theme_JSON_Gutenberg { * * @since 6.1.0 * @since 6.2.0 Added support for `:link` and `:any-link`. + * @since 6.8.0 Added support for `:focus-visible`. */ const VALID_ELEMENT_PSEUDO_SELECTORS = array( - 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ), - 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ), + 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), + 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), ); /** @@ -2931,7 +2932,11 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) { array_filter( $element_pseudo_allowed, static function ( $pseudo_selector ) use ( $selector ) { - return str_contains( $selector, $pseudo_selector ); + /* + * Check if the pseudo selector is in the current selector, + * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible). + */ + return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1; } ) ); diff --git a/lib/compat/wordpress-6.8/block-template-utils.php b/lib/compat/wordpress-6.8/block-template-utils.php new file mode 100644 index 0000000000000..6e792f5be886e --- /dev/null +++ b/lib/compat/wordpress-6.8/block-template-utils.php @@ -0,0 +1,29 @@ + $post_format_name ) { + $default_template_types[ 'taxonomy-post_format-post-format-' . $post_format_slug ] = array( + 'title' => sprintf( + /* translators: %s: Post format name. */ + _x( 'Post Format: %s', 'Template name' ), + $post_format_name + ), + 'description' => sprintf( + /* translators: %s: Post format name. */ + __( 'Displays the %s post format archive.' ), + $post_format_name + ), + ); + } + return $default_template_types; +} + +add_filter( 'default_template_types', 'gutenberg_post_format_template_title_description', 10, 1 ); diff --git a/lib/compat/wordpress-6.8/blocks.php b/lib/compat/wordpress-6.8/blocks.php index 7fd4237bc81d3..ba6d23b2486b8 100644 --- a/lib/compat/wordpress-6.8/blocks.php +++ b/lib/compat/wordpress-6.8/blocks.php @@ -36,6 +36,25 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post return apply_block_hooks_to_content( $content, $post, $callback ); } + /* + * If the content was created using the classic editor or using a single Classic block + * (`core/freeform`), it might not contain any block markup at all. + * However, we still might need to inject hooked blocks in the first child or last child + * positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap + * the content in a `core/freeform` wrapper block. + */ + if ( ! has_blocks( $content ) ) { + $original_content = $content; + + $content_wrapped_in_classic_block = get_comment_delimited_block_content( + 'core/freeform', + array(), + $content + ); + + $content = $content_wrapped_in_classic_block; + } + $attributes = array(); // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta. @@ -71,6 +90,17 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post // Finally, we need to remove the temporary wrapper block. $content = remove_serialized_parent_block( $content ); + // If we wrapped the content in a `core/freeform` block, we also need to remove that. + if ( ! empty( $content_wrapped_in_classic_block ) ) { + /* + * We cannot simply use remove_serialized_parent_block() here, + * as that function assumes that the block wrapper is at the top level. + * However, there might now be a hooked block inserted next to it + * (as first or last child of the parent). + */ + $content = str_replace( $content_wrapped_in_classic_block, $original_content, $content ); + } + return $content; } // We need to apply this filter before `do_blocks` (which is hooked to `the_content` at priority 9). diff --git a/lib/load.php b/lib/load.php index 789c9f9980e9a..25b5189010909 100644 --- a/lib/load.php +++ b/lib/load.php @@ -104,6 +104,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/post.php'; require __DIR__ . '/compat/wordpress-6.8/site-editor.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php'; +require __DIR__ . '/compat/wordpress-6.8/block-template-utils.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; @@ -177,6 +178,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; require __DIR__ . '/block-supports/block-style-variations.php'; +require __DIR__ . '/block-supports/aria-label.php'; // Data views. require_once __DIR__ . '/experimental/data-views.php'; diff --git a/package-lock.json b/package-lock.json index 1c0795c90bcc9..a8bbafbfc2eb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "20.3.0-rc.2", + "version": "20.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "20.3.0-rc.2", + "version": "20.3.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "workspaces": [ diff --git a/package.json b/package.json index 2984502a07a5d..0737c415f71c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "20.3.0-rc.2", + "version": "20.3.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap index 3d082a14a92bf..eb665ced62079 100644 --- a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap +++ b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap @@ -203,7 +203,7 @@ exports[`ColorPaletteControl matches the snapshot 1`] = ` class="components-circular-option-picker" >
diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 3ae01525a8010..8411430dfbdad 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -131,8 +131,23 @@ function Iframe( { function preventFileDropDefault( event ) { event.preventDefault(); } + + const { ownerDocument } = node; + + // Ideally ALL classes that are added through get_body_class should + // be added in the editor too, which we'll somehow have to get from + // the server in the future (which will run the PHP filters). + setBodyClasses( + Array.from( ownerDocument.body.classList ).filter( + ( name ) => + name.startsWith( 'admin-color-' ) || + name.startsWith( 'post-type-' ) || + name === 'wp-embed-responsive' + ) + ); + function onLoad() { - const { contentDocument, ownerDocument } = node; + const { contentDocument } = node; const { documentElement } = contentDocument; iFrameDocument = contentDocument; @@ -140,18 +155,6 @@ function Iframe( { clearerRef( documentElement ); - // Ideally ALL classes that are added through get_body_class should - // be added in the editor too, which we'll somehow have to get from - // the server in the future (which will run the PHP filters). - setBodyClasses( - Array.from( ownerDocument.body.classList ).filter( - ( name ) => - name.startsWith( 'admin-color-' ) || - name.startsWith( 'post-type-' ) || - name === 'wp-embed-responsive' - ) - ); - contentDocument.dir = ownerDocument.dir; for ( const compatStyle of getCompatibilityStyles() ) { diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 1eafe99e283eb..b09093e312211 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -514,6 +514,8 @@ function CoverEdit( { value={ overlayColor.color } onChange={ onSetOverlayColor } clearable={ false } + asButtons + aria-label={ __( 'Overlay color' ) } />
diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index 0a18d2cf3f9f8..16695f53f6746 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -47,7 +47,7 @@ async function setup( attributes, useCoreBlocks, customSettings ) { async function createAndSelectBlock() { await userEvent.click( - screen.getByRole( 'option', { + screen.getByRole( 'button', { name: 'Black', } ) ); @@ -72,7 +72,7 @@ describe( 'Cover block', () => { test( 'can set overlay color using color picker on block placeholder', async () => { const { container } = await setup(); - const colorPicker = screen.getByRole( 'option', { + const colorPicker = screen.getByRole( 'button', { name: 'Black', } ); await userEvent.click( colorPicker ); @@ -96,7 +96,7 @@ describe( 'Cover block', () => { await setup(); await userEvent.click( - screen.getByRole( 'option', { + screen.getByRole( 'button', { name: 'Black', } ) ); @@ -389,7 +389,7 @@ describe( 'Cover block', () => { describe( 'isDark settings', () => { test( 'should toggle is-light class if background changed from light to dark', async () => { await setup(); - const colorPicker = screen.getByRole( 'option', { + const colorPicker = screen.getByRole( 'button', { name: 'White', } ); await userEvent.click( colorPicker ); @@ -413,7 +413,7 @@ describe( 'Cover block', () => { } ); test( 'should remove is-light class if overlay color is removed', async () => { await setup(); - const colorPicker = screen.getByRole( 'option', { + const colorPicker = screen.getByRole( 'button', { name: 'White', } ); await userEvent.click( colorPicker ); @@ -426,7 +426,7 @@ describe( 'Cover block', () => { } ) ); await userEvent.click( screen.getByText( 'Overlay' ) ); - // The default color is black, so clicking the black color option will remove the background color, + // The default color is black, so clicking the black color button will remove the background color, // which should remove the isDark setting and assign the is-light class. const popupColorPicker = screen.getByRole( 'option', { name: 'White', diff --git a/packages/block-library/src/form/edit.js b/packages/block-library/src/form/edit.js index a7260afabedf3..227f5ea05014f 100644 --- a/packages/block-library/src/form/edit.js +++ b/packages/block-library/src/form/edit.js @@ -123,6 +123,7 @@ const Edit = ( { attributes, setAttributes, clientId } ) => { help={ __( 'The email address where form submissions will be sent. Separate multiple email addresses with a comma.' ) } + type="email" /> ) } @@ -159,6 +160,7 @@ const Edit = ( { attributes, setAttributes, clientId } ) => { help={ __( 'The URL where the form should be submitted.' ) } + type="url" /> ) } diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 3c9a729538813..71a492a570b2a 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; /** * Tracks whether user is touching screen; used to differentiate behavior for @@ -128,7 +133,7 @@ const { state, actions, callbacks } = store( }, 450 ); } }, - handleKeydown( event ) { + handleKeydown: withSyncEvent( ( event ) => { if ( state.overlayEnabled ) { // Focuses the close button when the user presses the tab key. if ( event.key === 'Tab' ) { @@ -141,8 +146,8 @@ const { state, actions, callbacks } = store( actions.hideLightbox(); } } - }, - handleTouchMove( event ) { + } ), + handleTouchMove: withSyncEvent( ( event ) => { // On mobile devices, prevents triggering the scroll event because // otherwise the page jumps around when it resets the scroll position. // This also means that closing the lightbox requires that a user @@ -152,7 +157,7 @@ const { state, actions, callbacks } = store( if ( state.overlayEnabled ) { event.preventDefault(); } - }, + } ), handleTouchStart() { isTouching = true; }, diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 11a281a44a76d..e3018c274ef78 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -201,6 +201,7 @@ function Controls( { attributes, setAttributes, setIsLabelFieldFocused } ) { ); } } autoComplete="off" + type="url" /> diff --git a/packages/block-library/src/navigation-link/editor.scss b/packages/block-library/src/navigation-link/editor.scss index b27c4520921fd..5034c6f909ecd 100644 --- a/packages/block-library/src/navigation-link/editor.scss +++ b/packages/block-library/src/navigation-link/editor.scss @@ -82,30 +82,9 @@ // Draw a wavy underline. .wp-block-navigation-link__placeholder-text { span { - $blur: 10%; - $width: 6%; - $stop1: 30%; - $stop2: 64%; - - --wp-underline-color: var(--wp-admin-theme-color); - - background-image: - linear-gradient(45deg, transparent ($stop1 - $blur), var(--wp-underline-color) $stop1, var(--wp-underline-color) ($stop1 + $width), transparent ($stop1 + $width + $blur)), - linear-gradient(135deg, transparent ($stop2 - $blur), var(--wp-underline-color) $stop2, var(--wp-underline-color) ($stop2 + $width), transparent ($stop2 + $width + $blur)); - background-position: 0 100%; - background-size: 6px 3px; - background-repeat: repeat-x; - - // Since applied to a span, it doesn't change the footprint of the item, - // but it does vertically shift the underline to better align. - padding-bottom: 0.1em; - } - - &.is-invalid, - &.is-draft { - span { - --wp-underline-color: #{$alert-red}; - } + text-decoration: wavy underline; + text-decoration-skip-ink: none; + text-underline-offset: 0.25rem; } } diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index b5f40ffb67677..00ee02c50920f 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -431,6 +431,7 @@ export default function NavigationSubmenuEdit( { } } label={ __( 'Link' ) } autoComplete="off" + type="url" /> diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 9da7ab70d84f3..fd1fe33537b2f 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; const focusableSelectors = [ 'a[href]', @@ -106,7 +111,7 @@ const { state, actions } = store( actions.openMenu( 'click' ); } }, - handleMenuKeydown( event ) { + handleMenuKeydown: withSyncEvent( ( event ) => { const { type, firstFocusableElement, lastFocusableElement } = getContext(); if ( state.menuOpenedBy.click ) { @@ -137,7 +142,7 @@ const { state, actions } = store( } } } - }, + } ), handleMenuFocusout( event ) { const { modal, type } = getContext(); // If focus is outside modal, and in the document, close menu diff --git a/packages/block-library/src/post-comments-count/block.json b/packages/block-library/src/post-comments-count/block.json index 796e6e3830236..4b195a8c928e6 100644 --- a/packages/block-library/src/post-comments-count/block.json +++ b/packages/block-library/src/post-comments-count/block.json @@ -39,8 +39,15 @@ "fontSize": true } }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true + }, "interactivity": { "clientNavigation": true } - } + }, + "style": "wp-block-post-comments-count" } diff --git a/packages/block-library/src/post-comments-count/style.scss b/packages/block-library/src/post-comments-count/style.scss new file mode 100644 index 0000000000000..c596ec200ab1f --- /dev/null +++ b/packages/block-library/src/post-comments-count/style.scss @@ -0,0 +1,4 @@ +.wp-block-post-comments-count { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/query-total/index.php b/packages/block-library/src/query-total/index.php index ff2ac486727b9..ba99b32460133 100644 --- a/packages/block-library/src/query-total/index.php +++ b/packages/block-library/src/query-total/index.php @@ -10,6 +10,8 @@ * * @since 6.8.0 * + * @global WP_Query $wp_query WordPress Query object. + * * @param array $attributes Block attributes. * @param string $content Block default content. * @param WP_Block $block Block instance. diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index e23294a24e02e..fff12b16eac65 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; const isValidLink = ( ref ) => ref && @@ -22,7 +27,7 @@ store( 'core/query', { actions: { - *navigate( event ) { + navigate: withSyncEvent( function* ( event ) { const ctx = getContext(); const { ref } = getElement(); const queryRef = ref.closest( @@ -42,7 +47,7 @@ store( const firstAnchor = `.wp-block-post-template a[href]`; queryRef.querySelector( firstAnchor )?.focus(); } - }, + } ), *prefetch() { const { ref } = getElement(); if ( isValidLink( ref ) ) { diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 0e4c462a2e321..617e179b1dc88 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; const { actions } = store( 'core/search', @@ -31,7 +36,7 @@ const { actions } = store( }, }, actions: { - openSearchInput( event ) { + openSearchInput: withSyncEvent( ( event ) => { const ctx = getContext(); const { ref } = getElement(); if ( ! ctx.isSearchInputVisible ) { @@ -39,7 +44,7 @@ const { actions } = store( ctx.isSearchInputVisible = true; ref.parentElement.querySelector( 'input' ).focus(); } - }, + } ), closeSearchInput() { const ctx = getContext(); ctx.isSearchInputVisible = false; diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index c61049c23151b..3c8b8e623cc4d 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -36,6 +36,7 @@ @import "./post-author/style.scss"; @import "./post-author-biography/style.scss"; @import "./post-comments-form/style.scss"; +@import "./post-comments-count/style.scss"; @import "./post-content/style.scss"; @import "./post-comments-link/style.scss"; @import "./post-date/style.scss"; diff --git a/packages/block-library/src/video/edit-common-settings.js b/packages/block-library/src/video/edit-common-settings.js index 96d59d7e2f726..312586259efe1 100644 --- a/packages/block-library/src/video/edit-common-settings.js +++ b/packages/block-library/src/video/edit-common-settings.js @@ -22,6 +22,7 @@ const VideoSettings = ( { setAttributes, attributes } ) => { const autoPlayHelpText = __( 'Autoplay may cause usability issues for some users.' ); + const getAutoplayHelp = Platform.select( { web: useCallback( ( checked ) => { return checked ? autoPlayHelpText : null; @@ -32,7 +33,11 @@ const VideoSettings = ( { setAttributes, attributes } ) => { const toggleFactory = useMemo( () => { const toggleAttribute = ( attribute ) => { return ( newValue ) => { - setAttributes( { [ attribute ]: newValue } ); + setAttributes( { + [ attribute ]: newValue, + // Set muted when autoplay changes + ...( attribute === 'autoplay' && { muted: newValue } ), + } ); }; }; @@ -56,7 +61,7 @@ const VideoSettings = ( { setAttributes, attributes } ) => { isShownByDefault hasValue={ () => !! autoplay } onDeselect={ () => { - setAttributes( { autoplay: false } ); + setAttributes( { autoplay: false, muted: false } ); } } > { label={ __( 'Muted' ) } onChange={ toggleFactory.muted } checked={ !! muted } + disabled={ autoplay } + help={ + autoplay ? __( 'Muted because of Autoplay.' ) : null + } /> { await waitFor( () => expect( screen.getByRole( 'button', { - name: 'Custom color picker.', + name: 'Custom color picker', } ) ).toBeVisible() ); diff --git a/packages/components/src/border-control/test/index.js b/packages/components/src/border-control/test/index.js index ff9007be28f1a..c3e3987ed1351 100644 --- a/packages/components/src/border-control/test/index.js +++ b/packages/components/src/border-control/test/index.js @@ -138,7 +138,7 @@ describe( 'BorderControl', () => { const customColorPicker = getButton( /Custom color picker/ ); const circularOptionPicker = screen.getByRole( 'listbox', { - name: 'Custom color picker.', + name: 'Custom color picker', } ); const colorSwatchButtons = within( circularOptionPicker ).getAllByRole( 'option' ); diff --git a/packages/components/src/circular-option-picker/README.md b/packages/components/src/circular-option-picker/README.md index b6db6f06daf45..8a4d5ac3cf5ca 100644 --- a/packages/components/src/circular-option-picker/README.md +++ b/packages/components/src/circular-option-picker/README.md @@ -93,6 +93,19 @@ Prevents keyboard interaction from wrapping around. Only used when `asButtons` i - Required: No - Default: `true` +### `aria-labelledby`: `string` + +The ID reference list of one or more elements that label the wrapper element. + +- Required: No + +### `aria-label`: `string` + +The label for the wrapper element. Not used if an 'aria-labelledby' is provided. + +- Required: No +- Default: `Custom color picker` + ## Subcomponents ### `CircularOptionPicker.ButtonAction` diff --git a/packages/components/src/circular-option-picker/circular-option-picker.tsx b/packages/components/src/circular-option-picker/circular-option-picker.tsx index 8b6be8cd2215f..c4309ecf4dda3 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker.tsx @@ -132,7 +132,7 @@ function ButtonsCircularOptionPicker( ); return ( -
+
{ options } { children } diff --git a/packages/components/src/circular-option-picker/index.tsx b/packages/components/src/circular-option-picker/index.tsx index ef975c21ee654..ef379994b476f 100644 --- a/packages/components/src/circular-option-picker/index.tsx +++ b/packages/components/src/circular-option-picker/index.tsx @@ -9,5 +9,6 @@ export { ButtonAction, DropdownLinkAction, } from './circular-option-picker-actions'; +export { getComputeCircularOptionPickerCommonProps } from './utils'; export default CircularOptionPicker; diff --git a/packages/components/src/circular-option-picker/stories/index.story.tsx b/packages/components/src/circular-option-picker/stories/index.story.tsx index 9d45c9bb92f7d..6b564929fd8eb 100644 --- a/packages/components/src/circular-option-picker/stories/index.story.tsx +++ b/packages/components/src/circular-option-picker/stories/index.story.tsx @@ -131,7 +131,7 @@ WithLoopingDisabled.parameters = { docs: { source: { code: `} />`, diff --git a/packages/components/src/circular-option-picker/test/index.tsx b/packages/components/src/circular-option-picker/test/index.tsx index a6e9f2c45a05c..7d58ed3920f9b 100644 --- a/packages/components/src/circular-option-picker/test/index.tsx +++ b/packages/components/src/circular-option-picker/test/index.tsx @@ -57,6 +57,7 @@ describe( 'CircularOptionPicker', () => { expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument(); expect( screen.queryByRole( 'option' ) ).not.toBeInTheDocument(); + expect( screen.getByRole( 'group' ) ).toBeInTheDocument(); expect( screen.getByRole( 'button' ) ).toBeInTheDocument(); } ); } ); diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index 411782aed575b..54fae3ab2e798 100644 --- a/packages/components/src/circular-option-picker/types.ts +++ b/packages/components/src/circular-option-picker/types.ts @@ -40,6 +40,16 @@ type CommonCircularOptionPickerProps = { * The child elements. */ children?: ReactNode; + /** + * The ID reference list of one or more elements that label the wrapper + * element. + */ + 'aria-labelledby'?: string; + /** + * The label for the wrapper element. Defaults to 'Custom color picker'. Not + * used if an 'aria-labelledby' is provided. + */ + 'aria-label'?: string; }; type WithBaseId = { @@ -59,16 +69,7 @@ type FullListboxCircularOptionPickerProps = CommonCircularOptionPickerProps & { * @default true */ loop?: boolean; -} & ( - | { - 'aria-label': string; - 'aria-labelledby'?: never; - } - | { - 'aria-label'?: never; - 'aria-labelledby': string; - } - ); +}; export type ListboxCircularOptionPickerProps = WithBaseId & Omit< FullListboxCircularOptionPickerProps, 'asButtons' >; diff --git a/packages/components/src/circular-option-picker/utils.tsx b/packages/components/src/circular-option-picker/utils.tsx new file mode 100644 index 0000000000000..fcb3b2bcac369 --- /dev/null +++ b/packages/components/src/circular-option-picker/utils.tsx @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Computes the common props for the CircularOptionPicker. + */ +export function getComputeCircularOptionPickerCommonProps( + asButtons?: boolean, + loop?: boolean, + ariaLabel?: string, + ariaLabelledby?: string +) { + const metaProps = asButtons + ? { asButtons: true } + : { asButtons: false, loop }; + + const labelProps = { + 'aria-labelledby': ariaLabelledby, + 'aria-label': ariaLabelledby + ? undefined + : ariaLabel || __( 'Custom color picker' ), + }; + + return { metaProps, labelProps }; +} diff --git a/packages/components/src/color-palette/index.tsx b/packages/components/src/color-palette/index.tsx index de4e4f4206fe3..eb981e8b9acc7 100644 --- a/packages/components/src/color-palette/index.tsx +++ b/packages/components/src/color-palette/index.tsx @@ -19,7 +19,9 @@ import { useCallback, useMemo, useState, forwardRef } from '@wordpress/element'; */ import Dropdown from '../dropdown'; import { ColorPicker } from '../color-picker'; -import CircularOptionPicker from '../circular-option-picker'; +import CircularOptionPicker, { + getComputeCircularOptionPickerCommonProps, +} from '../circular-option-picker'; import { VStack } from '../v-stack'; import { Truncate } from '../truncate'; import { ColorHeading } from './styles'; @@ -233,7 +235,7 @@ function UnforwardedColorPalette( buttonLabelName, displayValue ) - : __( 'Custom color picker.' ); + : __( 'Custom color picker' ); const paletteCommonProps = { clearColor, @@ -251,33 +253,12 @@ function UnforwardedColorPalette( ); - let metaProps: - | { asButtons: false; loop?: boolean; 'aria-label': string } - | { asButtons: false; loop?: boolean; 'aria-labelledby': string } - | { asButtons: true }; - - if ( asButtons ) { - metaProps = { asButtons: true }; - } else { - const _metaProps: { asButtons: false; loop?: boolean } = { - asButtons: false, - loop, - }; - - if ( ariaLabel ) { - metaProps = { ..._metaProps, 'aria-label': ariaLabel }; - } else if ( ariaLabelledby ) { - metaProps = { - ..._metaProps, - 'aria-labelledby': ariaLabelledby, - }; - } else { - metaProps = { - ..._metaProps, - 'aria-label': __( 'Custom color picker.' ), - }; - } - } + const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps( + asButtons, + loop, + ariaLabel, + ariaLabelledby + ); return ( @@ -335,6 +316,7 @@ function UnforwardedColorPalette( { ( colors.length > 0 || actions ) && ( { expect( screen.queryByText( colorCode ) ).not.toBeInTheDocument(); expect( screen.getByRole( 'button', { - name: /^Custom color picker.$/, + name: /^Custom color picker$/, } ) ).toBeInTheDocument(); } ); diff --git a/packages/components/src/duotone-picker/duotone-picker.tsx b/packages/components/src/duotone-picker/duotone-picker.tsx index 8764b401c3829..a21d12b73a65c 100644 --- a/packages/components/src/duotone-picker/duotone-picker.tsx +++ b/packages/components/src/duotone-picker/duotone-picker.tsx @@ -13,7 +13,9 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import ColorListPicker from './color-list-picker'; -import CircularOptionPicker from '../circular-option-picker'; +import CircularOptionPicker, { + getComputeCircularOptionPickerCommonProps, +} from '../circular-option-picker'; import { VStack } from '../v-stack'; import CustomDuotoneBar from './custom-duotone-bar'; @@ -127,33 +129,12 @@ function DuotonePicker( { ); } ); - let metaProps: - | { asButtons: false; loop?: boolean; 'aria-label': string } - | { asButtons: false; loop?: boolean; 'aria-labelledby': string } - | { asButtons: true }; - - if ( asButtons ) { - metaProps = { asButtons: true }; - } else { - const _metaProps: { asButtons: false; loop?: boolean } = { - asButtons: false, - loop, - }; - - if ( ariaLabel ) { - metaProps = { ..._metaProps, 'aria-label': ariaLabel }; - } else if ( ariaLabelledby ) { - metaProps = { - ..._metaProps, - 'aria-labelledby': ariaLabelledby, - }; - } else { - metaProps = { - ..._metaProps, - 'aria-label': __( 'Custom color picker.' ), - }; - } - } + const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps( + asButtons, + loop, + ariaLabel, + ariaLabelledby + ); const options = unsetable ? [ unsetOption, ...duotoneOptions ] @@ -163,6 +144,7 @@ function DuotonePicker( { ) { ); - let metaProps: - | { asButtons: false; loop?: boolean; 'aria-label': string } - | { asButtons: false; loop?: boolean; 'aria-labelledby': string } - | { asButtons: true }; - - if ( asButtons ) { - metaProps = { asButtons: true }; - } else { - const _metaProps: { asButtons: false; loop?: boolean } = { - asButtons: false, - loop, - }; - - if ( ariaLabel ) { - metaProps = { ..._metaProps, 'aria-label': ariaLabel }; - } else if ( ariaLabelledby ) { - metaProps = { - ..._metaProps, - 'aria-labelledby': ariaLabelledby, - }; - } else { - metaProps = { - ..._metaProps, - 'aria-label': __( 'Custom color picker.' ), - }; - } - } + const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps( + asButtons, + loop, + ariaLabel, + ariaLabelledby + ); return ( diff --git a/packages/components/src/notice/style.scss b/packages/components/src/notice/style.scss index a2d6aca530a93..9cfc2b983a491 100644 --- a/packages/components/src/notice/style.scss +++ b/packages/components/src/notice/style.scss @@ -6,6 +6,7 @@ border-left: 4px solid $components-color-accent; padding: 8px 12px; align-items: center; + color: $gray-900; &.is-dismissible { position: relative; diff --git a/packages/components/src/text-control/style.scss b/packages/components/src/text-control/style.scss index 5ae2a90b82773..3e59c47532a61 100644 --- a/packages/components/src/text-control/style.scss +++ b/packages/components/src/text-control/style.scss @@ -28,3 +28,9 @@ padding-right: $grid-unit-15; } } + +.components-text-control__input[type="email"], +.components-text-control__input[type="url"] { + /* rtl:ignore */ + direction: ltr; +} diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index c31ebc0425464..64ff340afd0c2 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -465,7 +465,10 @@ export const getRawEntityRecord = createSelector( // Because edits are the "raw" attribute values, // we return those from record selectors to make rendering, // comparisons, and joins with edits easier. - accumulator[ _key ] = record[ _key ]?.raw ?? record[ _key ]; + accumulator[ _key ] = + record[ _key ]?.raw !== undefined + ? record[ _key ]?.raw + : record[ _key ]; } else { accumulator[ _key ] = record[ _key ]; } diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index 4b5e8417ad202..0330865e95957 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -332,6 +332,48 @@ describe( 'getRawEntityRecord', () => { }, } ); } ); + it( 'should allow `null` as raw value', () => { + const state = deepFreeze( { + entities: { + config: [ + { + kind: 'someKind', + name: 'someName', + rawAttributes: [ 'title' ], + }, + ], + records: { + someKind: { + someName: { + queriedData: { + items: { + default: { + post: { + title: { + raw: null, + rendered: 'Placeholder', + }, + }, + }, + }, + itemIsComplete: { + default: { + post: true, + }, + }, + queries: {}, + }, + }, + }, + }, + }, + } ); + expect( + getRawEntityRecord( state, 'someKind', 'someName', 'post' ) + ).toEqual( { + title: null, + } ); + } ); } ); describe( 'getEntityRecords', () => { diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx index 8e69e7353ce92..b3789df8fbb68 100644 --- a/packages/dataviews/src/dataviews-layouts/table/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx @@ -140,12 +140,7 @@ function TableRow< Item >( { } } > { hasBulkActions && ( - +
( { { hasBulkActions && ( ( { ) } { hasPrimaryColumn && ( - - { titleField && ( - - ) } - + { titleField && ( + + ) } ) } { columns.map( ( column, index ) => { diff --git a/packages/dataviews/src/dataviews-layouts/table/style.scss b/packages/dataviews/src/dataviews-layouts/table/style.scss index 5a4ac01b566f7..02ccbe4fed7bd 100644 --- a/packages/dataviews/src/dataviews-layouts/table/style.scss +++ b/packages/dataviews/src/dataviews-layouts/table/style.scss @@ -24,6 +24,7 @@ &.dataviews-view-table__checkbox-column { padding-right: 0; + width: 1%; } } tr { @@ -37,8 +38,7 @@ th:first-child { padding-left: $grid-unit-60; - .dataviews-view-table-header-button, - .dataviews-view-table-header { + .dataviews-view-table-header-button { margin-left: - #{$grid-unit-10}; } } diff --git a/packages/date/README.md b/packages/date/README.md index 4f0a64c24aa71..a7543df01cd63 100644 --- a/packages/date/README.md +++ b/packages/date/README.md @@ -27,7 +27,7 @@ _Related_ _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. - _timezone_ `string | number | undefined`: Timezone to output result in or a UTC offset. Defaults to timezone from site. @@ -48,7 +48,7 @@ _Related_ _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. - _timezone_ `string | number | boolean | undefined=`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. @@ -62,7 +62,7 @@ Formats a date. Does not alter the date's timezone. _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -95,7 +95,7 @@ Formats a date (like `date()` in PHP), in the UTC timezone. _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -108,7 +108,7 @@ Formats a date (like `wp_date()` in PHP), translating it into site's locale and _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 8d8f53fd8bc10..5af8b5f4780e8 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -445,7 +445,7 @@ const formatMap = { * Formats a date. Does not alter the date's timezone. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * @@ -487,7 +487,7 @@ export function format( dateFormat, dateValue = new Date() ) { * Formats a date (like `date()` in PHP). * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable * by moment.js. * @param {string | number | undefined} timezone Timezone to output result in or a @@ -508,7 +508,7 @@ export function date( dateFormat, dateValue = new Date(), timezone ) { * Formats a date (like `date()` in PHP), in the UTC timezone. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * @@ -526,7 +526,7 @@ export function gmdate( dateFormat, dateValue = new Date() ) { * behaves like `gmdateI18n`. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable by * moment.js. * @param {string | number | boolean | undefined=} timezone Timezone to output result in or a @@ -559,7 +559,7 @@ export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { * and using the UTC timezone. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js index 7577810b6bb87..98fd1fdb5593d 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js @@ -240,7 +240,12 @@ directive( 'priority-2-init', ( { directives: { 'priority-2-init': init }, evaluate } ) => { init.forEach( ( entry ) => { - useInit( () => evaluate( entry ) ); + useInit( () => { + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + result(); + } + } ); } ); }, { priority: 2 } diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js index a8c70a4a90720..c27fe8d534d86 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js index b9689ac978f85..f7918f3c6bf53 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js index ef72e266e1075..0c29b09e5a70c 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js index 77f2f25c5f9a4..dd4cad1c32ed6 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js @@ -58,10 +58,13 @@ directive( */ directive( 'test-attribute', ( { evaluate, element } ) => { executionProof( 'attribute' ); - const attributeValue = evaluate( { + let attributeValue = evaluate( { namespace, value: 'context.attribute', } ); + if ( typeof attributeValue === 'function' ) { + attributeValue = attributeValue(); + } useEffect( () => { element.ref.current.setAttribute( 'data-attribute', attributeValue ); }, [] ); @@ -76,7 +79,10 @@ directive( 'test-text', ( { evaluate, element } ) => { executionProof( 'text' ); - const textValue = evaluate( { namespace, value: 'context.text' } ); + let textValue = evaluate( { namespace, value: 'context.text' } ); + if ( typeof textValue === 'function' ) { + textValue = textValue(); + } element.props.children = h( 'p', { 'data-testid': 'text' }, textValue ); }, { priority: 12 } @@ -92,10 +98,22 @@ directive( ( { evaluate, element } ) => { executionProof( 'children' ); const updateAttribute = () => { - evaluate( { namespace, value: 'actions.updateAttribute' } ); + const result = evaluate( { + namespace, + value: 'actions.updateAttribute', + } ); + if ( typeof result === 'function' ) { + result(); + } }; const updateText = () => { - evaluate( { namespace, value: 'actions.updateText' } ); + const result = evaluate( { + namespace, + value: 'actions.updateText', + } ); + if ( typeof result === 'function' ) { + result(); + } }; element.props.children = h( 'div', diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js index 125ac39204230..3b623baa43a09 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js @@ -22,9 +22,11 @@ directive( evaluate, } ) => { const entry = showChildren.find( ( { suffix } ) => suffix === null ); - return evaluate( entry ) - ? element - : cloneElement( element, { children: null } ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } + return result ? element : cloneElement( element, { children: null } ); }, { priority: 9 } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js index ad035811a0bcd..bb533ef9a208a 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js index 83f016e2eac16..d9eb2005cef88 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js @@ -1,17 +1,22 @@ /** * WordPress dependencies */ -import { store, getContext, getServerContext } from '@wordpress/interactivity'; +import { + store, + getContext, + getServerContext, + withSyncEvent, +} from '@wordpress/interactivity'; store( 'test/get-server-context', { actions: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), attemptModification() { try { getServerContext().prop = 'updated from client'; diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js index db2992ec4a586..23cd0c328aee6 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js @@ -1,17 +1,22 @@ /** * WordPress dependencies */ -import { store, getServerState, getContext } from '@wordpress/interactivity'; +import { + store, + getServerState, + getContext, + withSyncEvent, +} from '@wordpress/interactivity'; const { state } = store( 'test/get-server-state', { actions: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), attemptModification() { try { getServerState().prop = 'updated from client'; diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js index bd1d6e1164779..266a989ada739 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store } from '@wordpress/interactivity'; +import { store, withSyncEvent } from '@wordpress/interactivity'; const { state } = store( 'router', { state: { @@ -18,7 +18,7 @@ const { state } = store( 'router', { }, }, actions: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); state.navigations.count += 1; @@ -38,7 +38,7 @@ const { state } = store( 'router', { if ( state.navigations.pending === 0 ) { state.status = 'idle'; } - }, + } ), toggleTimeout() { state.timeout = state.timeout === 10000 ? 0 : 10000; }, diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js index f3468eb88aff0..a3a35d792755c 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store, getContext } from '@wordpress/interactivity'; +import { store, getContext, withSyncEvent } from '@wordpress/interactivity'; const { state } = store( 'router-regions', { state: { @@ -17,13 +17,13 @@ const { state } = store( 'router-regions', { }, actions: { router: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), back() { history.back(); }, diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js index 8016e931624a1..b4fc12a91a4f2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js @@ -14,7 +14,11 @@ directive( ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } + if ( ! result ) { element.props.children = h( 'template', null, diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 85702ae162285..959c63083a9d3 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -530,8 +530,8 @@ export const toggleFullscreenMode = .dispatch( noticesStore ) .createInfoNotice( isFullscreen - ? __( 'Fullscreen mode activated.' ) - : __( 'Fullscreen mode deactivated.' ), + ? __( 'Fullscreen mode deactivated.' ) + : __( 'Fullscreen mode activated.' ), { id: 'core/edit-post/toggle-fullscreen-mode/notice', type: 'snackbar', diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 16813e8ac0494..caf7dd78da4b3 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -262,10 +262,6 @@ html.canvas-mode-edit-transition::view-transition-group(toggle) { } } -.edit-site-layout__area__404 { - margin: $canvas-padding; -} - .edit-site .components-editor-notices__snackbar { position: fixed; right: 0; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js index 473b28245eaee..f5f7e380467c2 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js @@ -122,9 +122,9 @@ export default function SidebarNavigationScreen( {
{ description && ( -

+

{ description } -

+
) } { content }
diff --git a/packages/edit-site/src/components/site-editor-routes/notfound.js b/packages/edit-site/src/components/site-editor-routes/notfound.js index ee15e28e06f25..2a42eb48f10eb 100644 --- a/packages/edit-site/src/components/site-editor-routes/notfound.js +++ b/packages/edit-site/src/components/site-editor-routes/notfound.js @@ -6,8 +6,19 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import { Notice, __experimentalSpacer as Spacer } from '@wordpress/components'; import SidebarNavigationScreenMain from '../sidebar-navigation-screen-main'; +function NotFoundError() { + return ( + + { __( + 'The requested page could not be found. Please check the URL.' + ) } + + ); +} + export const notFoundRoute = { name: 'notfound', path: '*', @@ -15,13 +26,13 @@ export const notFoundRoute = { sidebar: , mobile: ( } /> ), content: ( -

- { __( '404 (Not Found)' ) } -

+ + + ), }, }; diff --git a/packages/edit-site/src/components/site-editor-routes/patterns.js b/packages/edit-site/src/components/site-editor-routes/patterns.js index db97c4b5c080f..785528f09afb0 100644 --- a/packages/edit-site/src/components/site-editor-routes/patterns.js +++ b/packages/edit-site/src/components/site-editor-routes/patterns.js @@ -1,8 +1,27 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + /** * Internal dependencies */ import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; import PagePatterns from '../page-patterns'; +import { unlock } from '../../lock-unlock'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobilePatternsView() { + const { query = {} } = useLocation(); + const { categoryId } = query; + + return !! categoryId ? ( + + ) : ( + + ); +} export const patternsRoute = { name: 'patterns', @@ -10,6 +29,6 @@ export const patternsRoute = { areas: { sidebar: , content: , - mobile: , + mobile: , }, }; diff --git a/packages/editor/src/components/preferences-modal/block-visibility.js b/packages/editor/src/components/preferences-modal/block-visibility.js index 8726b114d9748..fc6deab6b5fb5 100644 --- a/packages/editor/src/components/preferences-modal/block-visibility.js +++ b/packages/editor/src/components/preferences-modal/block-visibility.js @@ -14,6 +14,7 @@ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; const { BlockManager } = unlock( blockEditorPrivateApis ); +const EMPTY_ARRAY = []; export default function BlockVisibility() { const { showBlockTypes, hideBlockTypes } = unlock( @@ -31,7 +32,7 @@ export default function BlockVisibility() { select( editorStore ).getEditorSettings().allowedBlockTypes, hiddenBlockTypes: select( preferencesStore ).get( 'core', 'hiddenBlockTypes' ) ?? - [], + EMPTY_ARRAY, }; }, [] ); diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js index bc1e0eef07b0c..a14240ed9f489 100644 --- a/packages/format-library/src/text-color/inline.js +++ b/packages/format-library/src/text-color/inline.js @@ -142,6 +142,8 @@ function ColorPicker( { name, property, value, onChange } ) { setColors( value, name, colors, { [ property ]: color } ) ); } } + // Prevent the text and color picker from overlapping. + __experimentalIsRenderedInSidebar /> ); } diff --git a/packages/interactivity-router/README.md b/packages/interactivity-router/README.md index b79e6b310e239..3491ad3b459a4 100644 --- a/packages/interactivity-router/README.md +++ b/packages/interactivity-router/README.md @@ -17,12 +17,13 @@ The package is intended to be imported dynamically in the `view.js` files of int ```js /* view.js */ -import { store } from '@wordpress/interactivity'; +import { store, withSyncEvent } from '@wordpress/interactivity'; // This is how you would typically use the navigate() action in your block. store( 'my-namespace/myblock', { actions: { - *goToPage( e ) { + // The withSyncEvent() utility needs to be used because preventDefault() requires synchronous event access. + goToPage: withSyncEvent( function* ( e ) { e.preventDefault(); // We import the package dynamically to reduce the initial JS bundle size. @@ -31,7 +32,7 @@ store( 'my-namespace/myblock', { '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), }, } ); ``` diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index bddd017b1c99d..4568eba013c3b 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -50,6 +50,54 @@ function deepClone< T >( source: T ): T { return source; } +/** + * Wraps event object to warn about access of synchronous properties and methods. + * + * For all store actions attached to an event listener the event object is proxied via this function, unless the action + * uses the `withSyncEvent()` utility to indicate that it requires synchronous access to the event object. + * + * At the moment, the proxied event only emits warnings when synchronous properties or methods are being accessed. In + * the future this will be changed and result in an error. The current temporary behavior allows implementers to update + * their relevant actions to use `withSyncEvent()`. + * + * For additional context, see https://github.com/WordPress/gutenberg/issues/64944. + * + * @param event Event object. + * @return Proxied event object. + */ +function wrapEventAsync( event: Event ) { + const handler = { + get( target: Event, prop: string | symbol, receiver: any ) { + const value = target[ prop ]; + switch ( prop ) { + case 'currentTarget': + warn( + `Accessing the synchronous event.${ prop } property in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 6.9. Please wrap the store action in withSyncEvent().` + ); + break; + case 'preventDefault': + case 'stopImmediatePropagation': + case 'stopPropagation': + warn( + `Using the synchronous event.${ prop }() function in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 6.9. Please wrap the store action in withSyncEvent().` + ); + break; + } + if ( value instanceof Function ) { + return function ( this: any, ...args: any[] ) { + return value.apply( + this === receiver ? target : this, + args + ); + }; + } + return value; + }, + }; + + return new Proxy( event, handler ); +} + const newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g; const ruleClean = /\/\*[^]*?\*\/| +/g; @@ -102,7 +150,15 @@ const getGlobalEventDirective = ( .forEach( ( entry ) => { const eventName = entry.suffix.split( '--', 1 )[ 0 ]; useInit( () => { - const cb = ( event: Event ) => evaluate( entry, event ); + const cb = ( event: Event ) => { + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + if ( ! result?.sync ) { + event = wrapEventAsync( event ); + } + result( event ); + } + }; const globalVar = type === 'window' ? window : document; globalVar.addEventListener( eventName, cb ); return () => globalVar.removeEventListener( eventName, cb ); @@ -128,7 +184,10 @@ const getGlobalAsyncEventDirective = ( useInit( () => { const cb = async ( event: Event ) => { await splitTask(); - evaluate( entry, event ); + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + result( event ); + } }; const globalVar = type === 'window' ? window : document; globalVar.addEventListener( eventName, cb, { @@ -206,7 +265,10 @@ export default () => { start = performance.now(); } } - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( globalThis.SCRIPT_DEBUG ) { performance.measure( @@ -239,7 +301,10 @@ export default () => { start = performance.now(); } } - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( globalThis.SCRIPT_DEBUG ) { performance.measure( @@ -286,7 +351,13 @@ export default () => { start = performance.now(); } } - evaluate( entry, event ); + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + if ( ! result?.sync ) { + event = wrapEventAsync( event ); + } + result( event ); + } if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( globalThis.SCRIPT_DEBUG ) { performance.measure( @@ -332,7 +403,10 @@ export default () => { } entries.forEach( async ( entry ) => { await splitTask(); - evaluate( entry, event ); + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + result( event ); + } } ); }; } ); @@ -360,7 +434,10 @@ export default () => { .filter( isNonDefaultDirectiveSuffix ) .forEach( ( entry ) => { const className = entry.suffix; - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } const currentClass = element.props.class || ''; const classFinder = new RegExp( `(^|\\s)${ className }(\\s|$)`, @@ -400,7 +477,10 @@ export default () => { directive( 'style', ( { directives: { style }, element, evaluate } ) => { style.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => { const styleProp = entry.suffix; - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } element.props.style = element.props.style || {}; if ( typeof element.props.style === 'string' ) { element.props.style = cssStringToObject( element.props.style ); @@ -434,7 +514,10 @@ export default () => { directive( 'bind', ( { directives: { bind }, element, evaluate } ) => { bind.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => { const attribute = entry.suffix; - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } element.props[ attribute ] = result; /* @@ -535,7 +618,10 @@ export default () => { } try { - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } element.props.children = typeof result === 'object' ? null : result.toString(); } catch ( e ) { @@ -545,7 +631,13 @@ export default () => { // data-wp-run directive( 'run', ( { directives: { run }, evaluate } ) => { - run.forEach( ( entry ) => evaluate( entry ) ); + run.forEach( ( entry ) => { + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } + return result; + } ); } ); // data-wp-each--[item] @@ -567,7 +659,10 @@ export default () => { const [ entry ] = each; const { namespace } = entry; - const iterable = evaluate( entry ); + let iterable = evaluate( entry ); + if ( typeof iterable === 'function' ) { + iterable = iterable(); + } if ( typeof iterable?.[ Symbol.iterator ] !== 'function' ) { return; diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 7899e3eafd228..3d75fb03aa728 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -231,6 +231,7 @@ const resolve = ( path: string, namespace: string ) => { // Generate the evaluate function. export const getEvaluate: GetEvaluate = ( { scope } ) => + // TODO: When removing the temporarily remaining `value( ...args )` call below, remove the `...args` parameter too. ( entry, ...args ) => { let { value: path, namespace } = entry; if ( typeof path !== 'string' ) { @@ -241,7 +242,29 @@ export const getEvaluate: GetEvaluate = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); setScope( scope ); const value = resolve( path, namespace ); - const result = typeof value === 'function' ? value( ...args ) : value; + // Functions are returned without invoking them. + if ( typeof value === 'function' ) { + // Except if they have a negation operator present, for backward compatibility. + // This pattern is strongly discouraged and deprecated, and it will be removed in a near future release. + // TODO: Remove this condition to effectively ignore negation operator when provided with a function. + if ( hasNegationOperator ) { + warn( + 'Using a function with a negation operator is deprecated and will stop working in WordPress 6.9. Please use derived state instead.' + ); + const functionResult = ! value( ...args ); + resetScope(); + return functionResult; + } + // Reset scope before return and wrap the function so it will still run within the correct scope. + resetScope(); + return ( ...functionArgs: any[] ) => { + setScope( scope ); + const functionResult = value( ...functionArgs ); + resetScope(); + return functionResult; + }; + } + const result = value; resetScope(); return hasNegationOperator ? ! result : result; }; diff --git a/packages/interactivity/src/index.ts b/packages/interactivity/src/index.ts index 9d013e4e744ed..b7d68fd200705 100644 --- a/packages/interactivity/src/index.ts +++ b/packages/interactivity/src/index.ts @@ -27,6 +27,7 @@ export { useCallback, useMemo, splitTask, + withSyncEvent, } from './utils'; export { useState, useRef } from 'preact/hooks'; diff --git a/packages/interactivity/src/utils.ts b/packages/interactivity/src/utils.ts index d894d37a7b84b..7069088a8836b 100644 --- a/packages/interactivity/src/utils.ts +++ b/packages/interactivity/src/utils.ts @@ -30,6 +30,10 @@ declare global { } } +interface SyncAwareFunction extends Function { + sync?: boolean; +} + /** * Executes a callback function after the next frame is rendered. * @@ -135,11 +139,14 @@ export function withScope< ? Promise< Return > : never; export function withScope< Func extends Function >( func: Func ): Func; +export function withScope< Func extends SyncAwareFunction >( func: Func ): Func; export function withScope( func: ( ...args: unknown[] ) => unknown ) { const scope = getScope(); const ns = getNamespace(); + + let wrapped: Function; if ( func?.constructor?.name === 'GeneratorFunction' ) { - return async ( ...args: Parameters< typeof func > ) => { + wrapped = async ( ...args: Parameters< typeof func > ) => { const gen = func( ...args ) as Generator; let value: any; let it: any; @@ -171,17 +178,28 @@ export function withScope( func: ( ...args: unknown[] ) => unknown ) { return value; }; + } else { + wrapped = ( ...args: Parameters< typeof func > ) => { + setNamespace( ns ); + setScope( scope ); + try { + return func( ...args ); + } finally { + resetNamespace(); + resetScope(); + } + }; } - return ( ...args: Parameters< typeof func > ) => { - setNamespace( ns ); - setScope( scope ); - try { - return func( ...args ); - } finally { - resetNamespace(); - resetScope(); - } - }; + + // If function was annotated via `withSyncEvent()`, maintain the annotation. + const syncAware = func as SyncAwareFunction; + if ( syncAware.sync ) { + const syncAwareWrapped = wrapped as SyncAwareFunction; + syncAwareWrapped.sync = true; + return syncAwareWrapped; + } + + return wrapped; } /** @@ -374,3 +392,15 @@ export const isPlainObject = ( typeof candidate === 'object' && candidate.constructor === Object ); + +/** + * Indicates that the passed `callback` requires synchronous access to the event object. + * + * @param callback The event callback. + * @return Altered event callback. + */ +export function withSyncEvent( callback: Function ): SyncAwareFunction { + const syncAware = callback as SyncAwareFunction; + syncAware.sync = true; + return syncAware; +} diff --git a/phpunit/block-supports/aria-label-test.php b/phpunit/block-supports/aria-label-test.php new file mode 100644 index 0000000000000..52b14edd5b67c --- /dev/null +++ b/phpunit/block-supports/aria-label-test.php @@ -0,0 +1,85 @@ +test_block_name = null; + } + + public function tear_down() { + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + /** + * Registers a new block for testing aria-label support. + * + * @param string $block_name Name for the test block. + * @param array $supports Array defining block support configuration. + * + * @return WP_Block_Type The block type for the newly registered test block. + */ + private function register_aria_label_block_with_support( $block_name, $supports = array() ) { + $this->test_block_name = $block_name; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'supports' => $supports, + ) + ); + $registry = WP_Block_Type_Registry::get_instance(); + + return $registry->get_registered( $this->test_block_name ); + } + + /** + * Tests that position block support works as expected. + * + * @dataProvider data_aria_label_block_support + * + * @param boolean|array $support Aria label block support configuration. + * @param string $value Aria label value for attribute object. + * @param array $expected Expected aria label block support styles. + */ + public function test_gutenberg_apply_aria_label_support( $support, $value, $expected ) { + $block_type = self::register_aria_label_block_with_support( + 'test/aria-label-block', + array( 'ariaLabel' => $support ) + ); + $block_attrs = array( 'ariaLabel' => $value ); + $actual = gutenberg_apply_aria_label_support( $block_type, $block_attrs ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_aria_label_block_support() { + return array( + 'aria-label attribute is applied' => array( + 'support' => true, + 'value' => 'Label', + 'expected' => array( 'aria-label' => 'Label' ), + ), + 'aria-label attribute is not applied if block does not support it' => array( + 'support' => false, + 'value' => 'Label', + 'expected' => array(), + ), + ); + } +} diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 4eec377e3a94b..158b72e648cc2 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -1713,6 +1713,9 @@ ":focus": { "$ref": "#/definitions/stylesPropertiesComplete" }, + ":focus-visible": { + "$ref": "#/definitions/stylesPropertiesComplete" + }, ":hover": { "$ref": "#/definitions/stylesPropertiesComplete" }, @@ -1729,6 +1732,7 @@ ":active", ":any-link", ":focus", + ":focus-visible", ":hover", ":link", ":visited" diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js index 554bd8947f0bf..7830a934529aa 100644 --- a/test/e2e/specs/editor/blocks/buttons.spec.js +++ b/test/e2e/specs/editor/blocks/buttons.spec.js @@ -324,13 +324,13 @@ test.describe( 'Buttons', () => { await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Text"i]' ); - await page.click( 'role=button[name="Custom color picker."i]' ); + await page.click( 'role=button[name="Custom color picker"i]' ); await page.fill( 'role=textbox[name="Hex color"i]', 'ff0000' ); await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Background"i]' ); - await page.click( 'role=button[name="Custom color picker."i]' ); + await page.click( 'role=button[name="Custom color picker"i]' ); await page.fill( 'role=textbox[name="Hex color"i]', '00ff00' ); // Check the content. diff --git a/test/e2e/specs/editor/blocks/cover.spec.js b/test/e2e/specs/editor/blocks/cover.spec.js index 87c244a7306dc..bee2548c2305d 100644 --- a/test/e2e/specs/editor/blocks/cover.spec.js +++ b/test/e2e/specs/editor/blocks/cover.spec.js @@ -33,7 +33,7 @@ test.describe( 'Cover', () => { } ); // Locate the Black color swatch. - const blackColorSwatch = coverBlock.getByRole( 'option', { + const blackColorSwatch = coverBlock.getByRole( 'button', { name: 'Black', } ); await expect( blackColorSwatch ).toBeVisible(); @@ -105,7 +105,7 @@ test.describe( 'Cover', () => { // Choose a color swatch to transform the placeholder block into // a functioning block. await coverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); @@ -128,7 +128,7 @@ test.describe( 'Cover', () => { name: 'Block: Cover', } ); await coverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); @@ -240,7 +240,7 @@ test.describe( 'Cover', () => { // Choose a color swatch to transform the placeholder block into // a functioning block. await coverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); @@ -266,7 +266,7 @@ test.describe( 'Cover', () => { // Choose a color swatch to transform the placeholder block into // a functioning block. await secondCoverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js index 906095cad9d08..6ff7e11bb334e 100644 --- a/test/e2e/specs/editor/blocks/heading.spec.js +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -184,7 +184,7 @@ test.describe( 'Heading', () => { await textColor.click(); await page - .getByRole( 'button', { name: /Custom color picker./i } ) + .getByRole( 'button', { name: /Custom color picker/i } ) .click(); await page diff --git a/test/e2e/specs/editor/plugins/block-hooks.spec.js b/test/e2e/specs/editor/plugins/block-hooks.spec.js index ec78db6b3a8fe..969f8ebb7091e 100644 --- a/test/e2e/specs/editor/plugins/block-hooks.spec.js +++ b/test/e2e/specs/editor/plugins/block-hooks.spec.js @@ -3,12 +3,14 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -const dummyBlockContent = ` +const dummyBlocksContent = `

This is a dummy heading

This is a dummy paragraph.

`; +const dummyClassicContent = + '

This is a dummy heading

This is a dummy paragraph.

'; const getHookedBlockClassName = ( relativePosition, anchorBlock ) => `hooked-block-${ relativePosition }-${ anchorBlock.replace( @@ -34,13 +36,13 @@ test.describe( 'Block Hooks API', () => { createMethod: 'createBlock', }, ].forEach( ( { name, postType, blockType, createMethod } ) => { - test.describe( `Hooked blocks in ${ name }`, () => { + test.describe( `Hooked blocks in ${ name } (blocks)`, () => { let postObject, containerPost; test.beforeAll( async ( { requestUtils } ) => { postObject = await requestUtils[ createMethod ]( { title: name, status: 'publish', - content: dummyBlockContent, + content: dummyBlocksContent, } ); await requestUtils.activatePlugin( @@ -162,6 +164,119 @@ test.describe( 'Block Hooks API', () => { ] ); } ); } ); + + test.describe( `Hooked blocks in ${ name } (classic)`, () => { + let postObject, containerPost; + test.beforeAll( async ( { requestUtils } ) => { + postObject = await requestUtils[ createMethod ]( { + title: name, + status: 'publish', + content: dummyClassicContent, + } ); + + await requestUtils.activatePlugin( + 'gutenberg-test-block-hooks' + ); + + if ( postType !== 'post' ) { + // We need a container post to hold our block instance. + containerPost = await requestUtils.createPost( { + title: `Block Hooks in ${ name }`, + status: 'publish', + content: ``, + meta: { + // Prevent Block Hooks from injecting blocks into the container + // post content so they won't distract from the ones injected + // into the block instance. + _wp_ignored_hooked_blocks: '["core/paragraph"]', + }, + } ); + } else { + containerPost = postObject; + } + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-hooks' + ); + + await requestUtils.deleteAllPosts(); + await requestUtils.deleteAllBlocks(); + } ); + + test( `should insert hooked blocks into ${ name } on frontend`, async ( { + page, + } ) => { + await page.goto( `/?p=${ containerPost.id }` ); + await expect( + page.locator( '.entry-content > *' ) + ).toHaveClass( [ + 'dummy-heading', + 'dummy-paragraph', + getHookedBlockClassName( 'last_child', blockType ), + ] ); + } ); + + test( `should insert hooked blocks into ${ name } in editor and respect changes made there`, async ( { + admin, + editor, + page, + } ) => { + const expectedHookedBlockLastChild = { + name: 'core/paragraph', + attributes: { + className: getHookedBlockClassName( + 'last_child', + blockType + ), + }, + }; + + await admin.editPost( postObject.id ); + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ + { name: 'core/freeform' }, + expectedHookedBlockLastChild, + ] ); + + const hookedBlock = editor.canvas.getByText( + getHookedBlockContent( 'last_child', blockType ) + ); + await editor.selectBlocks( hookedBlock ); + await editor.clickBlockToolbarButton( 'Move up' ); + + // Save updated post. + const saveButton = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save', exact: true } ); + await saveButton.click(); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'updated' } ) + .waitFor(); + + // Reload and verify that the new position of the hooked block has been persisted. + await page.reload(); + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ + expectedHookedBlockLastChild, + { name: 'core/freeform' }, + ] ); + + // Verify that the frontend reflects the changes made in the editor. + await page.goto( `/?p=${ containerPost.id }` ); + await expect( + page.locator( '.entry-content > *' ) + ).toHaveClass( [ + getHookedBlockClassName( 'last_child', blockType ), + 'dummy-heading', + 'dummy-paragraph', + ] ); + } ); + } ); } ); test.describe( 'Hooked blocks in Navigation Menu', () => { diff --git a/test/e2e/specs/editor/various/block-editor-dark-background.spec.js b/test/e2e/specs/editor/various/block-editor-dark-background.spec.js new file mode 100644 index 0000000000000..dc333f31c7ed9 --- /dev/null +++ b/test/e2e/specs/editor/various/block-editor-dark-background.spec.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block editor with dark background theme', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'darktheme' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Block editor iframe body', () => { + test( 'Should have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); + +test.describe( 'Block editor with light background theme', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyfour' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Block editor iframe body', () => { + test( 'Should not have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).not.toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 988683c8d11aa..98dfe5e304f80 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -162,10 +162,10 @@ test.describe( 'List View', () => { // make the inner blocks appear. await editor.canvas .getByRole( 'document', { name: 'Block: Cover' } ) - .getByRole( 'listbox', { - name: 'Custom color picker.', + .getByRole( 'group', { + name: 'Overlay color', } ) - .getByRole( 'option' ) + .getByRole( 'button' ) .first() .click(); diff --git a/test/e2e/specs/site-editor/navigation.spec.js b/test/e2e/specs/site-editor/navigation.spec.js index 18eb6c9904b44..2a886c2048f88 100644 --- a/test/e2e/specs/site-editor/navigation.spec.js +++ b/test/e2e/specs/site-editor/navigation.spec.js @@ -106,6 +106,23 @@ test.describe( 'Site editor navigation', () => { // We should have our editor canvas button back await expect( editorCanvasButton ).toBeVisible(); } ); + + test( 'Should show 404 page when navigating to non-existent template', async ( { + admin, + page, + } ) => { + // Navigate to a non-existent template. + await admin.visitAdminPage( 'site-editor.php', 'p=/template-foo-bar' ); + + // Verify the 404 error notice is displayed with the correct message. + await expect( + page.locator( + '.edit-site-layout__area .components-notice__content' + ) + ).toHaveText( + 'The requested page could not be found. Please check the URL.' + ); + } ); } ); class EditorNavigationUtils { diff --git a/test/e2e/specs/site-editor/site-editor-dark-background.spec.js b/test/e2e/specs/site-editor/site-editor-dark-background.spec.js new file mode 100644 index 0000000000000..1bf49c196bdfc --- /dev/null +++ b/test/e2e/specs/site-editor/site-editor-dark-background.spec.js @@ -0,0 +1,75 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Site editor with dark background theme', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'darktheme' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.visitSiteEditor(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Site editor iframe body', () => { + test( 'Should have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); + +test.describe( 'Site editor with light background theme and theme variations', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyfour' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.visitSiteEditor(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Site editor iframe body', () => { + test( 'Should not have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).not.toHaveClass( /is-dark-theme/ ); + } ); + + test( 'Should add and remove the is-dark-theme CSS class with dark and light theme variation', async ( { + page, + editor, + } ) => { + // Click "Styles" + await page.getByRole( 'button', { name: 'Styles' } ).click(); + + // Click "Browse styles" + await page.getByRole( 'button', { name: 'Browse styles' } ).click(); + + const canvasBody = editor.canvas.locator( 'body' ); + + // Activate "Maelstrom" Theme Variation. + await page.getByRole( 'button', { name: 'Maelstrom' } ).click(); + + await expect( canvasBody ).toHaveClass( /is-dark-theme/ ); + + // Activate "Ember" Theme Variation. + await page.getByRole( 'button', { name: 'Ember' } ).click(); + + await expect( canvasBody ).not.toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); diff --git a/test/gutenberg-test-themes/darktheme/block-templates/index.html b/test/gutenberg-test-themes/darktheme/block-templates/index.html new file mode 100644 index 0000000000000..0283daeb54c6f --- /dev/null +++ b/test/gutenberg-test-themes/darktheme/block-templates/index.html @@ -0,0 +1,11 @@ + +
+ + + + +
+ + +

My awesome paragraph

+ diff --git a/test/gutenberg-test-themes/darktheme/block-templates/singular.html b/test/gutenberg-test-themes/darktheme/block-templates/singular.html new file mode 100644 index 0000000000000..cd05d5fe917fe --- /dev/null +++ b/test/gutenberg-test-themes/darktheme/block-templates/singular.html @@ -0,0 +1,2 @@ + + diff --git a/test/gutenberg-test-themes/darktheme/index.php b/test/gutenberg-test-themes/darktheme/index.php new file mode 100644 index 0000000000000..0c6530acc1aaf --- /dev/null +++ b/test/gutenberg-test-themes/darktheme/index.php @@ -0,0 +1,9 @@ +